UNPKG

kapellmeister

Version:

Orchestration For Animated Transitions

499 lines (423 loc) 12.5 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.Kapellmeister = {})); }(this, function (exports) { 'use strict'; var frame = 0, // is an animation frame pending? timeout = 0, // is a timeout pending? interval = 0, // are any timers active? pokeDelay = 1000, // how frequently we check for clock skew taskHead, taskTail, clockLast = 0, clockNow = 0, clockSkew = 0, clock = typeof performance === "object" && performance.now ? performance : Date, setFrame = typeof window === "object" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(f) { setTimeout(f, 17); }; function now() { return clockNow || (setFrame(clearNow), clockNow = clock.now() + clockSkew); } function clearNow() { clockNow = 0; } function Timer() { this._call = this._time = this._next = null; } Timer.prototype = timer.prototype = { constructor: Timer, restart: function(callback, delay, time) { if (typeof callback !== "function") throw new TypeError("callback is not a function"); time = (time == null ? now() : +time) + (delay == null ? 0 : +delay); if (!this._next && taskTail !== this) { if (taskTail) taskTail._next = this; else taskHead = this; taskTail = this; } this._call = callback; this._time = time; sleep(); }, stop: function() { if (this._call) { this._call = null; this._time = Infinity; sleep(); } } }; function timer(callback, delay, time) { var t = new Timer; t.restart(callback, delay, time); return t; } function timerFlush() { now(); // Get the current time, if not already set. ++frame; // Pretend we’ve set an alarm, if we haven’t already. var t = taskHead, e; while (t) { if ((e = clockNow - t._time) >= 0) t._call.call(null, e); t = t._next; } --frame; } function wake() { clockNow = (clockLast = clock.now()) + clockSkew; frame = timeout = 0; try { timerFlush(); } finally { frame = 0; nap(); clockNow = 0; } } function poke() { var now = clock.now(), delay = now - clockLast; if (delay > pokeDelay) clockSkew -= delay, clockLast = now; } function nap() { var t0, t1 = taskHead, t2, time = Infinity; while (t1) { if (t1._call) { if (time > t1._time) time = t1._time; t0 = t1, t1 = t1._next; } else { t2 = t1._next, t1._next = null; t1 = t0 ? t0._next = t2 : taskHead = t2; } } taskTail = t0; sleep(time); } function sleep(time) { if (frame) return; // Soonest alarm already set, or will be. if (timeout) timeout = clearTimeout(timeout); var delay = time - clockNow; // Strictly less than if we recomputed clockNow. if (delay > 24) { if (time < Infinity) timeout = setTimeout(wake, time - clock.now() - clockSkew); if (interval) interval = clearInterval(interval); } else { if (!interval) clockLast = clock.now(), interval = setInterval(poke, pokeDelay); frame = 1, setFrame(wake); } } function timeout$1(callback, delay, time) { var t = new Timer; delay = delay == null ? 0 : +delay; t.restart(function(elapsed) { t.stop(); callback(elapsed + delay); }, delay, time); return t; } function interval$1(callback, delay, time) { var t = new Timer, total = delay; if (delay == null) return t.restart(callback, delay, time), t; delay = +delay, time = time == null ? now() : +time; t.restart(function tick(elapsed) { elapsed += total; t.restart(tick, total += delay, time); callback(elapsed); }, delay, time); return t; } let transitionId = 0; function getTransitionId() { return ++transitionId; } function extend(obj, props) { for (const i in props) { obj[i] = props[i]; } } function once(func) { let called = false; return function transitionEvent() { if (!called) { called = true; func.call(this); } }; } function isNamespace(prop) { return typeof prop === 'object' && Array.isArray(prop) === false; } const linear = t => { return +t; }; const timingDefaults = { delay: 0, duration: 250, ease: linear }; class Events { constructor(config) { this.start = null; this.interrupt = null; this.end = null; if (config.events) { Object.keys(config.events).forEach(d => { if (typeof config.events[d] !== 'function') { throw new Error('Event handlers must be a function'); } else { this[d] = once(config.events[d]); } }); } } } class BaseNode { constructor(state) { this.state = state || {}; } transition(config) { if (Array.isArray(config)) { for (const item of config) { this.parse(item); } } else { this.parse(config); } } isTransitioning() { return !!this.transitionData; } stopTransitions() { const transitions = this.transitionData; if (transitions) { Object.keys(transitions).forEach(t => { transitions[t].timer.stop(); }); } } setState(update) { if (typeof update === 'function') { extend(this.state, update(this.state)); } else { extend(this.state, update); } } parse(config) { const clone = { ...config }; const events = new Events(clone); if (clone.events) { delete clone.events; } const timing = { ...timingDefaults, ...(clone.timing || {}), time: now() }; if (clone.timing) { delete clone.timing; } Object.keys(clone).forEach(stateKey => { const tweens = []; const next = clone[stateKey]; if (isNamespace(next)) { Object.keys(next).forEach(attr => { const val = next[attr]; if (Array.isArray(val)) { if (val.length === 1) { tweens.push(this.getTween(attr, val[0], stateKey)); } else { this.setState(state => { return { [stateKey]: { ...state[stateKey], [attr]: val[0] } }; }); tweens.push(this.getTween(attr, val[1], stateKey)); } } else if (typeof val === 'function') { const getNameSpacedCustomTween = () => { const kapellmeisterNamespacedTween = t => { this.setState(state => { return { [stateKey]: { ...state[stateKey], [attr]: val(t) } }; }); }; return kapellmeisterNamespacedTween; }; tweens.push(getNameSpacedCustomTween); } else { this.setState(state => { return { [stateKey]: { ...state[stateKey], [attr]: val } }; }); tweens.push(this.getTween(attr, val, stateKey)); } }); } else { if (Array.isArray(next)) { if (next.length === 1) { tweens.push(this.getTween(stateKey, next[0], null)); } else { this.setState({ [stateKey]: next[0] }); tweens.push(this.getTween(stateKey, next[1], null)); } } else if (typeof next === 'function') { const getCustomTween = () => { const kapellmeisterTween = t => { this.setState({ [stateKey]: next(t) }); }; return kapellmeisterTween; }; tweens.push(getCustomTween); } else { this.setState({ [stateKey]: next }); tweens.push(this.getTween(stateKey, next, null)); } } this.update({ stateKey, timing, tweens, events, status: 0 }); }); } getTween(attr, endValue, nameSpace) { return () => { const begValue = nameSpace ? this.state[nameSpace][attr] : this.state[attr]; if (begValue === endValue) { return null; } const i = this.getInterpolator(begValue, endValue, attr, nameSpace); let stateTween; if (nameSpace === null) { stateTween = t => { this.setState({ [attr]: i(t) }); }; } else { stateTween = t => { this.setState(state => { return { [nameSpace]: { ...state[nameSpace], [attr]: i(t) } }; }); }; } return stateTween; }; } update(transition) { if (!this.transitionData) { this.transitionData = {}; } this.init(getTransitionId(), transition); } init(id, transition) { const n = transition.tweens.length; const tweens = new Array(n); const queue = elapsed => { transition.status = 1; transition.timer.restart(start, transition.timing.delay, transition.timing.time); if (transition.timing.delay <= elapsed) { start(elapsed - transition.timing.delay); } }; this.transitionData[id] = transition; transition.timer = timer(queue, 0, transition.timing.time); const start = elapsed => { if (transition.status !== 1) return stop(); for (const tid in this.transitionData) { const t = this.transitionData[tid]; if (t.stateKey !== transition.stateKey) { continue; } if (t.status === 3) { return timeout$1(start); } if (t.status === 4) { t.status = 6; t.timer.stop(); if (t.events.interrupt) { t.events.interrupt.call(this); } delete this.transitionData[tid]; } else if (+tid < id) { t.status = 6; t.timer.stop(); delete this.transitionData[tid]; } } timeout$1(() => { if (transition.status === 3) { transition.status = 4; transition.timer.restart(tick, transition.timing.delay, transition.timing.time); tick(elapsed); } }); transition.status = 2; if (transition.events.start) { transition.events.start.call(this); } if (transition.status !== 2) { return; } transition.status = 3; let j = -1; for (let i = 0; i < n; ++i) { const res = transition.tweens[i](); if (res) { tweens[++j] = res; } } tweens.length = j + 1; }; const tick = elapsed => { let t = 1; if (elapsed < transition.timing.duration) { t = transition.timing.ease(elapsed / transition.timing.duration); } else { transition.timer.restart(stop); transition.status = 5; } let i = -1; while (++i < tweens.length) { tweens[i](t); } if (transition.status === 5) { if (transition.events.end) { transition.events.end.call(this); } stop(); } }; const stop = () => { transition.status = 6; transition.timer.stop(); delete this.transitionData[id]; for (const _ in this.transitionData) return; delete this.transitionData; }; } } exports.BaseNode = BaseNode; exports.now = now; exports.timer = timer; exports.interval = interval$1; exports.timeout = timeout$1; Object.defineProperty(exports, '__esModule', { value: true }); }));