veilmap/fog.js
2026-04-20 05:56:05 -04:00

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 };