Files
prosperon/tween.cm
2025-11-22 09:43:51 -06:00

241 lines
5.4 KiB
Plaintext

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