173 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 (0100).
*/
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 };
})();