Il minigioco delle bruciate alla Smarronata: coi volontari della Misericordia si gira a mano la manovella del cilindro sul fuoco, facendo giri col dito o col mouse; troppo piano si carbonizzano sotto e restano crude sopra, troppo forte volano nel fuoco; cotte, nel sacco (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
// Le bruciate in primo piano (bruciate.js): il cilindro a rete sopra il fuoco, visto di fronte, con le castagne dentro
|
||||
// che ricascano, la manovella a destra, le fiamme che lo lambiscono; in basso il giro della manovella e la cottura.
|
||||
// Niente filtri e niente passate trasparenti a tutto schermo (vedi risveglio.js): solo forme piccole.
|
||||
import { DRUM, CRANK, BAR, N, COOKED, BURNT, POUR, MAX_SPEED, inside, doneness } from './bruciate.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
import { ambientHash as hash } from './ambient.js';
|
||||
|
||||
const SACK = { x: 604, y: 268 }, FIRE = 252, RODS = 18, RINGS = 9;
|
||||
const lerp = (a, b, k) => a + (b - a) * k;
|
||||
const mix = (p, q, k) => `rgb(${p.map((v, i) => Math.round(lerp(v, q[i], k))).join(',')})`;
|
||||
// Il colore della buccia: lucida e rossiccia da cruda, scura da cotta, nera da carbone.
|
||||
const RAW = [168, 96, 50], DONE = [118, 64, 30], COAL = [26, 20, 18];
|
||||
const shell = cook => cook < COOKED ? mix(RAW, DONE, Math.max(0, cook) / COOKED) : mix(DONE, COAL, Math.min(1, (cook - COOKED) / (BURNT - COOKED)));
|
||||
|
||||
// Dove sta una castagna sullo schermo, e quanto è davanti (per l'ordine e la luce).
|
||||
function place(k) {
|
||||
return { x: lerp(DRUM.x0 + 8, DRUM.x1 - 8, k.x), y: DRUM.y + Math.sin(k.a) * DRUM.r * k.r, z: Math.cos(k.a) * k.r };
|
||||
}
|
||||
function chestnut(ctx, x, y, cook, z = 0) {
|
||||
// Il bordino, perché si stacchino dal fondo del cilindro (chiaro su quelle carbone).
|
||||
ctx.fillStyle = cook > BURNT ? '#6a5a4e' : '#1e120a';
|
||||
ctx.beginPath(); ctx.ellipse(Math.round(x), Math.round(y), 7, 6, 0, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = shell(cook);
|
||||
ctx.beginPath(); ctx.ellipse(Math.round(x), Math.round(y), 6, 5, 0, 0, Math.PI * 2); ctx.fill();
|
||||
// La punta chiara e, da cotte, l'incisione aperta col giallo della castagna.
|
||||
ctx.fillStyle = cook > BURNT ? '#2a2420' : '#d9b48a';
|
||||
ctx.fillRect(Math.round(x) - 1, Math.round(y) - 4, 2, 1);
|
||||
if (cook >= COOKED * 0.8 && cook <= BURNT) { ctx.fillStyle = '#e8c070'; ctx.fillRect(Math.round(x) - 2, Math.round(y) - 1, 4, 1); }
|
||||
// Il riflesso del fuoco, più forte su quelle davanti.
|
||||
if (cook < BURNT) { ctx.fillStyle = `rgba(255,190,120,${0.25 + 0.3 * Math.max(0, z)})`; ctx.fillRect(Math.round(x) + 1, Math.round(y) - 2, 2, 1); }
|
||||
}
|
||||
// Le fiamme che lambiscono il cilindro: lingue basse e storte, ognuna col suo passo, che ogni tanto si allungano.
|
||||
function flames(ctx, time, front) {
|
||||
for (let i = 0; i < 17; i++) {
|
||||
if ((i % 3 === 0) !== front) continue;
|
||||
const r = hash(i * 7.3), x = DRUM.x0 + 6 + i * (DRUM.x1 - DRUM.x0 - 12) / 16 + 6 * (hash(i * 1.9) - 0.5);
|
||||
const lick = Math.max(0, Math.sin(time * (1.3 + r) + i * 2.1)) ** 4, h = front ? 5 + 6 * r + 2 * Math.sin(time * (6 + r * 5) + i) + 7 * lick : 18 + 16 * r + 7 * Math.sin(time * (6 + r * 5) + i) + 22 * lick;
|
||||
const w = 5 + 5 * hash(i * 3.1), sway = 2.5 * Math.sin(time * (3 + r) + i * 1.7);
|
||||
for (const [k, color] of [[1, '#b8301a'], [0.7, '#ea7a1a'], [0.4, '#ffd04a']]) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - w * k, FIRE);
|
||||
ctx.quadraticCurveTo(x - w * k * 0.2 + sway, FIRE - h * k * 0.6, x + sway * 2, FIRE - h * k);
|
||||
ctx.quadraticCurveTo(x + w * k * 0.5 + sway, FIRE - h * k * 0.45, x + w * k, FIRE);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Il cilindro appeso: la sbarra sulle due forcelle e i due ferri che lo reggono per l'asse.
|
||||
function hanger(ctx) {
|
||||
ctx.fillStyle = '#2a2420'; ctx.fillRect(150, BAR - 3, 336, 6);
|
||||
ctx.fillStyle = '#6a5a4a'; ctx.fillRect(150, BAR - 3, 336, 1);
|
||||
for (const x of [DRUM.x0 - 12, DRUM.x1 + 12]) {
|
||||
ctx.fillStyle = '#2a2420'; ctx.fillRect(x - 2, BAR, 4, DRUM.y - BAR + 4);
|
||||
ctx.fillStyle = '#5a4a3e'; ctx.fillRect(x - 2, BAR, 1, DRUM.y - BAR + 4);
|
||||
ctx.fillStyle = '#2a2420'; ctx.beginPath(); ctx.arc(x, DRUM.y, 5, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
}
|
||||
// La rete: gli anelli e i tondini che girano col cilindro (quelli davanti più chiari).
|
||||
function mesh(ctx, c, front) {
|
||||
const { x0, x1, y, r } = DRUM;
|
||||
for (let j = 0; j < RODS; j++) {
|
||||
const a = c.drum + j * Math.PI * 2 / RODS, z = Math.cos(a);
|
||||
if (front !== z > 0) continue;
|
||||
ctx.fillStyle = front ? '#9a9088' : '#3a302a';
|
||||
ctx.fillRect(x0, Math.round(y + Math.sin(a) * r), x1 - x0, 1);
|
||||
}
|
||||
ctx.strokeStyle = front ? '#8a8078' : '#3a302a'; ctx.lineWidth = 1;
|
||||
for (let i = 0; i <= RINGS; i++) {
|
||||
const x = Math.round(lerp(x0, x1, i / RINGS)) + 0.5, end = i === 0 || i === RINGS;
|
||||
ctx.beginPath(); ctx.ellipse(x, y, end ? 9 : 4, r, 0, front ? -Math.PI / 2 : Math.PI / 2, front ? Math.PI / 2 : Math.PI * 1.5); ctx.stroke();
|
||||
}
|
||||
}
|
||||
function crank(ctx, c, time) {
|
||||
const { x, y, rx, ry } = CRANK, a = c.drum;
|
||||
// L'asse, da una forcella all'altra e fino alla manovella.
|
||||
ctx.fillStyle = '#2a2420'; ctx.fillRect(DRUM.x0 - 16, y - 2, x - DRUM.x0 + 18, 4);
|
||||
ctx.fillStyle = '#6a625a'; ctx.fillRect(DRUM.x0 - 16, y - 2, x - DRUM.x0 + 18, 1);
|
||||
// Il giro da fare: un'ellisse tratteggiata che pulsa, finché il cilindro è fermo.
|
||||
if (c.step === 'turn' && Math.abs(c.speed) < 0.5) {
|
||||
ctx.strokeStyle = `rgba(255,224,140,${0.5 + 0.4 * Math.sin(time * 5)})`; ctx.setLineDash([3, 3]);
|
||||
ctx.beginPath(); ctx.ellipse(x, y, rx + 6, ry + 6, 0, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
||||
text(ctx, 'GIRA!', x, y + ry + 18, 6, '#ffe08c', 'center');
|
||||
}
|
||||
const hx = x + Math.cos(a) * rx, hy = y + Math.sin(a) * ry;
|
||||
ctx.strokeStyle = '#5a5450'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(hx, hy); ctx.stroke();
|
||||
ctx.strokeStyle = '#b0a698'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, y - 1); ctx.lineTo(hx, hy - 1); ctx.stroke();
|
||||
ctx.fillStyle = '#4a4440'; ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fill();
|
||||
// L'impugnatura di legno, consumata dalle mani.
|
||||
ctx.fillStyle = '#3a2414'; ctx.fillRect(Math.round(hx) - 4, Math.round(hy) - 8, 9, 16);
|
||||
ctx.fillStyle = '#a0703c'; ctx.fillRect(Math.round(hx) - 3, Math.round(hy) - 7, 7, 14);
|
||||
ctx.fillStyle = '#d6a062'; ctx.fillRect(Math.round(hx) - 2, Math.round(hy) - 6, 2, 12);
|
||||
}
|
||||
// In basso a sinistra (in alto ci sono i cursori del volume): il giro della manovella (rosso troppo piano, verde giusto, rosso troppo forte) e la cottura.
|
||||
function gauges(ctx, c) {
|
||||
panel(ctx, 10, 296, 196, 54);
|
||||
const bar = (y, label, k, zones, mark) => {
|
||||
text(ctx, label, 18, y, 6, '#efd49e');
|
||||
const x0 = 82, w = 116;
|
||||
for (const [a, b, color] of zones) { ctx.fillStyle = color; ctx.fillRect(x0 + Math.round(a * w), y - 3, Math.max(1, Math.round((b - a) * w)), 7); }
|
||||
ctx.fillStyle = mark; ctx.fillRect(x0 + Math.round(Math.max(0, Math.min(1, k)) * w) - 1, y - 6, 3, 13);
|
||||
};
|
||||
const s = c.skill, lo = s.lo / MAX_SPEED, hi = s.hi / MAX_SPEED;
|
||||
bar(312, 'GIRO', Math.abs(c.speed) / MAX_SPEED, [[0, lo, '#8a3a2a'], [lo, hi, '#4f8a3a'], [hi, 1, '#b3402a']], '#fff4c8');
|
||||
const top = BURNT * 1.15;
|
||||
bar(334, 'COTTURA', doneness(c) / top, [[0, COOKED / top, '#b07a4a'], [COOKED / top, BURNT / top, '#6a8a3a'], [BURNT / top, 1, '#2a2420']], '#fff4c8');
|
||||
}
|
||||
function result(ctx, c) {
|
||||
const r = c.result;
|
||||
panel(ctx, 170, 120, 300, 104);
|
||||
text(ctx, r.winner === 'player' ? 'BRUCIATE COI FIOCCHI!' : 'BRUCIATE COSÌ COSÌ', 320, 140, 8, '#efd49e', 'center');
|
||||
const rows = [['Buone', r.good, '#9fd27a'], ['Crude', r.raw, '#e0b07a'], ['Carbonizzate', r.burnt, '#c0b0a0'], ['Nel fuoco', r.spilled, '#f08a5a']];
|
||||
rows.forEach(([label, n, color], i) => { text(ctx, label, 200, 162 + i * 13, 6, color); text(ctx, `${n}`, 440, 162 + i * 13, 6, color, 'right'); });
|
||||
text(ctx, `Ne servivano ${r.goal} su ${N}`, 320, 216, 5, '#d7ccb4', 'center');
|
||||
}
|
||||
|
||||
export function drawBruciate(ctx, c, def, bg) {
|
||||
const time = c.visual.time;
|
||||
ctx.save();
|
||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||
else { ctx.fillStyle = '#6a5a40'; ctx.fillRect(0, 0, 640, 360); }
|
||||
hanger(ctx);
|
||||
// Il fondo del cilindro, scuro, col bagliore della brace sotto; poi le fiamme dietro e la rete di dietro.
|
||||
ctx.fillStyle = 'rgba(74,46,30,0.72)'; ctx.fillRect(DRUM.x0, DRUM.y - DRUM.r, DRUM.x1 - DRUM.x0, DRUM.r * 2);
|
||||
ctx.fillStyle = 'rgba(240,120,40,0.18)'; ctx.fillRect(DRUM.x0, DRUM.y + DRUM.r * 0.3, DRUM.x1 - DRUM.x0, DRUM.r * 0.7);
|
||||
flames(ctx, time, false);
|
||||
mesh(ctx, c, false);
|
||||
// Le castagne: da dietro a davanti. Rovesciandole, scivolano tutte verso il sacco.
|
||||
const pour = c.step === 'pour' ? Math.min(1, c.t / POUR) : c.step === 'result' ? 1 : 0;
|
||||
const ks = (pour ? c.chestnuts.filter(k => !k.out) : inside(c)).map(k => ({ k, ...place(k) })).sort((p, q) => p.z - q.z);
|
||||
for (const p of ks) {
|
||||
if (pour) {
|
||||
const d = Math.min(1, Math.max(0, pour * 1.6 - p.k.x * 0.6)), x = lerp(p.x, SACK.x, d), y = lerp(p.y, SACK.y, d) - Math.sin(d * Math.PI) * 40;
|
||||
if (d < 1) chestnut(ctx, x, y, p.k.cook, 1);
|
||||
} else chestnut(ctx, p.x, p.y, p.k.cook, p.z);
|
||||
}
|
||||
mesh(ctx, c, true);
|
||||
flames(ctx, time, true);
|
||||
crank(ctx, c, time);
|
||||
// Quelle che volano nel fuoco, e lo sbuffo quando ci cascano.
|
||||
for (const f of c.flying) {
|
||||
const k = Math.min(1, f.t / 0.6), x = f.x + f.dir * 60 * k, y = lerp(f.y, FIRE - 6, k) - Math.sin(k * Math.PI) * 50;
|
||||
if (f.t < 0.6) chestnut(ctx, x, y, 0.5);
|
||||
else for (let i = 0; i < 4; i++) {
|
||||
const s = (f.t - 0.6) / 0.6;
|
||||
ctx.fillStyle = `rgba(200,195,185,${0.6 * (1 - s)})`;
|
||||
ctx.beginPath(); ctx.arc(x + (i - 1.5) * 5 * (1 + s), FIRE - 10 - s * 30 - i * 3, 4 + s * 6, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
}
|
||||
if (c.step === 'result') result(ctx, c); else gauges(ctx, c);
|
||||
ctx.restore();
|
||||
}
|
||||
Reference in New Issue
Block a user