diff --git a/prosperon/tween.cm b/prosperon/tween.cm index 071c08e0..fe97be46 100644 --- a/prosperon/tween.cm +++ b/prosperon/tween.cm @@ -5,19 +5,24 @@ var rate = 1/240 var TweenEngine = { tweens: [], + default_clock: null, // Will be set during init add(tween) { this.tweens.push(tween) }, remove(tween) { this.tweens = this.tweens.filter(t => t != tween) }, - update(dt) { - var now = time.number() - for (var tween of this.tweens.slice()) { - tween._update(now) + update(current_time) { + // If no time provided, use real time + if (current_time == null) { + current_time = time.number() } - - $_.delay(_ => TweenEngine.update(), rate) + for (var tween of this.tweens.slice()) { + tween._update(current_time) + } + }, + clear() { + this.tweens = [] } } @@ -29,17 +34,26 @@ function Tween(obj) { this.easing = Ease.linear this.startTime = 0 this.onCompleteCallback = function() {} + this.onUpdateCallback = null + this.engine = null // Track which engine owns this tween } -Tween.prototype.to = function(props, duration) { +Tween.prototype.to = function(props, duration, start_time) { for (var key in props) { this.startVals[key] = this.obj[key] this.endVals[key] = props[key] } this.duration = duration - this.startTime = time.number() - - TweenEngine.add(this) + + // If no start_time provided, use the default clock + if (start_time == null) { + this.startTime = TweenEngine.default_clock ? TweenEngine.default_clock() : time.number() + } else { + this.startTime = start_time + } + + this.engine = this.engine || TweenEngine + this.engine.add(this) return this } @@ -59,27 +73,136 @@ Tween.prototype.onUpdate = function(cb) { } Tween.prototype._update = function(now) { - var elapsed = now - this.startTime - var t = Math.min(elapsed / this.duration, 1) + this.seek(now) + this.onUpdateCallback?.() +} + +Tween.prototype.seek = function(global_time) { + var elapsed = global_time - this.startTime + var t = Math.min(Math.max(elapsed / this.duration, 0), 1) var eased = this.easing(t) for (var key in this.endVals) { var start = this.startVals[key] var end = this.endVals[key] this.obj[key] = start + (end - start) * eased - this.onUpdateCallback?.() } - if (t == 1) { + if (t == 1 && this.engine) { this.onCompleteCallback() - TweenEngine.remove(this) + this.engine.remove(this) } } -function tween(obj) { - return new Tween(obj) +Tween.prototype.toJSON = function() { + return { + startVals: this.startVals, + endVals: this.endVals, + duration: this.duration, + startTime: this.startTime, + easing: this.easing.name || 'linear' + } } -$_.delay(_ => TweenEngine.update(), rate) +function Timeline() { + this.current_time = 0 + this.events = [] // { time, fn, fired } + this.playing = false + this.last_tick = 0 + this.engine = { + tweens: [], + add: TweenEngine.add.bind(this.engine), + remove: TweenEngine.remove.bind(this.engine), + update: TweenEngine.update.bind(this.engine), + clear: TweenEngine.clear.bind(this.engine) + } + this.engine.tweens = [] +} -return tween \ No newline at end of file +Timeline.prototype.add_event = function(time, fn) { + this.events.push({ time, fn, fired: false }) +} + +Timeline.prototype.add_tween = function(obj, props, duration, start_time) { + var tw = new Tween(obj) + tw.engine = this.engine + return tw.to(props, duration, start_time) +} + +Timeline.prototype.play = function() { + this.playing = true + this.last_tick = time.number() + var loop = () => { + if (!this.playing) return + var now = time.number() + var dt = now - this.last_tick + this.last_tick = now + this.current_time += dt + this.seek(this.current_time) + $_.delay(loop, rate) + } + loop() +} + +Timeline.prototype.pause = function() { + this.playing = false +} + +Timeline.prototype.seek = function(t) { + this.current_time = t + // Update all tweens in this timeline + this.engine.update(t) + // Fire any events + for (var ev of this.events) { + if (!ev.fired && t >= ev.time) { + ev.fn() + ev.fired = true + } else if (ev.fired && t < ev.time) { + // Reset fired flag when seeking backwards + ev.fired = false + } + } +} + +Timeline.prototype.toJSON = function() { + return { + current_time: this.current_time, + events: this.events.map(e => ({ time: e.time, fired: e.fired })), + tweens: this.engine.tweens.map(t => t.toJSON()) + } +} + +// Live update loop for fire-and-forget tweens +function live_update_loop() { + TweenEngine.update() + $_.delay(live_update_loop, rate) +} + +// Factory function +function tween(obj, engine) { + var tw = new Tween(obj) + if (engine) { + tw.engine = engine + } + return tw +} + +// Initialize with a default clock that returns real time +function init(default_clock) { + TweenEngine.default_clock = default_clock || (() => time.number()) + // Start the live update loop + live_update_loop() +} + +// Auto-init with real time if not explicitly initialized +$_.delay(() => { + if (!TweenEngine.default_clock) { + init() + } +}, 0) + +tween.init = init +tween.Timeline = Timeline +tween.TweenEngine = TweenEngine + +return tween