Il tabellone del flipper è un vero backglass: il fondale di Codex col Castello del Trebbio, la Fortezza, il Regolo e i fuochi dei rioni; le cifre nella finestra nera, le lampadine sui bordi, i dischi delle palline e dei rioni, i ponti ai lati, il fondale che lampeggia ai colpi grossi (Andrea)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
2026-10-31 22:10:55 +01:00
co-authored by Claude Fable 5.1
parent 4d4aa8aeea
commit 3079c6ea8a
6 changed files with 53 additions and 42 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

+5
View File
@@ -76,6 +76,11 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
sulla tavola e la scossa del mobile, la scia della pallina; sul tabellone la regia (la corona al traguardo, le sulla tavola e la scossa del mobile, la scia della pallina; sul tabellone la regia (la corona al traguardo, le
bandiere al Palio, i due archi ai ponti) e le fanfare. Ponti, sponde e griglia dei punti spenti in cache su tele a bandiere al Palio, i due archi ai ponti) e le fanfare. Ponti, sponde e griglia dei punti spenti in cache su tele a
parte (l'arretrato grafico con swiftshader da 8 s a meno di un secondo). parte (l'arretrato grafico con swiftshader da 8 s a meno di un secondo).
Il tabellone è un vero «backglass» (Andrea: «col Trebbio, la Fortezza e effetti speciali»): fondale di Codex
(`assets/closeups/river-flipper-tabellone.png`, 200×360: il Castello del Trebbio, la Fortezza sotto i fasci di luce,
il Regolo coi fulmini, i fuochi nei colori dei rioni), le cifre nella finestra nera, le lampadine sui bordi, i cinque
dischi in basso (palline e rioni), i ponti ai lati della finestra, il fondale che lampeggia ai colpi grossi.
[DA ANDREA] un record per livello che resti salvato (suggerito da Codex).
- **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio - **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio
Mario, che mi ha fatto vivere tante avventure più belle della fantasia.» Mario, che mi ha fatto vivere tante avventure più belle della fantasia.»
- **Il Taglia (31 ottobre)**: tolte le battute sulla sua altezza (Baldoria: non gli fa piacere). - **Il Taglia (31 ottobre)**: tolte le battute sulla sua altezza (Baldoria: non gli fa piacere).
+42 -39
View File
@@ -158,7 +158,11 @@ function dots(ctx, line, x, y, pitch, on, off = 'rgba(255,170,60,0.12)', align =
// Il tabellone ricorda i punti mostrati (che corrono dietro a quelli veri) e la scritta che lampeggia: stato di sola // Il tabellone ricorda i punti mostrati (che corrono dietro a quelli veri) e la scritta che lampeggia: stato di sola
// presentazione, fuori dalla partita (così disegnare non cambia niente). // presentazione, fuori dalla partita (così disegnare non cambia niente).
const shown = new WeakMap(); const shown = new WeakMap();
function board(ctx, c, time) { // Il tabellone è il fondale luminoso di Codex (il «backglass»: il Trebbio, la Fortezza, il Regolo, i fuochi) sul
// fianco sinistro, 200×360: le cifre nella finestra nera, le lampadine sui bordi che girano, i cinque dischi in basso
// per le palline e i rioni, i ponti ai lati della finestra. Senza il fondale (tests), un pannello scuro.
const GLASS = { window: [20, 78, 160, 46], sockets: [[23, 329], [67, 329], [100, 329], [133, 329], [169, 329]], lampX: [8, 192], lampY0: 16, lampStep: 20 };
function board(ctx, c, time, glass) {
let d = shown.get(c); let d = shown.get(c);
if (!d) { d = { from: c.score, to: c.score, t0: time, flash: 0, lineId: 0 }; shown.set(c, d); } if (!d) { d = { from: c.score, to: c.score, t0: time, flash: 0, lineId: 0 }; shown.set(c, d); }
// Le cifre corrono: da dove erano a dove devono arrivare, in mezzo secondo, a scatti di dieci; a partita finita // Le cifre corrono: da dove erano a dove devono arrivare, in mezzo secondo, a scatti di dieci; a partita finita
@@ -166,55 +170,54 @@ function board(ctx, c, time) {
if (d.to !== c.score) { d.from = shownScore(d, time); d.to = c.score; d.t0 = time; } if (d.to !== c.score) { d.from = shownScore(d, time); d.to = c.score; d.t0 = time; }
const score = c.step === 'result' ? c.score : shownScore(d, time), hot = score !== c.score; const score = c.step === 'result' ? c.score : shownScore(d, time), hot = score !== c.score;
if (c.lineId !== d.lineId) { d.lineId = c.lineId; d.flash = time; } if (c.lineId !== d.lineId) { d.lineId = c.lineId; d.flash = time; }
const x0 = 16, y0 = 50, w = 172, h = 180; const party = c.line && time - d.flash < 2, [wx, wy, ww, wh] = GLASS.window;
ctx.fillStyle = '#0c0806'; ctx.fillRect(x0, y0, w, h); if (glass) ctx.drawImage(glass, 0, 0, 200, 360);
ctx.strokeStyle = '#5a4a30'; ctx.lineWidth = 2; ctx.strokeRect(x0 + 1, y0 + 1, w - 2, h - 2); else { ctx.fillStyle = '#0c0806'; ctx.fillRect(0, 0, 200, 360); ctx.strokeStyle = '#5a4a30'; ctx.lineWidth = 2; ctx.strokeRect(wx - 3, wy - 3, ww + 6, wh + 6); }
// Le lampadine intorno: tranquille di solito, impazzite per due secondi a ogni scritta. // Alla festa il fondale lampeggia (un lampo breve, solo sul fianco: una passata piccola).
const party = c.line && time - d.flash < 2, beat = time * (party ? 14 : 2); if (party && Math.sin(time * 12) > 0.6) { ctx.globalAlpha = 0.25; ctx.fillStyle = '#fff6c8'; ctx.fillRect(0, 0, 200, 360); ctx.globalAlpha = 1; }
// Le lampadine sui bordi: tranquille di solito, impazzite per due secondi a ogni scritta.
const beat = time * (party ? 14 : 2);
let n = 0; let n = 0;
const lamp = (x, y) => { const k = (n++ + Math.floor(beat)) % 3; ctx.fillStyle = k === 0 ? '#ffd45a' : k === 1 ? (party ? '#ff5a3a' : '#c8842a') : '#5a3a1e'; ctx.fillRect(x - 1, y - 1, 3, 3); }; const lamp = (x, y) => { const k = (n++ + Math.floor(beat)) % 3; ctx.fillStyle = k === 0 ? '#ffd45a' : k === 1 ? (party ? '#ff5a3a' : '#c8842a') : '#5a3a1e'; ctx.fillRect(x - 2, y - 2, 4, 4); };
for (let x = x0 + 6; x < x0 + w - 4; x += 8) lamp(x, y0 + 4); for (let y = GLASS.lampY0; y < 300; y += GLASS.lampStep) lamp(GLASS.lampX[0], y);
for (let y = y0 + 12; y < y0 + h - 4; y += 8) lamp(x0 + w - 5, y); for (let y = 300 - ((300 - GLASS.lampY0) % GLASS.lampStep); y >= GLASS.lampY0; y -= GLASS.lampStep) lamp(GLASS.lampX[1], y);
for (let x = x0 + w - 10; x > x0 + 4; x -= 8) lamp(x, y0 + h - 5); // La finestra: le cifre grosse color ambra col bagliore caldo dietro (sei cifre larghe, da un milione in su più
for (let y = y0 + h - 12; y > y0 + 4; y -= 8) lamp(x0 + 4, y); // fitte) e sotto la scritta: l'ultimo evento (lampeggia per due secondi), se no l'esito o il traguardo.
text(ctx, 'FLIPPER DEL RIVER', x0 + w / 2, y0 + 16, 7, '#efd49e', 'center'); ctx.save(); ctx.beginPath(); ctx.rect(wx, wy, ww, wh); ctx.clip();
// I punti, a cifre grosse color ambra col bagliore caldo dietro: sei cifre larghe, da un milione in su più fitte. ctx.fillStyle = '#000'; ctx.fillRect(wx, wy, ww, wh);
ctx.save(); ctx.beginPath(); ctx.rect(x0 + 8, y0 + 24, w - 16, h - 32); ctx.clip();
const digits = String(Math.round(score)).padStart(6, '0'), pitch = digits.length > 6 ? 3 : 4; const digits = String(Math.round(score)).padStart(6, '0'), pitch = digits.length > 6 ? 3 : 4;
ctx.fillStyle = hot ? 'rgba(255,120,30,0.22)' : 'rgba(255,120,30,0.12)'; ctx.fillRect(x0 + 10, y0 + 26, w - 20, 34); ctx.fillStyle = hot ? 'rgba(255,120,30,0.25)' : 'rgba(255,120,30,0.12)'; ctx.fillRect(wx + 2, wy + 1, ww - 4, 29);
dots(ctx, digits, x0 + w / 2, y0 + 29 + (4 - pitch) * 3, pitch, hot ? '#fff0a0' : '#ffb13a', 'rgba(255,170,60,0.1)', 'center'); dots(ctx, digits, wx + ww / 2, wy + 1 + (4 - pitch) * 3, pitch, hot ? '#fff0a0' : '#ffb13a', 'rgba(255,170,60,0.1)', 'center');
// La scritta grossa: l'ultimo evento (lampeggia per due secondi), se no l'esito o il traguardo.
const won = c.result?.winner === 'player'; const won = c.result?.winner === 'player';
const line = c.step === 'result' ? (won ? 'TRAGUARDO!' : 'FINITA') : c.line ?? (won ? 'TRAGUARDO!' : `TRAG. ${c.skill.goal}`); const line = c.step === 'result' ? (won ? 'TRAGUARDO!' : 'FINITA') : c.line ?? (won ? 'TRAGUARDO!' : `TRAG. ${c.skill.goal}`);
const flashing = c.line && c.step !== 'result' && time - d.flash < 2, visible = !flashing || Math.sin(time * 14) > -0.3; const flashing = c.line && c.step !== 'result' && time - d.flash < 2, visible = !flashing || Math.sin(time * 14) > -0.3;
if (visible) dots(ctx, line.slice(0, 12), x0 + w / 2, y0 + 68, line.length <= 8 ? 3 : 2, flashing ? '#fff6c8' : '#f0a24a', 'rgba(255,170,60,0.08)', 'center'); if (visible) dots(ctx, line.slice(0, 12), wx + ww / 2, wy + 31, 2, flashing ? '#fff6c8' : '#f0a24a', 'rgba(255,170,60,0.08)', 'center');
if (flashing) regia(ctx, c.line, time - d.flash, x0, y0, w); if (flashing) regia(ctx, c.line, time - d.flash, wx, wy, ww, wh);
ctx.restore(); ctx.restore();
// Palline, salvataggio, rioni e ponti: in chiaro, leggibili anche sul telefono. // I ponti: due lampade ai lati della finestra (accese se fatte; quella da fare lampeggia).
text(ctx, `PALLINE ${c.step === 'result' ? 0 : c.balls}`, x0 + 10, y0 + 102, 7, '#f0a24a'); c.ramps.forEach((r, i) => disc(ctx, i ? wx + ww + 8 : wx - 8, wy + wh / 2, 6, r.lit > 0 || c.lastRamp?.id === r.id ? '#ffd45a' : c.lastRamp && Math.sin(time * 6) > 0 ? '#fff6c8' : '#2a2420', '#8a7a5a'));
// I cinque dischi in basso: le palline che restano, e i quattro rioni accesi.
const [[bx, by], ...rioni] = GLASS.sockets;
disc(ctx, bx, by, 11, '#2a2420', '#8a7a5a'); text(ctx, String(c.step === 'result' ? 0 : c.balls), bx, by + 1, 9, '#ffd45a', 'center');
rioni.forEach(([x, y], i) => { const l = c.lanes[i]; disc(ctx, x, y, 11, l.lit ? COLORS[l.rione] : '#2a2420', l.lit ? '#fff4c8' : '#8a7a5a'); });
// Sotto, in piccolo: il salvataggio o la combo.
const save = saveLeft(c); const save = saveLeft(c);
if (save > 0) text(ctx, `SALVA ${Math.ceil(save)} s`, x0 + w - 10, y0 + 102, 7, '#9fd27a', 'right'); const note = save > 0 ? `PALLINA SALVA ${Math.ceil(save)} s` : c.lastRamp ? `L'ALTRO PONTE ENTRO ${Math.ceil(12 - (c.sim - c.lastRamp.at))} s` : c.ball && !c.ball.waiting && c.saved ? 'SENZA RETE' : '';
else if (c.ball && !c.ball.waiting && c.saved) text(ctx, 'SENZA RETE', x0 + w - 10, y0 + 102, 6, '#a08a60', 'right'); if (note) text(ctx, note, 100, 352, 6, save > 0 ? '#9fd27a' : '#ffe08c', 'center');
text(ctx, 'RIONI', x0 + 10, y0 + 124, 7, '#f0a24a');
c.lanes.forEach((l, i) => disc(ctx, x0 + 86 + i * 22, y0 + 124, 7, l.lit ? COLORS[l.rione] : '#2a2420', l.lit ? '#fff4c8' : '#5a5040'));
text(ctx, 'PONTI', x0 + 10, y0 + 146, 7, '#f0a24a');
c.ramps.forEach((r, i) => disc(ctx, x0 + 86 + i * 22, y0 + 146, 7, r.lit > 0 || c.lastRamp?.id === r.id ? '#ffd45a' : '#2a2420', r.lit > 0 ? '#fff4c8' : '#5a5040'));
if (c.lastRamp) text(ctx, `L'ALTRO ENTRO ${Math.ceil(12 - (c.sim - c.lastRamp.at))} s`, x0 + w - 10, y0 + 164, 6, '#ffe08c', 'right');
else text(ctx, 'ANDREA', x0 + 10, y0 + 164, 6, '#a9a08a');
} }
// La piccola regia del tabellone ai colpi grossi: al traguardo la corona del Regolo che si compone a colonne, al // La piccola regia del tabellone ai colpi grossi: al traguardo la corona del Regolo che si compone a colonne, al
// Palio le quattro bandiere dei rioni che sfilano, ai ponti due archi di punti che si uniscono. // Palio le quattro bandiere dei rioni che sfilano, ai ponti due archi di punti che si uniscono.
const CROWN = ['10001000100010001', '11011101110111011', '11111111111111111', '01111111111111110', '01111111111111110']; const CROWN = ['10001000100010001', '11011101110111011', '11111111111111111', '01111111111111110', '01111111111111110'];
function regia(ctx, line, age, x0, y0, w) { function regia(ctx, line, age, wx, wy, ww, wh) {
const cx = x0 + w / 2; const cx = wx + ww / 2;
if (line === 'TRAGUARDO!') { if (line === 'TRAGUARDO!') {
const cols = Math.min(17, Math.floor(age * 12)); const cols = Math.min(17, Math.floor(age * 12));
for (let row = 0; row < 5; row++) for (let col = 0; col < cols; col++) if (CROWN[row][col] === '1') { ctx.fillStyle = row < 2 ? '#fff6c8' : '#ffd45a'; ctx.fillRect(cx - 34 + col * 4, y0 + 24 + row * 4, 3, 3); } for (let row = 0; row < 5; row++) for (let col = 0; col < cols; col++) if (CROWN[row][col] === '1') { ctx.fillStyle = row < 2 ? '#fff6c8' : '#ffd45a'; ctx.fillRect(cx - 34 + col * 4, wy + 2 + row * 3, 3, 2); }
} else if (line === 'PALIO!') { } else if (line === 'PALIO!') {
['#d0453a', '#f2eee2', '#3fa34d', '#2f6fd0'].forEach((color, i) => { const x = x0 + 10 + ((age * 140 + i * 40) % (w - 20)); ctx.fillStyle = color; ctx.fillRect(Math.round(x), y0 + 60, 8, 6); ctx.fillRect(Math.round(x), y0 + 88, 8, 6); }); ['#d0453a', '#f2eee2', '#3fa34d', '#2f6fd0'].forEach((color, i) => { const x = wx + 2 + ((age * 140 + i * 40) % (ww - 4)); ctx.fillStyle = color; ctx.fillRect(Math.round(x), wy + 1, 8, 3); ctx.fillRect(Math.round(x), wy + wh - 4, 8, 3); });
} else if (line === 'I PONTI!') { } else if (line === 'I PONTI!') {
const k = Math.min(1, age / 0.8); const k = Math.min(1, age / 0.8);
for (let i = 0; i <= 16 * k; i++) { const a = Math.PI + i / 16 * Math.PI; ctx.fillStyle = i % 2 ? '#ffd45a' : '#fff6c8'; ctx.fillRect(Math.round(cx - 60 + Math.cos(a) * 30 + 30), Math.round(y0 + 94 + Math.sin(a) * 10), 3, 3); ctx.fillRect(Math.round(cx + 60 - Math.cos(a) * 30 - 30), Math.round(y0 + 94 + Math.sin(a) * 10), 3, 3); } for (let i = 0; i <= 16 * k; i++) { const a = Math.PI + i / 16 * Math.PI; ctx.fillStyle = i % 2 ? '#ffd45a' : '#fff6c8'; ctx.fillRect(Math.round(cx - 60 + Math.cos(a) * 30 + 30), Math.round(wy + wh - 3 + Math.sin(a) * 6), 3, 2); ctx.fillRect(Math.round(cx + 60 - Math.cos(a) * 30 - 30), Math.round(wy + wh - 3 + Math.sin(a) * 6), 3, 2); }
} }
} }
// I punti mostrati dal tabellone, che corrono da `from` a `to` in mezzo secondo. // I punti mostrati dal tabellone, che corrono da `from` a `to` in mezzo secondo.
@@ -280,12 +283,12 @@ function trail(ctx, c) {
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
} }
function result(ctx, c) { function result(ctx, c) {
panel(ctx, 16, 238, 172, 56); panel(ctx, 14, 236, 172, 56);
text(ctx, c.result.winner === 'player' ? 'TRAGUARDO FATTO!' : 'FINE PARTITA', 102, 254, 8, '#efd49e', 'center'); text(ctx, c.result.winner === 'player' ? 'TRAGUARDO FATTO!' : 'FINE PARTITA', 100, 252, 8, '#efd49e', 'center');
text(ctx, `${c.result.score} su ${c.result.goal}`, 102, 274, 8, c.result.winner === 'player' ? '#9fd27a' : '#e0b07a', 'center'); text(ctx, `${c.result.score} su ${c.result.goal}`, 100, 272, 8, c.result.winner === 'player' ? '#9fd27a' : '#e0b07a', 'center');
} }
export function drawFlipper(ctx, c, def, bg) { export function drawFlipper(ctx, c, def, bg, glass) {
const time = c.visual.time, [sx, sy] = shake(c, time); const time = c.visual.time, [sx, sy] = shake(c, time);
ctx.save(); ctx.save();
if (sx || sy) { ctx.fillStyle = '#0c0806'; ctx.fillRect(0, 0, 640, 360); ctx.translate(sx, sy); } if (sx || sy) { ctx.fillStyle = '#0c0806'; ctx.fillRect(0, 0, 640, 360); ctx.translate(sx, sy); }
@@ -319,7 +322,7 @@ export function drawFlipper(ctx, c, def, bg) {
drawFx(ctx, c, time); drawFx(ctx, c, time);
// Da fermo, l'invito: la molla da caricare. // Da fermo, l'invito: la molla da caricare.
if (waiting(c) && !c.holding && Math.sin(time * 5) > 0) text(ctx, 'LANCIA', (LANE.x0 + LANE.x1) / 2, 300, 5, '#ffe08c', 'center'); if (waiting(c) && !c.holding && Math.sin(time * 5) > 0) text(ctx, 'LANCIA', (LANE.x0 + LANE.x1) / 2, 300, 5, '#ffe08c', 'center');
board(ctx, c, time); board(ctx, c, time, glass);
if (c.step === 'result') result(ctx, c); if (c.step === 'result') result(ctx, c);
ctx.restore(); ctx.restore();
} }
+2 -2
View File
@@ -1057,8 +1057,8 @@ function drawCloseup(ctx, game, time) {
return; return;
} }
if (def.type === 'flipper') { if (def.type === 'flipper') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture); for (const src of [def.picture, def.backglass]) if (src && !backgrounds.has(src)) loadBackground(src);
drawFlipper(ctx, c, def, backgrounds.get(def.picture)); drawFlipper(ctx, c, def, backgrounds.get(def.picture), def.backglass && backgrounds.get(def.backglass));
return; return;
} }
if (def.type === 'bruciate') { if (def.type === 'bruciate') {
+1
View File
@@ -365,6 +365,7 @@ export const story = {
flipper: { flipper: {
type: 'flipper', type: 'flipper',
picture: 'assets/closeups/river-flipper.png', picture: 'assets/closeups/river-flipper.png',
backglass: 'assets/closeups/river-flipper-tabellone.png', // il fondale luminoso del tabellone (Codex: Trebbio, Fortezza, Regolo, fuochi)
names: { player: 'Andrea', cpu: 'il flipper' }, names: { player: 'Andrea', cpu: 'il flipper' },
win: [{ say: 'Traguardo! Se c\'era la classifica, oggi ci mettevo le mie iniziali.' }, { set: 'flipper_vinto' }], win: [{ say: 'Traguardo! Se c\'era la classifica, oggi ci mettevo le mie iniziali.' }, { set: 'flipper_vinto' }],
lose: [{ say: 'Tre palline, e tutte nella Sieve. Il flipper del River non perdona.' }], lose: [{ say: 'Tre palline, e tutte nella Sieve. Il flipper del River non perdona.' }],
+3 -1
View File
@@ -5,6 +5,7 @@ import { drawFlipper } from '../src/engine/flipper-render.js';
import { Game } from '../src/engine/game.js'; import { Game } from '../src/engine/game.js';
import { story } from '../src/story/story.js'; import { story } from '../src/story/story.js';
import { settle } from './helpers.mjs'; import { settle } from './helpers.mjs';
import { existsSync } from 'node:fs';
const run = (c, secs) => { for (let i = 0; i < secs * 60; i++) m.update(c, 1 / 60); }; const run = (c, secs) => { for (let i = 0; i < secs * 60; i++) m.update(c, 1 / 60); };
const put = (c, ball) => Object.assign(c.ball, ball); const put = (c, ball) => Object.assign(c.ball, ball);
@@ -228,7 +229,8 @@ test('il disegno del flipper non cambia lo stato; il tabellone dice l\'esito giu
const c = m.newFlipper(11, { level: 'bona' }), bg = {}; const c = m.newFlipper(11, { level: 'bona' }), bg = {};
const draw = () => { const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, bg); assert.deepEqual(c, before); }; const draw = () => { const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, bg); assert.deepEqual(c, before); };
draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg)); draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'FLIPPER DEL RIVER')); assert.ok(existsSync(new URL('../' + story.closeups.flipper.backglass, import.meta.url)), 'il fondale del tabellone esiste');
const glass = {}; drawFlipper(ctx, c, story.closeups.flipper, bg, glass); assert.ok(calls.some(([op, img, x, y, w, h]) => op === 'drawImage' && img === glass && w === 200 && h === 360), 'il fondale sul fianco');
m.launch(c, 0.8); run(c, 1); calls.length = 0; draw(); m.launch(c, 0.8); run(c, 1); calls.length = 0; draw();
assert.ok(calls.filter(([op]) => op === 'fillRect').length > 300, 'il tabellone a punti luminosi'); assert.ok(calls.filter(([op]) => op === 'fillRect').length > 300, 'il tabellone a punti luminosi');
c.step = 'result'; c.result = { winner: 'player', score: 70000, goal: 60000 }; calls.length = 0; draw(); c.step = 'result'; c.result = { winner: 'player', score: 70000, goal: 60000 }; calls.length = 0; draw();