// drawing.js — canvas drawing module const Drawing = (() => { /** @type {HTMLCanvasElement} */ let canvas; /** @type {CanvasRenderingContext2D} */ let ctx; let isDrawing = false; /** @type {{ x: number, y: number }[]} */ let points = []; /** @type {{ name: string, x: number, y: number }[]} */ let cities = []; const STROKE_COLOR = "#1a7fc4"; const STROKE_WIDTH = 2.5; /** * Initialise the drawing module on a canvas element. * @param {HTMLCanvasElement} canvasEl */ function init(canvasEl) { canvas = canvasEl; ctx = canvas.getContext("2d"); canvas.addEventListener("pointerdown", onDown); canvas.addEventListener("pointermove", onMove); canvas.addEventListener("pointerup", onUp); canvas.addEventListener("pointerleave", onUp); canvas.style.touchAction = "none"; _resize(); window.addEventListener("resize", _resize); } /** Resize canvas to match its CSS size, accounting for device pixel ratio. */ function _resize() { if (!canvas) return; const { width, height } = canvas.getBoundingClientRect(); canvas.width = width * window.devicePixelRatio; canvas.height = height * window.devicePixelRatio; ctx.scale(window.devicePixelRatio, window.devicePixelRatio); _redraw(); } /** * Convert a pointer event to canvas-local coordinates. * @param {PointerEvent} e * @returns {{ x: number, y: number }} */ function _pos(e) { const rect = canvas.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; } /** @param {PointerEvent} e */ function onDown(e) { e.preventDefault(); isDrawing = true; const p = _pos(e); points.push(p); ctx.beginPath(); ctx.moveTo(p.x, p.y); } /** @param {PointerEvent} e */ function onMove(e) { if (!isDrawing) return; e.preventDefault(); const p = _pos(e); points.push(p); ctx.lineTo(p.x, p.y); ctx.strokeStyle = STROKE_COLOR; ctx.lineWidth = STROKE_WIDTH; ctx.lineJoin = "round"; ctx.lineCap = "round"; ctx.stroke(); } /** @param {PointerEvent} e */ function onUp(e) { if (!isDrawing) return; isDrawing = false; e.preventDefault(); } /** Clear the canvas and redraw city markers. */ function clear() { points = []; if (!ctx) return; const { width, height } = canvas.getBoundingClientRect(); ctx.clearRect(0, 0, width, height); _drawCities(); } /** * Set city markers to display on the canvas. * @param {{ name: string, x: number, y: number }[]} cityList - Coords in percent (0–100). */ function setCities(cityList) { cities = cityList || []; _drawCities(); } /** Render all city markers with labels. */ function _drawCities() { if (!ctx || !cities.length) return; const { width, height } = canvas.getBoundingClientRect(); cities.forEach((city) => { const cx = (city.x / 100) * width; const cy = (city.y / 100) * height; // Dot ctx.beginPath(); ctx.arc(cx, cy, 5, 0, Math.PI * 2); ctx.fillStyle = "rgba(240,180,40,0.9)"; ctx.fill(); ctx.strokeStyle = "rgba(255,255,255,0.9)"; ctx.lineWidth = 1.5; ctx.stroke(); // White pill label background ctx.font = "bold 11px 'DM Sans', sans-serif"; const textW = ctx.measureText(city.name).width + 10; const textH = 16; const tx = cx; const ty = cy - 14; ctx.save(); ctx.fillStyle = "rgba(255,255,255,0.88)"; ctx.beginPath(); ctx.roundRect(tx - textW / 2, ty - textH / 2 - 1, textW, textH, 4); ctx.fill(); ctx.restore(); ctx.fillStyle = "#0b1f2a"; ctx.textAlign = "center"; ctx.fillText(city.name, tx, ty + 4); }); } /** Redraw the full stroke from stored points. */ function _redraw() { _drawCities(); if (!points.length) return; ctx.beginPath(); ctx.strokeStyle = STROKE_COLOR; ctx.lineWidth = STROKE_WIDTH; ctx.lineJoin = "round"; ctx.lineCap = "round"; points.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y), ); ctx.stroke(); } /** * Return a copy of the current drawn points. * @returns {{ x: number, y: number }[]} */ function getPoints() { return [...points]; } /** Remove event listeners and clean up. */ function destroy() { window.removeEventListener("resize", _resize); } return { init, clear, setCities, getPoints, destroy }; })();