kapellmeister
Version:
Orchestration For Animated Transitions
499 lines (423 loc) • 12.5 kB
JavaScript
(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 });
}));