var Ease = use('ease') var time = use('time') 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(current_time) { // If no time provided, use real time if (current_time == null) { current_time = time.number() } for (var tween of this.tweens.slice()) { tween._update(current_time) } }, clear() { this.tweens = [] } } function Tween(obj) { this.obj = obj this.startVals = {} this.endVals = {} this.duration = 0 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, start_time) { for (var key in props) { var value = props[key] if (typeof value == 'object' && value != null && !Array.isArray(value)) { // Handle nested objects by flattening them for (var subkey in value) { var flatKey = key + '.' + subkey this.startVals[flatKey] = this.obj[key] ? this.obj[key][subkey] : undefined this.endVals[flatKey] = value[subkey] } } else { this.startVals[key] = this.obj[key] this.endVals[key] = value } } this.duration = duration // 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 } Tween.prototype.ease = function(easingFn) { this.easing = easingFn return this } Tween.prototype.onComplete = function(callback) { this.onCompleteCallback = callback return this } Tween.prototype.onUpdate = function(cb) { this.onUpdateCallback = cb return this } Tween.prototype._update = function(now) { 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] var value = start + (end - start) * eased if (key.includes('.')) { // Handle nested object properties var parts = key.split('.') var objKey = parts[0] var subKey = parts[1] // Ensure the nested object exists if (!this.obj[objKey]) { this.obj[objKey] = {} } this.obj[objKey][subKey] = value } else { this.obj[key] = value } } if (t == 1 && this.engine) { this.onCompleteCallback() this.engine.remove(this) } } Tween.prototype.cancel = function() { if (this.engine) { this.engine.remove(this) } } Tween.prototype.toJSON = function() { return { startVals: this.startVals, endVals: this.endVals, duration: this.duration, startTime: this.startTime, easing: this.easing.name || 'linear' } } 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 = [] } 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