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.
📁 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. startRewind,
clock.add,#applyTime(0). -
2. update
elapsed += dt, seek dei tween armati, call/set in andata. -
3. fine cicloRepeat/yoyo: elapsed 0,
rewind, apply t=0 nello stesso frame (residuo dt scartato). Altrimenti
finished. - 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.
📁 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:
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
📁 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. |