- Il risveglio (src/engine/risveglio.js): primo piano sopra la scena, con le battute mentre le palpebre si aprono e i vecchietti che danno la notizia del palio; «Salta» lo salta. - Il telefonino del Barone (src/engine/telefono.js): sale dal basso come il libro, la home di sanpiero50037.it dalla Wayback Machine nello schermo, il mattone del muro apre il muro. - Il muro (assets/muro, tools/muro.py): la pagina vera rifatta statica dal muro riesumato, coi 24 messaggi del dump (senza email né IP) e quelli del gioco, che arrivano con la storia; «Scrivi» spento. - Il gioco sgancia i service worker di altre app rimasti sul suo indirizzo (la Bussola su localhost:8080). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
82 lines
4.5 KiB
JavaScript
82 lines
4.5 KiB
JavaScript
// Il telefonino del Barone (story.closeups, type 'telefono'; Andrea, 31 ottobre: «prenda il cellulare, nello stile
|
||
// di come si sfoglia la storia infinita, e quando si clicca viene a tutto schermo il sito di San Piero»).
|
||
// Come il libro: la scena si scurisce e le mani alzano il telefonino dal basso; nello schermo c'è il sito
|
||
// (`site`, un'immagine 16:9 della home di sanpiero50037.it). Un tocco: lo schermo si allarga a tutta la scena.
|
||
// Sul sito si tocca il link (`link`, in pixel dell'immagine del sito) e si apre il muro: una pagina vera
|
||
// (assets/muro, la mette main.js sopra la scena), che qui sotto lascia solo il fondo. A muro aperto partono le
|
||
// battute `comment`, e chiudendo `closing`. Unità: i pixel della scena 640×360.
|
||
|
||
export const RISE = 0.45, ZOOM = 0.55;
|
||
const W = 640, H = 360;
|
||
const clamp01 = k => Math.max(0, Math.min(1, k));
|
||
const ease = k => (1 - Math.cos(Math.PI * clamp01(k))) / 2;
|
||
|
||
export function newTelefono(id) { return { id, step: 'phone', t: 0, age: 0, aim: null }; }
|
||
|
||
// Il tempo: lo zoom finisce sul sito; a muro aperto, dopo un attimo, le battute (una alla volta, come il libro).
|
||
export function updateTelefono(c, dt) {
|
||
c.t += dt;
|
||
if (c.step === 'zoom' && c.t >= ZOOM) Object.assign(c, { step: 'site', t: 0 });
|
||
}
|
||
|
||
// Dove sta il link del muro sulla scena, a sito aperto: il riquadro `link` riportato da immagine a scena.
|
||
export function linkBox(def, site) {
|
||
const [x, y, w, h] = def.link, kx = W / site[0], ky = H / site[1];
|
||
return [x * kx, y * ky, w * kx, h * ky];
|
||
}
|
||
const inside = ([x, y, w, h], [px, py]) => px >= x && px <= x + w && py >= y && py <= y + h;
|
||
|
||
// Un tocco: sul telefonino apre il sito; sul sito, solo sul link, apre il muro. Torna true se è successo qualcosa.
|
||
export function tapTelefono(c, def) {
|
||
if (c.step === 'phone') { if (c.age < RISE) return false; Object.assign(c, { step: 'zoom', t: 0 }); return true; }
|
||
if (c.step === 'site' && c.aim && inside(linkBox(def, def.siteSize), c.aim)) { Object.assign(c, { step: 'wall', t: 0 }); return true; }
|
||
return false;
|
||
}
|
||
|
||
// Il riquadro dello schermo sulla scena, col telefonino che sale dal basso.
|
||
function phoneRect(def, img, age) {
|
||
const rise = 1 - clamp01(age / RISE), k = def.phoneScale ?? 1;
|
||
const w = img.width * k, h = img.height * k, x = Math.round((W - w) / 2), y = Math.round(H - h + 140 * rise * rise);
|
||
return { x, y, w, h, k };
|
||
}
|
||
|
||
export function drawTelefono(ctx, c, def, { phone, site }, time) {
|
||
ctx.save();
|
||
ctx.fillStyle = `rgba(8, 5, 12, ${Math.min(0.78, c.age / 0.35 * 0.78)})`;
|
||
ctx.fillRect(0, 0, W, H);
|
||
if (!phone || !site) { ctx.restore(); return; }
|
||
const p = phoneRect(def, phone, c.age), [sx, sy, sw, sh] = def.screen;
|
||
const screen = [p.x + sx * p.k, p.y + sy * p.k, sw * p.k, sh * p.k];
|
||
const z = c.step === 'phone' ? 0 : c.step === 'zoom' ? ease(c.t / ZOOM) : 1;
|
||
// Il telefonino, finché lo schermo non ha preso tutta la scena; poi sfuma.
|
||
if (z < 1) {
|
||
ctx.globalAlpha = 1 - z;
|
||
ctx.imageSmoothingEnabled = false;
|
||
ctx.drawImage(phone, p.x, p.y, p.w, p.h);
|
||
ctx.globalAlpha = 1;
|
||
}
|
||
// Il sito: dentro lo schermo, poi sempre più grande fino a tutta la scena. Lo schermo ha le sue proporzioni:
|
||
// il sito ci sta intero, centrato, sul fondo del sito.
|
||
const r = screen.map((v, i) => v + ([0, 0, W, H][i] - v) * z);
|
||
const fit = Math.min(r[2] / site.width, r[3] / site.height), dw = site.width * fit, dh = site.height * fit;
|
||
ctx.fillStyle = def.paper ?? '#f7e0c0';
|
||
ctx.fillRect(r[0], r[1], r[2], r[3]);
|
||
ctx.imageSmoothingEnabled = true;
|
||
ctx.drawImage(site, r[0] + (r[2] - dw) / 2, r[1] + (r[3] - dh) / 2, dw, dh);
|
||
// Sotto il muro (la pagina vera) resta il fondo del sito.
|
||
if (c.step === 'wall') { ctx.fillStyle = def.paper ?? '#f7e0c0'; ctx.fillRect(0, 0, W, H); }
|
||
// Il link da toccare: un riquadro che pulsa intorno al mattone del muro.
|
||
if (c.step === 'site') {
|
||
const [x, y, w, h] = linkBox(def, def.siteSize), pulse = 0.5 + 0.5 * Math.sin(time / 260);
|
||
ctx.strokeStyle = `rgba(255, 210, 90, ${0.5 + 0.5 * pulse})`;
|
||
ctx.lineWidth = 2 + pulse;
|
||
ctx.strokeRect(Math.round(x) - 2, Math.round(y) - 2, Math.round(w) + 4, Math.round(h) + 4);
|
||
}
|
||
ctx.imageSmoothingEnabled = false;
|
||
ctx.restore();
|
||
}
|
||
|
||
// Il suggerimento in alto, finché c'è qualcosa da toccare.
|
||
export const telefonoHint = c => c.step === 'phone' && c.age > RISE ? 'Clic o tocco: guarda il sito'
|
||
: c.step === 'site' ? 'Clic o tocco sul Muro del Sanpierino' : null;
|