110 lines
2.9 KiB
JavaScript
110 lines
2.9 KiB
JavaScript
export class FogCanvas {
|
|
constructor(canvasEl, mapInstance) {
|
|
this.canvas = canvasEl;
|
|
this.ctx = canvasEl.getContext('2d');
|
|
this.map = mapInstance;
|
|
this.points = []; // { lngLat: [lng,lat], radiusMeters: number }
|
|
this.dirty = true;
|
|
this._raf = null;
|
|
this.resize();
|
|
}
|
|
|
|
addErasePoint(lngLat, radiusMeters) {
|
|
this.points.push({ lngLat, radiusMeters });
|
|
// Bake if over 2000 points
|
|
if (this.points.length > 2000) this._bake();
|
|
this.dirty = true;
|
|
this._scheduleRender();
|
|
}
|
|
|
|
render() {
|
|
const { ctx, canvas } = this;
|
|
const w = canvas.width, h = canvas.height;
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
// If we have a baked image, draw it first then punch through it
|
|
if (this._bakedImg) {
|
|
ctx.drawImage(this._bakedImg, 0, 0, w, h);
|
|
} else {
|
|
ctx.fillStyle = 'rgba(180,185,195,0.88)';
|
|
ctx.fillRect(0, 0, w, h);
|
|
}
|
|
|
|
ctx.globalCompositeOperation = 'destination-out';
|
|
for (const pt of this.points) {
|
|
const px = this.map.project(pt.lngLat);
|
|
const r = metersToPixels(this.map, pt.lngLat, pt.radiusMeters);
|
|
if (r < 0.5) continue;
|
|
const grad = ctx.createRadialGradient(px.x, px.y, 0, px.x, px.y, r);
|
|
grad.addColorStop(0, 'rgba(0,0,0,1)');
|
|
grad.addColorStop(0.3, 'rgba(0,0,0,0.85)');
|
|
grad.addColorStop(0.55, 'rgba(0,0,0,0.5)');
|
|
grad.addColorStop(0.8, 'rgba(0,0,0,0.15)');
|
|
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
|
ctx.fillStyle = grad;
|
|
ctx.beginPath();
|
|
ctx.arc(px.x, px.y, r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
this.dirty = false;
|
|
}
|
|
|
|
reset() {
|
|
this.points = [];
|
|
this._bakedImg = null;
|
|
this.dirty = true;
|
|
this.render();
|
|
}
|
|
|
|
setBrushRadius(meters) {
|
|
this._brushRadius = meters;
|
|
}
|
|
|
|
resize() {
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const w = this.canvas.clientWidth;
|
|
const h = this.canvas.clientHeight;
|
|
this.canvas.width = w * dpr;
|
|
this.canvas.height = h * dpr;
|
|
this.ctx.scale(dpr, dpr);
|
|
this.dirty = true;
|
|
this._scheduleRender();
|
|
}
|
|
|
|
markDirty() {
|
|
this.dirty = true;
|
|
this._scheduleRender();
|
|
}
|
|
|
|
_scheduleRender() {
|
|
if (this._raf) return;
|
|
this._raf = requestAnimationFrame(() => {
|
|
this._raf = null;
|
|
if (this.dirty) this.render();
|
|
});
|
|
}
|
|
|
|
_bake() {
|
|
// Rasterize current fog state to an offscreen canvas
|
|
const off = document.createElement('canvas');
|
|
off.width = this.canvas.width;
|
|
off.height = this.canvas.height;
|
|
const octx = off.getContext('2d');
|
|
octx.drawImage(this.canvas, 0, 0);
|
|
this._bakedImg = off;
|
|
this.points = [];
|
|
}
|
|
}
|
|
|
|
function metersToPixels(map, lngLat, meters) {
|
|
const lat = lngLat[1] * Math.PI / 180;
|
|
const metersPerPixel = (40075016.686 * Math.cos(lat)) / (256 * Math.pow(2, map.getZoom()));
|
|
return meters / metersPerPixel;
|
|
}
|
|
|
|
export { metersToPixels };
|
|
|
|
|
|
|