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