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) { push(this.tweens, tween) }, remove(tween) { this.tweens = filter(this.tweens, t => t != tween) }, update(current_time) { if (current_time == null) { current_time = this.default_clock ? this.default_clock() : time.number() } arrfor(this.tweens, function(tween) { 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) { arrfor(array(props), key => { var value = props[key] if (is_object(value)) { arrfor(array(value), subkey => { 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 = min(max(elapsed / this.duration, 0), 1) var eased = this.easing(t) arrfor(array(this.endVals), key => { var start = this.startVals[key] var end = this.endVals[key] var value = start + (end - start) * eased if (search(key, '.') != null) { var parts = array(key, '.') 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) { push(this.events, { 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 arrfor(this.events, function(ev) { 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: array(this.events, e => ({ time: e.time, fired: e.fired })), tweens: array(this.engine.tweens, 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() { if (is_function(TweenEngine.update)) 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