I nomi del «?» non si sovrappongono più: ognuno cerca un posto libero vicino al suo segno
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
+48
-10
@@ -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.
|
// 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) {
|
function drawSpots(ctx, game) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = '6px "Press Start 2P", monospace';
|
ctx.font = '6px "Press Start 2P", monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'bottom';
|
ctx.textBaseline = 'bottom';
|
||||||
for (const h of game.hotspots()) {
|
const spots = game.hotspots().map(h => {
|
||||||
const [x, y, w, hh] = h.box, cx = Math.round(x + w / 2), cy = Math.round(y + hh / 2);
|
const [x, y, w, hh] = h.box;
|
||||||
const color = h.exit ? '#ffd27a' : '#ffffff';
|
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.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.fillStyle = color;
|
||||||
ctx.fillRect(cx - 3, cy - 3, 7, 7);
|
ctx.fillRect(x - 3, y - 3, 7, 7);
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
ctx.strokeStyle = '#000';
|
ctx.strokeStyle = '#000';
|
||||||
// Il nome resta dentro la scena anche per le uscite sui bordi.
|
ctx.strokeText(h.name, l.x, l.y);
|
||||||
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.fillText(h.name, l.x, l.y);
|
||||||
ctx.strokeText(h.name, tx, ty);
|
});
|
||||||
ctx.fillText(h.name, tx, ty);
|
|
||||||
}
|
|
||||||
ctx.restore();
|
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
|
// 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.
|
// (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.
|
// In basso le coordinate del puntatore e i punti segnati con Maiusc+clic, per scrivere scene a mano.
|
||||||
|
|||||||
@@ -41,3 +41,18 @@ test('uno sfondo che non c\'è proprio si lascia stare dopo pochi tentativi', as
|
|||||||
delete globalThis.Image;
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user