BeeEngine logo BeeEngine JAVASCRIPT 2D ENGINE

Modulo di Interpolazione BeeTween

Documentazione – BeeEngine
v2.8.0

BeeTween — Documentazione Tecnica

Interpola numeri sul target ogni frame: to / from / fromTo, capture una tantum, clock gioco.tweens

📁 Panoramica

BeeTween scrive proprietà numeriche. Vive in src/core/BeeTween.js insieme a BeeEase, resolveEase, BEE_TWEEN_DEFAULTS e BeeTweenClock. Il barrel le re-esporta. BeeTimer conta i secondi. Questa classe assegna x, alpha, scaleX, volume ogni frame, con easing. Non è un cooldown, non è GSAP, non è BeeAnimator (grafo di clip). Per una fila di tween + wait + call usa BeeTimeline.

BeeEngine crea un clock: 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.to / from / fromTo iniettano quel clock e partono da soli (autoStart: true). I tween figli di una timeline non stanno sulla lista live: nascono con autoStart: false, overwrite: false e senza clock. Li guida la timeline con seek.

import { BeeEngine } from ‘beeengine’; const gioco = new BeeEngine(‘testCanvas’, 800, 600); const slide = gioco.to(box, { x: 640, alpha: 1 }, { duration: 0.6, ease: ‘backOut’ }); gioco.from(logo, { scaleX: 0.4, scaleY: 0.4 }, 0.35); gioco.fromTo(hud, { alpha: 0 }, { alpha: 1 }, { duration: 0.25, unscaled: true }); slide.pause(); slide.resume(); slide.cancel(); gioco.tweens.kill(box);

📁 Due vie: clock del motore vs update manuale

gioco.to(target, props, durationOrOptions) è zucchero: BeeTween.to(..., { ...opts, clock: this.tweens }). Stesso schema per from / fromTo. Il terzo argomento è un numero (solo duration) oppure un oggetto. Le factory statiche forzano autoStart: true: start() gira nel costruttore.

Un new BeeTween({...}) senza clock non entra in gioco.tweens. Devi chiamare tween.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 registralo sul clock.

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

Se passi clock nel costruttore senza autoStart, clock.add(this) parte comunque: l’istanza resta in lista con running === false (zombie) finché non fai start() o cancel(). gioco.to non cade qui: autoStart è già true. I figli della timeline non ricevono clock.

📁 to / from / fromTo

Tre mode, stessi numeri: path stringa (x, transform.x), valore finito. readProp / writeProp camminano il path; un intermedio null non crea oggetti, la write è no-op. Non-numero in lettura → 0.

Mode From (catturato a #begin) To
to valore corrente sul target spec to; se l’end non è finito resta il from (no-op)
from spec from (se non finito: corrente) valore corrente al capture
fromTo spec from se la chiave c’è, altrimenti corrente spec to se la chiave c’è, altrimenti il from (hold)

Il mode lo imposta la factory. Nel costruttore nudo: se c’è from e anche chiavi in to → fromTo; solo from → from; altrimenti to. Le chiavi sono uno snapshot dello spec: mutare l’oggetto passato dopo new non cambia il tween.

Ease: nome in BeeEase (quadOut default, backOut, bounceOut, elasticOut…) o funzione t => t. Stringa sconosciuta → linear. L’input della curva è clampato in [0, 1]; l’uscita no: back/elastic/bounce possono overshootare. Non ci sono bounceIn, elasticIn, circ*: passa una funzione.

fromTo non filtra i non-finiti (a differenza di to / from): Number('foo') diventa NaN e finisce sul target. Tieni numeri veri nello spec. Vedi Known limitations.

📁 Capture: una tantum, dopo il delay

#capture gira una volta, in #begin, quando #captured è ancora false. Poi il flag resta true. start() azzera elapsed / cycles / backward / delay, non ricattura. Un restart (o un repeat della timeline sul figlio) riparte dallo snapshot originale, non dai valori correnti. È il motivo per cui un .to(box, { x: 200 }) in loop non resta inchiodato sul to.

Il capture sta dopo il delay: un to con delay: 0.4 legge il from quando la coda scade, non alla new. seek chiama #begin subito: salta il delay e cattura in quel momento.

Cambiare target o lo spec e rifare start() sulla stessa istanza è sbagliato: scrive i vecchi from/delta sul (nuovo) target. Serve un tween nuovo. Non esiste reset / recapture pubblico.

📁 Delay e duration 0

delay è Math.max(0, Number(spec.delay) || 0), copiato in #wait a ogni start. update scala #wait con lo step; l’eventuale overflow del delay diventa step del tween nello stesso frame. Durante l’attesa elapsed resta 0, progress è 0, onStart non è ancora partito.

duration non finita o negativa → default BEE_TWEEN_DEFAULTS.duration (0.4). 0 è legale: al primo update dopo #begin applica u = 1 e entra in #complete. progress con duration 0 vale sempre 1. repeat: Infinity a duration 0 completa un ciclo ogni update (snap). Un numero negativo passato a update sottrae da elapsed: non è un rewind ufficiale.

📁 play → delay → capture → complete

start() è un restart: azzera elapsed / finished / cancelled, rimette #wait = delay, re-add sul clock. Se il delay è 0 chiama #begin nello stesso call (capture, overwrite, onStart). Il complete non parte da start: parte da update quando elapsed raggiunge duration (o duration è 0).

Un ciclo standalone: armi, aspetti il delay, catturi una volta, il clock somma dt, applichi, chiudi o ripeti. seek non è questo percorso.

  1. 1. startAzzera, clock.add, #begin se delay 0.
  2. 2. delay#wait -= dt. Overflow nello stesso frame.
  3. 3. updateCapture una tantum, elapsed += dt, ease, write.
  4. 4. #completeYoyo/repeat oppure onComplete. Il clock toglie i one-shot.

📁 Due assi: scaled e unscaled

Il tween non sceglie il delta da solo se gli passi un numero. Sceglie solo se gli passi BeeTime (ha delta) o un oggetto { dt, unscaledDt }. Il clock del motore passa sempre this.time.

Argomento di update Tween scalato Tween 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. resolveStep legge tween.unscaled. Se muti solo useUnscaledTime a caldo, lo step non cambia.

pause() sull’istanza è un asse diverso dalla pausa di BeeTime: spegne running, tiene elapsed, resta in lista, il prossimo update esce subito. In freeze del motore (F4) tweens.tick parte lo stesso: i tween scalati sommano dt = 0, gli unscaled sommano unscaledDt.

gioco.to(box, { x: 640 }); // si ferma con F4 / pause() gioco.to(hud, { alpha: 1 }, { unscaled: true }); // sbagliato: il numero è già lo step, unscaled non si applica tw.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 sull’istanza.

Metodo elapsed running / paused finished / cancelled Clock / capture
start() azzera; #wait = delay true / false entrambi false re-add; #begin se delay 0; non ricattura
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 clock.remove(this); niente onComplete
seek(seconds) clamp [0, duration] invariati, salvo il ramo “fine one-shot” può marcare finished senza #complete se non era started: #begin (salta il delay)

start due volte è un restart, non un no-op: elapsed torna a 0, il pose riparte dallo snapshot. Dopo un one-shot finito, resume non riparte: serve start(). Dopo cancel, stesso discorso: solo start (riazzera cancelled e re-add).

Teardown = cancel() o gioco.tweens.kill(target). engine.destroy() chiama tweens.clear(): stacca clock e fa cancel su ciascun live.

📁 seek vs update

Due API, due contratti. update è il tempo che scorre: delay → begin → elapsed += dt → #apply → a u >= 1 entra in #complete (yoyo, repeat, onComplete). seek(seconds) è un pose setter: forza #begin se manca, scrive elapsed, applica l’ease, non chiama #complete.

Se t >= duration e repeat === 0 e non c’è yoyo, seek marca finished = true e running = false. Stop. Niente onComplete, niente ciclo yoyo/repeat, niente overflow. Con yoyo o repeat resta dove l’hai messo (clamp alla duration) e non gira i cicli.

BeeTimeline.#applyTime fa esattamente questo: tween.start() poi tween.seek(local). Perciò tween.onComplete di un figlio non parte mai dalla timeline, né da timeline.seek. I marker di clip stanno su timeline.call o su timeline.onComplete.

seek su un tween già finished continua a scrivere il pose (utile per lo scrub) ma non rimette running. Il prossimo update esce subito. seek prima di start spara onStart e salta il delay; il start successivo può risparare onStart.

📁 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 lo tiene in lista.

A fine ciclo #complete incrementa #cycles. Se ne restano: elapsed = 0; se yoyo inverte #backward, altrimenti #apply(0) (snap al from). L’overflow elapsed - duration si scarta: niente catch-up come in BeeTimer.

yoyo: true con repeat: 0 non torna indietro: un ciclo, fine, onComplete. Serve repeat >= 1 (o Infinity) per l’andata/ritorno. In reverse il pose è ease(1 - u). progress resta 0→1 sul ciclo locale, non è l’asse visivo.

Sulla timeline, yoyo/repeat dell’item allungano il ciclo del BeeTween ma lo slot resta tween.duration (un ciclo) e il figlio è guidato con seek, che non cicla. Yoyo/repeat della sequenza stanno sul costruttore della timeline.

Se imposti tween.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

Default overwrite: true (spec.overwrite !== false). In #begin, dopo il capture, se c’è un clock con overwrite parte clock.overwrite(this): sugli altri live con lo stesso target chiama steal(target, keys). steal toglie le path uguali (confronto stringa: x ≠ transform.x). A chiavi finite fa cancel().

Contratto “ultimo tween che fa #begin vince”. Un gioco.to(stessoOggetto, stesseChiavi) ruba le chiavi a un altro standalone e, via timeline.steal, anche ai figli di una timeline già armati. I figli non sono sul clock: la timeline fa prima #stealSiblings tra item armati, poi clock.overwrite.

steal su un tween non ancora catturato è no-op (#keys è []). Un tween ancora in delay non ha chiavi: l’overwrite di un altro non lo svuota; quando scade il delay ricattura e può riprendersi le proprietà. Vedi Known limitations.

gioco.tweens.kill(target) scorre i live: se item.target === target (riferimento) chiama cancel; altrimenti, se esiste killTarget (timeline), inoltra. La timeline non si spegne: cancella solo i figli su quel target. Il rewind di un repeat timeline rimette cancelled = false sul figlio; al prossimo arm start() non ricattura.

📁 Target morto

In testa a ogni update: se target && target.destroyed === true → cancel(), niente onComplete. Il pose resta all’ultimo write. BeeEntity.destroy mette proprio destroyed = true. Un plain object senza quel flag continua.

target === null (o assente): il check non scatta, writeProp è no-op, il tween arriva a #complete e spara onComplete (fantasma). destroyed truthy ma non === true non conta. Durante il delay il check è già attivo: un’entity distrutta in coda non fa onStart.

📁 BeeTweenClock

add è idempotente e assegna item.clock = this. remove stacca il puntatore se era questo clock, non chiama cancel. tick(time) chiama update sui non cancelled, poi compatta: via i cancelled e i finished && !loop. Un paused (running false, non finished) resta. clear azzera la lista, mette clock = null, poi cancel su ciascuno.

Nessun try/catch. Un throw in onStart / onUpdate / onComplete ferma il resto del tick di quel frame.

📁 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); // tween e timeline, 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)); }

I tween sul clock si muovono prima di scene e entity. Un onComplete che fa scenes.change vede ancora le entity vecchie in quello stesso frame.

📁 Esempio: slide, pulse HUD, kill

import { BeeEngine } from ‘beeengine’; const gioco = new BeeEngine(‘testCanvas’, 800, 600); const slide = gioco.to(box, { x: 640 }, { duration: 0.5, ease: ‘quadOut’, onComplete(tw) { // finished è già true qui; parte solo da update, non da seek } }); const pulse = gioco.to(overlay, { alpha: 0.35 }, { duration: 0.6, yoyo: true, repeat: Infinity, unscaled: true }); gioco.start((dt, input, time) => { if (input.wasPressed(‘Escape’)) time.togglePause(); if (input.wasPressed(‘KeyK’)) gioco.tweens.kill(box); // non chiamare slide.update(time): è già sul clock });

📁 Note per chi integra

Clock per HUD e freeze. gioco.to se deve vivere la pausa (con unscaled: true) o non hai un hook ogni frame. Update manuale se il tween muore con l’entity: cancel() in destroy — oppure lascia che destroyed === true lo cancelli al prossimo tick.

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”. Lo snapshot resta quello del primo #begin.

Callback di fine. Su un tween standalone usa onComplete (solo da update → #complete). Su una timeline usa call / timeline.onComplete, non tween.onComplete.

Callback che lancia. Non è wrappata. Gli altri live dello stesso tick possono non girare. Tieni onStart / onUpdate / 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. Il path felice (gioco.to + clock + update) è solido; BeeTimeline dipende da capture-una-tantum e da seek che non completa.

seek non è update. Niente #complete: onComplete non parte, yoyo/repeat non ciclano. A fine one-shot marca solo finished / spegne running. Salta il delay e può sparare onStart. Non “fixare” facendo entrare seek in #complete: spezzeresti le timeline. Se serve un complete esplicito, è un’altra API.

Capture una tantum. start() non rilegge il target. Cambiare spec/target e riavviare la stessa istanza è fuori contratto. Per un from nuovo, new di un altro tween.

fromTo e valori non finiti. to / from cadono sul corrente se l’end/start non è finito. fromTo spinge Number(...) crudo: non-numero → NaN sul target.

Clock add nel costruttore. new BeeTween({ clock }) senza autoStart entra in lista da fermo. gioco.to e i figli timeline non passano di lì.

Overwrite vs delay. steal prima del capture non toglie chiavi. Il tween in coda, quando inizia, può riprendersi le proprietà.

Yoyo senza repeat. yoyo: true e repeat: 0 fanno un’andata sola. L’andata/ritorno vuole repeat >= 1.

Yoyo/repeat del figlio in timeline. Lo slot dura un ciclo; il guida è seek. Non aspettarti il loop dell’item.

Target assente. Solo destroyed === true cancella. null completa da fantasma. La write su path rotto è silenziosa.

Residuo di dt / niente catch-up. Superata la duration, si applica l’estremo e si chiama #complete. L’overflow si scarta. Un dt più grosso di un ciclo non entra nel giro nuovo (il delay invece sì versa l’avanzo nello stesso frame).

Nessun try/catch. Throw in callback ferma il clock.tick di quel frame.

📁 Riepilogo API

Fluent: start, pause, resume, cancel, steal, seek, update ritornano this. Default frozen: BEE_TWEEN_DEFAULTS (duration 0.4, ease quadOut, delay 0, overwrite true, autoStart false sulle opzioni nudo — le factory statiche forzano true).

Membro Ritorna Scopo
new BeeTween(options?) BeeTween target, to / from, mode, duration, delay, ease, unscaled / useUnscaledTime, yoyo, repeat, overwrite, callback, clock, autoStart.
BeeTween.to(target, props, durationOrOptions?) BeeTween Mode to, autoStart: true. Terzo arg: numero o opzioni (possono sovrascrivere anche autoStart / clock).
BeeTween.from(target, props, durationOrOptions?) BeeTween Mode from. Stesso zucchero.
BeeTween.fromTo(target, from, to, durationOrOptions?) BeeTween From e to nello spec. Stesso zucchero.
gioco.to / from / fromTo BeeTween Come le statiche, con clock: gioco.tweens.
duration / elapsed / delay number Secondi. delay è lo spec; il countdown vivo è privato (#wait).
progress number In [0, 1]; se duration <= 0 vale 1. In yoyo resta 0→1 sul ciclo locale. In delay vale 0.
keys string[] Path catturate. Vuoto prima di #begin. Usato da clock.overwrite.
repeat / yoyo / loop number / boolean Giri extra; invertito a fine ciclo. loop è repeat === Infinity (ristabilito in start).
unscaled boolean Asse reale. Alias costruttore: useUnscaledTime.
overwrite boolean Default true. In #begin chiama clock.overwrite(this) se il clock ce l’ha.
running / paused / finished / cancelled boolean paused è getter su campo privato: true solo dopo pause su un tween che stava running.
onStart / onUpdate / onComplete function | null Ricevono (tween). onStart in #begin; onUpdate in ogni #apply (anche seek); onComplete solo da #complete via update.
clock BeeTweenClock | null Clock owner, o null se è un tween manuale / dopo remove.
ease function Risolta da resolveEase nel costruttore. Mutarla a caldo con una stringa rompe #apply.
start() this Azzera e parte. Restart se già running. Non ricattura.
pause() this Ferma, tiene elapsed. Nessun alias stop.
resume() this Riparte senza azzerare. No-op se finished o cancelled.
cancel() this Spegne, marca cancelled, toglie dal clock. Niente onComplete.
steal(target, keys) this Toglie le path. A chiavi finite: cancel. Usato da clock.overwrite.
seek(seconds) this Pose setter. Non completa. Può marcare finished su un one-shot.
update(dtOrTime) this Avanza. Accetta BeeTime, { dt, unscaledDt }, o un number (step esplicito).
BeeEase / resolveEase(nameOrFn) oggetto frozen / function Curve t in [0, 1]. Nome sconosciuto o non function → linear.
BEE_TWEEN_DEFAULTS oggetto frozen duration 0.4, delay 0, ease quadOut, unscaled false, yoyo false, repeat 0, autoStart false, overwrite true.
new BeeTweenClock() BeeTweenClock Lista live. gioco.tweens è l’istanza del motore.
clock.add / remove / tick / clear item o this add idempotente. tick compatta cancelled e one-shot finished. clear cancel di tutti.
clock.overwrite(source) this (clock) steal sugli altri live con lo stesso target e le stesse keys.
clock.kill(target) this (clock) Cancel i tween con quel target; sulle timeline chiama killTarget.
clock.size number Lunghezza della lista live (paused compresi, finished one-shot già compattati no).
gioco.timeline(options?) BeeTimeline Sequencer sullo stesso clock. I figli non sono live: vedi BeeTimeline.