Il libro dell'oratorio in primo piano: copertina, frontespizio, una pagina che gira col fruscio e «FINE» (immagini di Codex, testi disegnati dal codice); l'Arcu a scelte (Borgo e la Casa dell'Armando); il cancello dell'oratorio e l'A. Bettini
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
+80
-1
@@ -1,6 +1,6 @@
|
||||
// Disegno della scena su un canvas 640×360, ingrandito dal CSS senza sfocatura.
|
||||
// Le forme provvisorie (`art`) verranno sostituite da sfondi e sprite in pixel art.
|
||||
import { check } from './game.js';
|
||||
import { check, PAGE_TURN } from './game.js';
|
||||
import { ambientEffects, ambientHash } from './ambient.js';
|
||||
import { versioned } from '../version.js';
|
||||
export const WIDTH = 640, HEIGHT = 360;
|
||||
@@ -170,6 +170,7 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now(
|
||||
drawLights(ctx, game, time);
|
||||
for (const shape of art) if (shape.top) drawShape(ctx, shape);
|
||||
if (spots) drawSpots(ctx, game);
|
||||
if (game.closeup) drawCloseup(ctx, game, time);
|
||||
if (debug) drawDebug(ctx, game, debug);
|
||||
if (hover) {
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
|
||||
@@ -644,3 +645,81 @@ function drawDebug(ctx, game, { mouse, points = [], help }) {
|
||||
label(`${game.state.scene} ${where} ${help ?? ''}`, 2, HEIGHT - 3, '#fff');
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// --- Il primo piano di un libro (story.closeups, game.closeup): la scena si oscura, le mani alzano il libro dal basso.
|
||||
// Le immagini hanno etichetta e pagine vuote: i testi si scrivono qui, nitidi. Una pagina sola gira, a fisarmonica
|
||||
// sulla piega; sotto c'è già l'ultima pagina.
|
||||
const ink = '#3a1f12', FONT = '"Press Start 2P", monospace';
|
||||
function pageText(ctx, lines, [x, y, w, h], size, at = 0.5, color = ink) {
|
||||
ctx.font = `${size}px ${FONT}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = color;
|
||||
const gap = size * 1.7, top = y + h * at - (lines.length - 1) * gap / 2;
|
||||
lines.forEach((line, i) => ctx.fillText(line, Math.round(x + w / 2), Math.round(top + i * gap)));
|
||||
}
|
||||
const twoLines = title => { const i = title.lastIndexOf(' '); return i > 0 ? [title.slice(0, i), title.slice(i + 1)] : [title]; };
|
||||
|
||||
function drawCloseup(ctx, game, time) {
|
||||
const c = game.closeup, def = game.story.closeups?.[c.id];
|
||||
if (!def) return;
|
||||
ctx.save();
|
||||
ctx.fillStyle = `rgba(8, 5, 12, ${Math.min(0.78, c.age / 0.35 * 0.78)})`;
|
||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
const src = c.step === 'cover' ? def.closed : def.open, img = backgrounds.get(src);
|
||||
if (!img) { loadBackground(src); loadBackground(c.step === 'cover' ? def.open : def.closed); ctx.restore(); return; }
|
||||
// Il libro sale dal basso (solo all'inizio), con le braccia che entrano dal bordo.
|
||||
const rise = 1 - Math.min(1, c.age / 0.45), ox = Math.round((WIDTH - img.width) / 2);
|
||||
const oy = Math.round(HEIGHT - img.height + 140 * rise * rise);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(img, ox, oy);
|
||||
ctx.translate(ox, oy);
|
||||
const lines = twoLines(def.title), { left, right, spine } = def.pages;
|
||||
if (c.step === 'cover') pageText(ctx, lines, def.label, 8);
|
||||
else {
|
||||
const flip = c.step === 'turn' ? Math.min(1, c.t / PAGE_TURN) : c.step === 'end' ? 1 : 0;
|
||||
// Sotto la pagina che gira c'è già l'ultima: «FINE», e nient'altro.
|
||||
if (flip > 0) pageText(ctx, [def.last], right, 20, 0.46);
|
||||
else pageText(ctx, lines, right, 8, 0.32);
|
||||
if (flip > 0 && flip < 1) {
|
||||
// La pagina che gira: si stringe sulla piega e passa a sinistra, col dorso un po' più scuro.
|
||||
const e = (1 - Math.cos(Math.PI * flip)) / 2, k = Math.cos(Math.PI * e);
|
||||
const lift = Math.round(Math.sin(Math.PI * e) * 10), [, y, , h] = right, pw = right[0] + right[2] - spine;
|
||||
ctx.save();
|
||||
ctx.translate(spine, 0);
|
||||
ctx.scale(k || 0.001, 1);
|
||||
ctx.fillStyle = k > 0 ? def.paper : '#e6cc9c';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y); ctx.lineTo(pw, y - lift); ctx.lineTo(pw, y + h - lift); ctx.lineTo(0, y + h);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(58, 31, 18, 0.55)';
|
||||
ctx.lineWidth = 1 / Math.max(0.2, Math.abs(k));
|
||||
ctx.stroke();
|
||||
if (k > 0) pageText(ctx, lines, [right[0] - spine, y - lift, right[2], h], 8, 0.32);
|
||||
ctx.restore();
|
||||
}
|
||||
// Dove toccare per sfogliare: l'angolo della pagina che pulsa.
|
||||
if (c.step === 'title' && !game.speech) {
|
||||
const pulse = 0.5 + 0.5 * Math.sin(time / 260), cx = right[0] + right[2] - 2, cy = right[1] + right[3] - 2, d = 12 + 3 * pulse;
|
||||
ctx.fillStyle = `rgba(232, 194, 90, ${0.55 + 0.4 * pulse})`;
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx - d, cy); ctx.lineTo(cx, cy - d); ctx.closePath(); ctx.fill();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
// Il suggerimento in alto, finché c'è qualcosa da sfogliare.
|
||||
const hint = c.step === 'cover' ? 'Clic o tocco: apri il libro' : c.step === 'title' ? 'Clic o tocco: gira la pagina' : null;
|
||||
if (hint && !game.speech && c.age > 0.5) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.65 + 0.35 * Math.sin(time / 300);
|
||||
ctx.font = `7px ${FONT}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
|
||||
const tw = ctx.measureText(hint).width;
|
||||
ctx.fillRect(WIDTH / 2 - tw / 2 - 6, 8, tw + 12, 14);
|
||||
ctx.fillStyle = '#ffd27a';
|
||||
ctx.fillText(hint, WIDTH / 2, 15);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user