Files
prosperon/tween.cm
2026-01-13 22:16:05 -06:00

261 lines
5.2 KiB
Plaintext

var Ease = use('ease')
var time = use('time')
var rate = 1/240
function make_engine(default_clock) {
return {
tweens: [],
default_clock: default_clock || null,
add(tween) {
this.tweens.push(tween)
},
remove(tween) {
this.tweens = this.tweens.filter(t => t != tween)
},
update(current_time) {
if (current_time == null) {
current_time = this.default_clock ? this.default_clock() : time.number()
}
for (var tween of this.tweens.slice()) {
tween._update(current_time)
}
},
clear() {
this.tweens = []
}
}
}
// Global fire-and-forget engine
var TweenEngine = make_engine(null)
var TweenProto = {
obj: null,
startVals: null,
endVals: null,
duration: 0,
easing: null,
startTime: 0,
onCompleteCallback: null,
onUpdateCallback: null,
engine: null,
to: function(props, duration, start_time) {
for (var key in props) {
var value = props[key]
if (is_object(value)) {
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
this.engine = this.engine || TweenEngine
if (start_time == null) {
this.startTime = this.engine.default_clock ? this.engine.default_clock() : time.number()
} else {
this.startTime = start_time
}
this.engine.add(this)
return this
},
ease: function(easingFn) {
this.easing = easingFn
return this
},
onComplete: function(callback) {
this.onCompleteCallback = callback
return this
},
onUpdate: function(cb) {
this.onUpdateCallback = cb
return this
},
_update: function(now) {
this.seek(now)
this.onUpdateCallback?.()
},
seek: function(global_time) {
var elapsed = global_time - this.startTime
var t = number.min(number.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('.')) {
var parts = key.split('.')
var objKey = parts[0]
var subKey = parts[1]
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)
}
},
cancel: function() {
if (this.engine) {
this.engine.remove(this)
}
},
toJSON: function() {
return {
startVals: this.startVals,
endVals: this.endVals,
duration: this.duration,
startTime: this.startTime,
easing: this.easing.name || 'linear'
}
}
}
function create_tween(obj) {
var tw = meme(TweenProto)
tw.obj = obj
tw.startVals = {}
tw.endVals = {}
tw.duration = 0
tw.easing = Ease.linear
tw.startTime = 0
tw.onCompleteCallback = function() {}
tw.onUpdateCallback = null
tw.engine = null
return tw
}
var TimelineProto = {
current_time: 0,
events: null,
playing: false,
last_tick: 0,
engine: null,
add_event: function(time_value, fn) {
this.events.push({ time: time_value, fn, fired: false })
},
add_tween: function(obj, props, duration, start_time) {
var tw = create_tween(obj)
tw.engine = this.engine
return tw.to(props, duration, start_time)
},
play: function() {
this.playing = true
this.last_tick = time.number()
var self = this
var loop = () => {
if (!self.playing) return
var now = time.number()
var dt = now - self.last_tick
self.last_tick = now
self.current_time += dt
self.seek(self.current_time)
$delay(loop, rate)
}
loop()
},
pause: function() {
this.playing = false
},
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) {
ev.fired = false
}
}
},
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())
}
}
}
function Timeline() {
var tl = meme(TimelineProto)
tl.current_time = 0
tl.events = []
tl.playing = false
tl.last_tick = 0
tl.engine = make_engine(() => tl.current_time)
return tl
}
// Live update loop for fire-and-forget tweens
function live_update_loop() {
TweenEngine.update()
$delay(live_update_loop, rate)
}
function tween(obj, engine) {
var tw = create_tween(obj)
if (engine) tw.engine = engine
return tw
}
function init(default_clock) {
TweenEngine.default_clock = default_clock || (() => time.number())
live_update_loop()
}
$delay(() => {
if (!TweenEngine.default_clock) {
init()
}
}, 0)
var tween_ret = {
init,
Timeline,
TweenEngine,
tween
}
return tween_ret