BeeEngine logo BeeEngine JAVASCRIPT 2D ENGINE

Sequenze e Tween Paralleli in JavaScript

Documentazione – BeeEngine
v2.7.0

BeeTimeline — Documentazione Tecnica

Sequenze e paralleli di tween: cursor di coda in serie, at solo per lo start, call/set che coprono duration

📁 Panoramica

BeeTimeline è il sequencer. Vive in src/core/BeeTimeline.js, il barrel lo re-esporta insieme a BeeTween / BeeTweenClock / BeeEase. BeeTimer conta i secondi. BeeTween scrive x, alpha, scaleX ogni frame. Questa classe mette tween, attese e callback in fila (o in parallelo con at: piazza lo start, il cursor di coda resta sequenziale). Non è un secondo rAF, non è un grafo di clip (BeeAnimator), non è GSAP.

BeeEngine crea un clock tween: this.tweens = new BeeTweenClock(). Ogni frame, anche in pausa, il loop fa time.tick(timestamp), this.timers.tick(this.time) e subito this.tweens.tick(this.time). gioco.timeline() inietta quel clock. I tween figli della timeline non stanno sulla lista live: nascono con autoStart: false, overwrite: false e senza clock. Avanza solo la timeline, che li guida con seek.

import { BeeEngine } from ‘beeengine’; const gioco = new BeeEngine(‘testCanvas’, 800, 600); const cut = gioco.timeline() .to(logo, { scaleX: 1, scaleY: 1 }, { duration: 0.4, ease: ‘backOut’ }) .wait(0.15) .to(logo, { y: 80 }, { duration: 0.35, ease: ‘quadOut’ }) .to(panel, { alpha: 1 }, { duration: 0.3, at: 0.2 }) .call(() => ready = true) .start(); cut.pause(); cut.resume(); cut.cancel();

📁 Due vie: clock del motore vs update manuale

gioco.timeline(options) è zucchero: new BeeTimeline({ ...options, clock: this.tweens }). Non imposta autoStart. La catena fluent costruisce la sequenza; senza .start() in coda la timeline non entra in lista e non avanza.

Un new BeeTimeline({...}) senza clock non entra in gioco.tweens. Devi chiamare timeline.update(gioco.time) (o un numero). Se lo metti in scene.update / entity.update, in pausa del motore quell’hook non parte: l’opzione unscaled da sola non basta. Il callback engine.update gira anche in pausa; oppure registrala sul clock.

Non fare entrambe. Una timeline già sul clock che riceve anche update a mano avanza due volte nello stesso frame.

autoStart: true nel costruttore chiama start() prima dei .to() incatenati. È legale solo se aggiungi gli item nello stesso frame, prima del prossimo tweens.tick. Il pattern giusto resta: costruisci, poi start().

📁 Cursor, sequenza, at

Due orologi distinti. Il cursor di coda avanza in serie: ogni to / from / fromTo lo sposta di tween.duration rispetto al valore prima della chiamata (sequentialStart + duration), anche se l’item è piazzato altrove. at finito è tempo assoluto (Math.max(0, at)): imposta solo lo start dell’item, non riassegna il cursor. Non è un offset dal cursor, e non lo tira più indietro.

wait(seconds) avanza solo il cursor (e cresce duration se il cursor supera il max). call / set lasciano il cursor fermo, ma #cover(start) allunga duration almeno fino al loro at (o al cursor, se at manca).

Chiamata Start item Cursor dopo
.to(t, props, 0.4) cursor corrente sequentialStart + 0.4; duration copre il max
.to(t, props, { duration: 0.3, at: 0 }) 0 (parallelo dall’inizio) sequentialStart + 0.3 — non retrocede a 0.3
.wait(0.15) — cursor + 0.15
.call(fn) / .set(t, props) cursor (o at) invariato; duration cresce almeno fino a start

Esempio: cursor a 0.4, poi .to(panel, { alpha: 1 }, { duration: 0.3, at: 0 }) → start 0, cursor 0.7, duration almeno 0.7. Il prossimo .to() senza at parte da 0.7, non da 0.3. Un overlay con at sposta comunque la coda di duration: se vuoi continuare esattamente dove eri, riallinea con at esplicito sul prossimo item.

Su una timeline vuota, .call(fn, 2) ha start 2 e duration 2: il marker vive. Un call / set a at === duration (fine vera) parte nel complete, che applica time = duration e spara i marker con start <= duration. Solo call/set a t=0 su timeline ancora a duration 0 restano un one-shot: il primo update applica t=0 e completa.

📁 to / from / fromTo, wait, call, set

I tre metodi tween wrappano new BeeTween. Durata default BEE_TWEEN_DEFAULTS.duration (0.4). Ease: nome in BeeEase (quadOut, backOut, bounceOut…) o funzione t => t; stringa sconosciuta → linear. Terzo argomento: numero (solo duration) oppure oggetto { duration, ease, at }.

Dall’oggetto opzioni la timeline inoltra al tween figlio duration, ease, yoyo, repeat. Non inoltra delay, onStart / onUpdate / onComplete, né clock / overwrite (i figli nascono autoStart: false, overwrite: false, senza clock). Yoyo/repeat della sequenza restano nel costruttore della timeline. Yoyo/repeat sull’item allungano il ciclo del BeeTween, non lo slot sulla timeline: l’item dura tween.duration (un ciclo) e il figlio è guidato con seek, che non cicla. Per un buco usa wait. I callback di clip stanno su timeline.onComplete o su call — vedi Known limitations.

call(fn, at?) ignora i non-function. Spara fn(this) una volta, solo in andata (forward). set(target, props, at?) copia le chiavi sull’oggetto nello stesso istante (anche non numeriche: non passa da BeeTween).

📁 play → update → complete

start() è un restart: azzera elapsed / finished / cancelled, rewind degli item, add sul clock, poi #applyTime(0) subito (arma i tween a t=0, spara call/set a 0). Il complete non parte da start: parte dal primo update in cui elapsed raggiunge duration (o duration è 0).

Un ciclo: armi a t=0, il clock somma dt, applichi il tempo locale, chiudi o ripeti. I tween figli non tickano da soli.

  1. 1. startRewind, clock.add, #applyTime(0).
  2. 2. updateelapsed += dt, seek dei tween armati, call/set in andata.
  3. 3. fine cicloRepeat/yoyo: elapsed 0, rewind, apply t=0 nello stesso frame (residuo dt scartato). Altrimenti finished.
  4. 4. onCompleteSolo all’uscita vera. Il clock toglie i one-shot.

📁 Due assi: scaled e unscaled

La timeline non sceglie il delta da sola se le passi un numero. Sceglie solo se le passi BeeTime (ha delta) o un oggetto { dt, unscaledDt }. I figli ereditano unscaled alla creazione, ma non chiamano update: lo step è solo della timeline.

Argomento di update Timeline scalata Timeline unscaled
BeeTime (ha delta) time.delta(false) → dt (0 in pausa, segue timeScale) time.delta(true) → unscaledDt
oggetto { dt, unscaledDt } dt se è un number, altrimenti 0 unscaledDt se è un number, altrimenti 0
numero quello step, punto. Il flag unscaled viene ignorato

unscaled e l’alias useUnscaledTime si allineano nel costruttore. resolveTimelineStep legge timeline.unscaled. Se muti solo useUnscaledTime a caldo, lo step non cambia.

gioco.timeline().to(box, { x: 640 }).start(); // si ferma con F4 / pause() gioco.timeline({ unscaled: true }) .to(hud, { alpha: 1 }) .start(); // sbagliato: il numero è già lo step, unscaled non si applica tl.update(gioco.time.dt);

📁 start, pause, resume, cancel, seek

Non sono sinonimi. Fluent: tutti ritornano this. Non esiste stop (alias di BeeTimer), non esiste reset, non esiste destroy.

Metodo elapsed running / paused finished / cancelled Clock
start() azzera true / false entrambi false re-add (idempotente) + apply t=0
pause() invariato false / true (no-op se non era running) invariati resta in lista, update esce subito
resume() invariato true / false no-op se finished o cancelled re-add
cancel() invariato false / false finished false, cancelled true cancel dei tween figli + clock.remove(this)
seek(seconds) clamp [0, duration] invariati invariati (non completa) resta in lista; apply forward

start due volte è un restart, non un no-op: elapsed torna a 0, gli item si ri-armano. Dopo un one-shot finito, resume non riparte: serve start(). Dopo cancel, stesso discorso: solo start (riazzera cancelled e re-add).

Teardown = cancel(). engine.destroy() chiama tweens.clear(), che stacca il puntatore clock e fa cancel su ciascun live (timeline comprese).

📁 repeat, yoyo, loop

Default repeat: 0 → un ciclo. repeat: 2 → tre giri (come GSAP: repeat + 1). repeat: Infinity imposta anche loop = true: il compact del clock la tiene in lista.

A fine ciclo #complete incrementa #cycles. Se ne restano: elapsed = 0, rewind (armed/fired false; sui tween azzera cancelled/finished/running, non i from catturati), se yoyo inverte #backward, poi nello stesso frame #applyTime a t=0 (o a duration in reverse). L’overflow elapsed - duration si scarta: niente catch-up — vedi Known limitations.

In reverse il tempo locale è duration - elapsed. I tween ricevono seek decrescente: il yoyo visivo della sequenza è #backward della timeline, non di tween.yoyo. call / set sparano solo se forward: in retromarcia restano zitti.

Il freeze to + repeat senza yoyo è risolto. BeeTween.#capture gira una volta (#captured resta true): start() al rewind del ciclo non rilegge i valori correnti. Un .to(box, { x: 200 }) con repeat sulla timeline riparte dal from originale, non resta fermo sul to. fromTo resta esplicito se vuoi from e to nello spec; non è più obbligatorio per far loopare un to.

Se imposti timeline.loop = true a mano ma repeat è finito, il complete marca finished e update esce: resta uno zombie sul clock. Lascia che loop lo derivi da Infinity.

📁 overwrite, steal, kill

Quando un item tween viene armato: tween.start(), poi #stealSiblings (toglie le stesse chiavi sugli altri tween già armati della stessa timeline), poi clock.overwrite(tween) sui live del clock (tween standalone e altre timeline via steal). I figli non sono sul clock: senza stealSiblings due clip sulla stessa proprietà si pesterebbero ogni frame.

gioco.tweens.kill(target) su una timeline chiama killTarget: cancel sui figli con quel target. La timeline non si spegne. Da lì #applyTime salta i tween cancelled. Il rewind di un repeat rimette cancelled = false; al prossimo arm start() non ricattura (#captured resta true), le chiavi restano quelle del primo capture.

Un gioco.to(stessoOggetto, stesseChiavi) standalone, quando fa #begin, ruba le chiavi alla timeline (overwrite default true). È il contratto “ultimo tween vince”, non un bug.

📁 update — chi avanza, e quando no

Guardia in testa: se non è running, o è finished, o è cancelled, return. Poi elapsed += resolveTimelineStep(...).

Uno step non finito (NaN, undefined, oggetto senza i campi) diventa 0. Uno step numerico negativo sottrae da elapsed. In pausa del motore tweens.tick(this.time) parte lo stesso: le scalate sommano dt = 0, le unscaled sommano unscaledDt.

duration <= 0 (timeline vuota, o solo call/set a t=0): applica t=0 e completa al primo update. onComplete riceve la timeline: fn(this). Nessun try/catch: un throw in call o in onComplete ferma il resto del clock.tick di quel frame.

seek(seconds) sulla timeline clamp in [0, duration], applica il tempo (con #backward sul pose dei tween) e passa forward: true a #applyTime: può sparare call/set non ancora fired. Non chiama #complete: non marca finished e non spara timeline.onComplete. Se cancelled, no-op.

I tween figli sono guidati con seek(local), non con update: tween.onComplete del figlio non parte — vedi Known limitations.

📁 Integrazione: loop, scene, entity

Ordine reale nel loop V2.4:

this.time.tick(timestamp); this.timers.tick(this.time); // anche in pausa this.tweens.tick(this.time); // timeline e tween, anche in pausa if (!this.time.paused) { this.scenes.update(dt, this.input); this.updateEntities(dt, this.input); this.time.consumeFixedSteps((fixedDt) => this.physics.step(fixedDt)); }

Le timeline sul clock si muovono prima di scene e entity. Un call che fa scenes.change vede ancora le entity vecchie in quello stesso frame. Non c’è add(timeline) nidificata, non ci sono label: solo numeri su at.

📁 Esempio: intro vs HUD in freeze

import { BeeEngine } from ‘beeengine’; const gioco = new BeeEngine(‘testCanvas’, 800, 600); const intro = gioco.timeline({ onComplete(tl) { // finished è già true qui } }) .fromTo(logo, { alpha: 0, scaleX: 0.4 }, { alpha: 1, scaleX: 1 }, { duration: 0.5, ease: ‘backOut’ }) .to(panel, { alpha: 1 }, { duration: 0.25, at: 0.2 }) .wait(0.1) .call(() => { ready = true; }) .start(); const pulse = gioco.timeline({ unscaled: true, yoyo: true, repeat: Infinity }) .to(overlay, { alpha: 0.35 }, 0.6) .start(); gioco.start((dt, input, time) => { if (input.wasPressed(‘Escape’)) time.togglePause(); // non chiamare intro.update(time): è già sul clock });

📁 Note per chi integra

Clock per le cut e l’HUD. gioco.timeline() se deve vivere la pausa (con unscaled: true) o non hai un hook ogni frame. Update manuale se la sequenza muore con l’entity: cancel() in destroy.

Un numero è uno step. Per rispettare pausa e slow-mo passa gioco.time, non gioco.time.dt.

start = restart. Utile per un intro che si rinfresca. Non è “parte solo se era ferma”.

Paralleli. at piazza lo start in tempo assoluto e non tira indietro il cursor. La coda avanza comunque di duration dell’item: se il prossimo clip deve ripartire dalla fine precedente, passa un at esplicito.

Loop di un to. Il capture del figlio è una tantum: repeat della timeline su un to riparte dal from originale. fromTo resta utile se from e to devono stare nello spec, non come workaround al freeze.

Callback che lancia. Non è wrappata. Gli altri item dello stesso tick possono non girare. Tieni call / onComplete corte e senza throw.

Niente destroy sull’istanza. cancel in entity.destroy se l’hai creata a mano. Il motore pulisce solo gioco.tweens.

📁 Known limitations

Limiti del contratto attuale — non workaround da “fixare in userland” se il JS non cambia. I tre bug di cursor / call+set / freeze del to in repeat sono chiusi nel sorgente: non stanno qui.

onComplete dei tween figli in seek. La timeline non fa tween.update: in #applyTime chiama item.tween.seek(local). BeeTween.seek applica il pose e, se t >= duration con repeat === 0 e senza yoyo, marca finished e spegne running, ma non entra in #complete: tween.onComplete del figlio non parte mai, né dal update della timeline né da timeline.seek. Anche timeline.seek si ferma a #applyTime: non marca finished e non spara timeline.onComplete (quello parte solo dal ramo elapsed >= duration di update). Per i marker di clip usa timeline.call o l’onComplete della timeline.

Residuo di dt a fine ciclo / niente catch-up. Quando elapsed supera duration, update applica l’estremo del ciclo (duration in andata, 0 in reverse) e chiama #complete. Se restano giri: elapsed = 0, rewind, nello stesso frame #applyTime a t=0 (o a duration se yoyo è appena andato in reverse). L’overflow elapsed - duration si scarta: nessun while di catch-up come in BeeTimer. Un dt più grosso di un ciclo non entra nel giro nuovo; il pose del ciclo successivo parte da capo, senza “saltare avanti” del residuo.

📁 Riepilogo API

Fluent: to, from, fromTo, wait, call, set, start, pause, resume, cancel, steal, killTarget, seek, update ritornano this. Default durata/ease dei figli: BEE_TWEEN_DEFAULTS (frozen, modulo BeeTween).

Membro Ritorna Scopo
new BeeTimeline(options?) BeeTimeline unscaled / useUnscaledTime, yoyo, repeat (0 o Infinity), onComplete, clock, autoStart.
gioco.timeline(options?) BeeTimeline Come sopra, con clock: gioco.tweens. Non forza autoStart.
duration / elapsed number Secondi. duration è il max tra cursor di coda (to/from/fromTo/wait) e gli start coperti da call / set (#cover).
progress number In [0, 1]; se duration <= 0 vale 1. In yoyo resta 0→1 sul ciclo locale.
repeat / yoyo / loop number / boolean Giri extra; invertito a fine ciclo. loop è repeat === Infinity (ristabilito in start).
unscaled boolean Asse reale. Alias costruttore: useUnscaledTime.
running / paused / finished / cancelled boolean paused è getter su campo privato: true solo dopo pause su una timeline che stava running.
onComplete function | null Chiamata con (timeline) all’uscita, dopo finished = true.
clock BeeTweenClock | null Clock owner, o null se è una timeline manuale / dopo remove.
to(target, props, durationOrOptions?) this Tween mode to. Opzioni: duration, ease, at, yoyo, repeat (questi due sul figlio; lo slot timeline resta un ciclo).
from(target, props, durationOrOptions?) this Mode from: parte dallo spec, arriva ai valori catturati all’arm (capture una tantum). Stesse opzioni di to.
fromTo(target, from, to, durationOrOptions?) this From e to nello spec. Capture una tantum come gli altri mode. Stesse opzioni di to.
wait(seconds) this Avanza il cursor. Negativi / NaN → 0.
call(fn, at?) this Marker a durata 0. Cursor invariato; #cover(start) allunga duration. No-op se fn non è function.
set(target, props, at?) this Assegnazione immediata delle chiavi. Stesso contratto di call su cursor; #cover(start) su duration.
start() this Azzera e parte. Restart se già running. Apply t=0 nello stesso call.
pause() this Ferma, tiene elapsed. Nessun alias stop.
resume() this Riparte senza azzerare. No-op se finished o cancelled.
cancel() this Spegne, marca cancelled, cancel figli, toglie dal clock.
steal(target, keys) this Inoltra tween.steal ai figli. Usato da clock.overwrite.
killTarget(target) this Cancel i tween figli su quel target. La timeline resta running.
seek(seconds) this Clamp in [0, duration], #applyTime (forward). Non completa la timeline né i onComplete dei figli.
update(dtOrTime) this Avanza. Accetta BeeTime, { dt, unscaledDt }, o un number (step esplicito).
gioco.to / from / fromTo BeeTween Tween singolo sul clock, non una timeline. Overwrite default true.
gioco.tweens.kill(target) this (clock) Cancel i tween con quel target; sulle timeline chiama killTarget.