2026-06-01 08:40:23 +02:00

173 lines
4.0 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
/** @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
*/
export 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. */
export 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).
*/
export 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) => {
if (i === 0) {
ctx.moveTo(p.x, p.y);
} else {
ctx.lineTo(p.x, p.y);
}
});
ctx.stroke();
}
/**
* Return a copy of the current drawn points.
* @returns {{ x: number, y: number }[]}
*/
export function getPoints() {
return [...points];
}
/** Remove event listeners and clean up. */
export function destroy() {
window.removeEventListener("resize", resize);
}