From ac1fb1538d341891c1010aef9c24f1f187e6aa82 Mon Sep 17 00:00:00 2001 From: Andrea Manzi Date: Sat, 24 Oct 2026 16:32:02 +0200 Subject: [PATCH] =?UTF-8?q?I=20nomi=20del=20=C2=AB=3F=C2=BB=20non=20si=20s?= =?UTF-8?q?ovrappongono=20pi=C3=B9:=20ognuno=20cerca=20un=20posto=20libero?= =?UTF-8?q?=20vicino=20al=20suo=20segno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit placeLabels prova sopra, sotto, più lontano e di lato, con un po' d'aria fra un nome e l'altro; una lineetta collega il nome al segno quando finisce lontano. Un test controlla tutte le scene, con tutti gli oggetti accesi. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh --- src/engine/render.js | 58 +++++++++++++++++++++++++++++++++++-------- tests/render.test.mjs | 15 +++++++++++ 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/engine/render.js b/src/engine/render.js index 7acb4bb..1c2a32c 100644 --- a/src/engine/render.js +++ b/src/engine/render.js @@ -169,28 +169,66 @@ export function render(ctx, game, { hover, spots, debug } = {}) { } // Il pulsante «?»: un segno su ogni cosa che si può toccare, col suo nome. Le uscite in giallo, il resto in bianco. +// I nomi non si sovrappongono: placeLabels cerca per ognuno un posto libero vicino al suo segno. function drawSpots(ctx, game) { ctx.save(); ctx.font = '6px "Press Start 2P", monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; - for (const h of game.hotspots()) { - const [x, y, w, hh] = h.box, cx = Math.round(x + w / 2), cy = Math.round(y + hh / 2); - const color = h.exit ? '#ffd27a' : '#ffffff'; + const spots = game.hotspots().map(h => { + const [x, y, w, hh] = h.box; + return { h, x: Math.round(x + w / 2), y: Math.round(y + hh / 2), w: ctx.measureText(h.name).width }; + }); + const labels = placeLabels(spots); + spots.forEach(({ h, x, y }, i) => { + const color = h.exit ? '#ffd27a' : '#ffffff', l = labels[i]; + // Nome spostato lontano dal segno: una lineetta li collega. + if (Math.abs(l.y - LABEL_H / 2 - y) > LABEL_H * 1.6 || Math.abs(l.x - x) > 4) { + ctx.strokeStyle = 'rgba(0,0,0,0.6)'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(l.x, l.y - LABEL_H / 2); + ctx.stroke(); + } ctx.fillStyle = 'rgba(0,0,0,0.55)'; - ctx.fillRect(cx - 4, cy - 4, 9, 9); + ctx.fillRect(x - 4, y - 4, 9, 9); ctx.fillStyle = color; - ctx.fillRect(cx - 3, cy - 3, 7, 7); + ctx.fillRect(x - 3, y - 3, 7, 7); ctx.lineWidth = 3; ctx.strokeStyle = '#000'; - // Il nome resta dentro la scena anche per le uscite sui bordi. - const half = ctx.measureText(h.name).width / 2, tx = Math.max(half + 2, Math.min(WIDTH - half - 2, cx)), ty = Math.max(10, cy - 6); - ctx.strokeText(h.name, tx, ty); - ctx.fillText(h.name, tx, ty); - } + ctx.strokeText(h.name, l.x, l.y); + ctx.fillText(h.name, l.x, l.y); + }); ctx.restore(); } +// Dove scrivere i nomi del «?»: per ogni segno { x, y, w } (centro e larghezza del testo) il punto { x, y } della base +// del testo, centrato. Si prova sopra il segno, sotto, poi sempre più lontano e di lato; vince il primo posto che non +// tocca né i nomi già messi né i segni, e sta dentro la scena. Prima i nomi dei segni più in alto. +const LABEL_H = 8; +export function placeLabels(spots, { width = WIDTH, height = HEIGHT } = {}) { + const boxes = spots.map(s => ({ l: s.x - 5, r: s.x + 5, t: s.y - 5, b: s.y + 5 })); + const hit = (a, b) => a.l < b.r && b.l < a.r && a.t < b.b && b.t < a.b; + const out = new Array(spots.length); + const order = spots.map((s, i) => i).sort((a, b) => spots[a].y - spots[b].y || spots[a].x - spots[b].x); + for (const i of order) { + const s = spots[i], half = s.w / 2 + 1; + const tries = []; + for (const dy of [-6, 13, -15, 22, -24, 31, -33, 40]) for (const dx of [0, -half - 6, half + 6]) tries.push([dx, dy]); + let best = null; + for (const [dx, dy] of tries) { + const x = Math.max(half + 2, Math.min(width - half - 2, s.x + dx)), y = Math.max(LABEL_H + 2, Math.min(height - 2, s.y + dy)); + const box = { l: x - half - 3, r: x + half + 3, t: y - LABEL_H - 1, b: y + 1 }; // con un po' d'aria intorno + if (!boxes.some(b => hit(b, box))) { best = { x, y, box }; break; } + best ??= { x, y, box }; // tutto pieno: almeno il primo + } + out[i] = { x: best.x, y: best.y }; + boxes.push(best.box); + } + return out; +} + // Modalità debug (?debug nell'indirizzo): area calpestabile coi vertici numerati, tutti gli hotspot col loro id // (tratteggiati quelli spenti dal loro `when`), punti dove si va (`at`), dove si parla e dove stanno i personaggi. // In basso le coordinate del puntatore e i punti segnati con Maiusc+clic, per scrivere scene a mano. diff --git a/tests/render.test.mjs b/tests/render.test.mjs index 41cd62a..5450433 100644 --- a/tests/render.test.mjs +++ b/tests/render.test.mjs @@ -41,3 +41,18 @@ test('uno sfondo che non c\'è proprio si lascia stare dopo pochi tentativi', as delete globalThis.Image; } }); + +test('i nomi del «?» non si sovrappongono, in nessuna scena', async () => { + const { placeLabels } = await import('../src/engine/render.js'); + const { story } = await import('../src/story/story.js'); + const H = 8, W = 6; // Press Start 2P a 6 px: 6 px a lettera + for (const [id, sc] of Object.entries(story.scenes)) { + const spots = (sc.hotspots ?? []).map(h => ({ x: Math.round(h.box[0] + h.box[2] / 2), y: Math.round(h.box[1] + h.box[3] / 2), w: h.name.length * W, name: h.name })); + const boxes = placeLabels(spots).map((p, i) => ({ l: p.x - spots[i].w / 2 - 1, r: p.x + spots[i].w / 2 + 1, t: p.y - H, b: p.y, name: spots[i].name })); + for (const [i, a] of boxes.entries()) { + assert.ok(a.l >= 0 && a.r <= 640 && a.t >= 0 && a.b <= 360, `${id}: «${a.name}» esce dalla scena`); + for (const b of boxes.slice(i + 1)) + assert.ok(!(a.l < b.r && b.l < a.r && a.t < b.b && b.t < a.b), `${id}: «${a.name}» e «${b.name}» si sovrappongono`); + } + } +});