Skip to content

Instantly share code, notes, and snippets.

@thunder9
Created September 23, 2012 09:58
Show Gist options
  • Save thunder9/3769554 to your computer and use it in GitHub Desktop.
Save thunder9/3769554 to your computer and use it in GitHub Desktop.
A sequencer in JavaScript
// A sequencer in JavaScript
// https://github.com/thunder9
//
(function () {
var List = function () {
this.first = null;
this.last = null;
this.current = null;
};
List.prototype = {
insert: function (data) {
var node = this.first,
prev = null;
for (;;) {
if (node && node.data.time > data.time || !node) {
var newNode = {
next: node,
prev: prev,
data: data
};
if (prev) {
prev.next = newNode;
} else {
this.first = newNode;
}
if (node) {
node.prev = newNode;
} else {
this.last = newNode;
}
break;
}
prev = node;
node = node.next;
}
},
removeBetween: function (start, end) {
this.remove(function (data) {
return data.time >= start && data.time <= end;
});
},
remove: function (func) {
var node = this.first;
while (node) {
if (func(node.data)) {
if (node.prev) {
node.prev.next = node.next;
} else {
this.first = node.next;
}
if (node.next) {
node.next.prev = node.prev;
} else {
this.last = node.prev;
}
}
node = node.next;
}
},
next: function () {
if (this.current) {
this.current = this.current.next;
}
return this.current;
},
prev: function () {
if (this.current) {
this.current = this.current.prev;
}
return this.current;
},
rewind: function () {
this.current = this.first;
return this.current;
},
seek: function (pos) {
var node = this.first;
if (this.current && this.current.data.time < pos) {
node = this.current;
}
while (node) {
if (node.data.time >= pos) {
this.current = node;
break;
}
node = node.next;
}
if (pos < 0) { pos = 0; }
if (!node) {
this.current = this.last;
pos = this.last ? this.last.data.time : 0;
}
return pos;
}
};
var PlayState = function (sequencer, events, func_space, args) {
this.sequencer = sequencer;
this.events = createObject(events);
this.events.current = null;
this.func_space = func_space;
this.args = args;
this.frame_events = [];
this.children = [];
};
PlayState.prototype = {
play: function (parent) {
if (!(this.events.first)) {
return null;
}
if (!(this.playing)) {
if (parent && firing) {
this.mark = parent.mark;
this.base_time = firing.scheduled_time;
this.$ = createObject(parent.func_space, this.func_space);
} else {
this.mark = now();
this.base_time = 0;
this.$ = this.func_space;
}
this.playing = true;
this.paused = false;
this.repeated = this.accumulated = this.last_time = 0;
this.seek(0);
}
return this;
},
playChild: function (child, args) {
this.children.push(child.play(args, this));
return this;
},
stop: function () {
if (this.playing) {
this.playing = this.paused = false;
if (this.id) { clearTimeout(this.id); }
}
for (var i = 0; i < this.children.length; i++) {
this.children[i].stop();
this.children.shift();
}
return this;
},
togglePause: function () {
if (this.playing) {
if (this.paused) {
this.paused = false;
this.mark = now() - this.elapsed;
handleEvents(this);
handleEventsByFrame(this);
} else {
this.paused = true;
this.elapsed = now() - this.mark;
if (this.id) { clearTimeout(this.id); }
}
}
for (var i = 0; i < this.children.length; i++) {
if (this.children[i].playing()) {
this.children[i].togglePause();
} else {
this.children.splice(i, 1);
}
}
return this;
},
seek: function (pos) {
if (this.id) { clearTimeout(this.id); }
if (this.playing && !(this.paused)) {
this.mark -= this.sequencer.warp(this.events.seek(get_ms(pos)));
handleEvents(this);
handleEventsByFrame(this);
}
return this;
}
};
var createPlayState = function (sequencer, events, func_space, args, parent) {
var state = new PlayState(sequencer, events, func_space, args);
state.play(parent);
return {
playChild: function (child) {
state.playChild(child);
return this;
},
stop: function () {
state.stop();
return this;
},
togglePause: function () {
state.togglePause();
return this;
},
seek: function (pos) {
state.seek(pos);
return this;
},
playing: function () {
return this.playing;
},
$: state.$,
args: state.args,
};
};
var Sequencer = function (opts) {
if (!(this instanceof Sequencer)) {
return new Sequencer(opts);
}
var _events = new List(),
_plays = [],
_func_space = createObject(null);
this.appendEvents = function (events) {
for (var i = 0; i < events.length; i++) {
var e = events[i], time, callback, trigger;
if (classof(e) === 'Array') {
time = get_ms(e[0]);
callback = e[1];
trigger = e[2] || 'timeout';
} else {
time = get_ms(e.time);
callback = e.callback;
trigger = e.trigger || 'timeout';
}
if (isnum(time)) {
_events.insert({
time: time,
callback: callback,
trigger: trigger
});
}
}
return this;
};
this.removeEventsBetween = function (start, end) {
_events.removeBetween(get_ms(start), get_ms(end));
return this;
};
this.appendFunction = function (name, func) {
_func_space[name] = function (t) {
if (isnum(t)) {
return func(t);
} else if (firing) {
var state = firing.play_state;
return func(state.sequencer.inverse(state.elapsed) - state.accumulated - state.base_time);
} else {
return null;
}
};
return this;
};
this.removeFunction = function (name) {
if (_func_space.hasOwnProperty(name)) {
delete _func_space[name];
}
return this;
};
this.setDuration = function (duration) {
this.clearDuration();
_events.insert({
time: get_ms(duration),
callback: null,
trigger: 'timeout',
__END_OF_SEQUENCER__: true
});
return this;
};
this.clearDuration = function () {
_events.remove(function (data) {
return data.__END_OF_SEQUENCER__;
});
return this;
};
this.play = function (args, parent) {
if (!(_events.first)) { return; }
var state = createPlayState(this, _events, _func_space, args, parent);
if (state) {
_plays.push(state);
return state;
}
return this;
};
this.stop = function () {
for (var i = 0; i < _plays.length; i++) {
_plays[i].stop();
}
_plays = [];
return this;
};
this.togglePause = function () {
for (var i = 0; i < _plays.length; i++) {
if (_plays[i].playing()) {
_plays[i].togglePause();
} else {
_plays.splice(i, 1);
}
}
return this;
};
if (!opts) { opts = {}; }
this.times = opts.times || 1;
this.onTick = opts.onTick || null;
this.fps = opts.fps || 60;
this.warp = opts.warp || function (t) { return t; };
this.inverse = opts.inverse || function (w) { return w; };
if (classof(opts.events) === 'Array') {
this.appendEvents(opts.events);
}
if (opts.functionSpace || opts.$) {
var fs = opts.functionSpace || opts.$;
for (var f in fs) {
this.appendFunction(f, fs[f]);
}
}
if (opts.duration) {
this.setDuration(opts.duration);
}
};
Sequencer.prototype = {
appendEvent: function (time, callback, trigger) {
this.appendEvents([{
time: time,
callback: callback,
trigger: trigger
}]);
return this;
},
appendPeriodicEvents: function (start, end, interval, callback, trigger) {
start = get_ms(start);
end = get_ms(end);
interval = get_ms(interval);
for (var t = start; t <= end; t += interval) {
this.appendEvent(t, callback, trigger);
}
return this;
},
removeEventsAt: function (time) {
this.removeEventsBetween(time, time);
return this;
},
setTimes: function (times) {
this.times = times;
return this;
}
};
var firing = null;
var fire = function (events, context) {
var queue = [];
var f = function () {
var d = queue.shift();
for (var i = 0; i < d.events.length; i++) {
var event = d.events[i];
if (isfunc(event.callback)) {
firing = event;
event.callback.call(d.context, event);
firing = null;
}
}
};
if (isfunc(window.addEventListener) && isfunc(window.postMessage)) {
window.addEventListener('message', function(event) {
if (event.source === window && event.data === 'squencerjs-fire') {
event.stopPropagation();
f();
}
}, true);
fire = function (events, context) {
queue.push({ events: events, context: context });
window.postMessage('squencerjs-fire', '*');
};
} else {
fire = function (events, context) {
queue.push({ events: events, context: context });
setTimeout(f, 0);
};
}
fire(events, context);
};
var handleEvents = function (state) {
var node = null,
events = [];
state.id = null;
state.elapsed = now() - state.mark;
for (;;) {
var duration = state.events.last.data.time;
node = !node ? state.events.current : state.events.next();
if (node && node.prev && node.prev.data.__END_OF_SEQUENCER__) {
node = null;
duration = node.prev.data.time;
}
if (!node) {
if (++(state.repeated) < state.sequencer.times) {
state.accumulated += duration;
node = state.events.rewind();
} else {
state.playing = false;
for (var i = 0; i < state.children.length; i++) {
if (!state.children[i].playing()) {
state.children.splice(i, 1);
}
}
break;
};
}
if (node) {
node.data.scheduled_time = state.base_time + state.accumulated + state.sequencer.warp(node.data.time);
if (node.data.trigger === 'frame') {
state.frame_events.push(node.data);
} else if (node.data.scheduled_time <= state.elapsed) {
node.data.play_state = state;
events.push(node.data);
} else {
var timeout = node.data.scheduled_time - state.elapsed;
state.id = setTimeout(handleEvents, timeout, state);
break;
}
}
}
if (events.length > 0) {
fire(events, state);
}
};
var handleEventsByFrame = function (state) {
(function animate () {
if (state.playing && !(state.paused)) {
var events = [];
state.elapsed = now() - state.mark;
if (state.elapsed - state.last_time >= 1000 / state.sequencer.fps * 0.97) {
state.last_time = state.elapsed;
while (state.frame_events.length > 0) {
if (state.frame_events[0].scheduled_time <= state.elapsed) {
state.frame_events[0].play_state = state;
events.push(state.frame_events.shift());
} else {
break;
}
}
if (isfunc(state.sequencer.onTick)) {
events.push({
callback: state.sequencer.onTick,
play_state: state,
trigger: 'frame'
});
}
if (events.length > 0) {
fire(events, state);
}
}
reqAnimFrame(animate);
}
})();
};
var classof = function (obj) {
if (obj === null) return 'Null';
if (obj === undefined) return 'Undefined';
return Object.prototype.toString.call(obj).slice(8, -1);
};
var isnum = function (obj) {
return classof(obj) === 'Number';
};
var isfunc = function (obj) {
return classof(obj) === 'Function';
};
var get_ms = function (time) {
if (isnum(time)) {
return time;
} else if (classof(time) === 'String') {
var ms = parseFloat(time);
if (isNaN(ms)) {
ms = Date.parse(time);
}
return ms;
} else {
return null;
}
};
var now = (function () {
var p = window.performance || {},
pn = p.now || p.webkitNow || p.msNow || p.oNow || p.mozNow;
if (isfunc(pn)) {
return function () { return pn.call(p); };
} else if (isfunc(Date.now)) {
return Date.now;
} else {
return function () { return (new Date()).getTime(); };
}
})();
var reqAnimFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function (callback) { setTimeout(callback, 1000 / 60); };
var createObject = (function () {
if (isfunc(Object.create)) {
return Object.create;
} else {
return function (prototype, descs) {
var F = function () {
if (classof(descs) === 'Object'){
for (var p in descs) {
this[p] = descs[p];
}
}
};
F.prototype = prototype;
return new F();
};
}
})();
this.Sequencer = this.Sequencer || Sequencer;
}).call(this);
// TODO:
// * Improve interface
// * Improve performance
// * Test
<!DOCTYPE html>
<html>
<head>
<script src="sequencer.js"></script>
</head>
<body>
<button onclick="play()">Play</button>
<button onclick="stop()">Stop</button>
<button onclick="togglePause()">Toggle pause</button>
<button onclick="clearLog()">Clear log</button>
<div id="box"></div>
<pre id="out"></pre>
<script>
var p = function (o) {
var out = document.getElementById('out');
p = function (o) {
out.innerHTML += '// ' + JSON.stringify(o);
out.appendChild(document.createElement('br'));
}
p(o);
};
var t = function (o, expected) {
var out = document.getElementById('out');
t = function (o, expected) {
var s = JSON.stringify(o);
out.innerHTML += 'T/ ' + s + ' ...' + (s === expected ? 'ok' : 'NG');
out.appendChild(document.createElement('br'));
}
t(o, expected);
};
var clearLog = function () {
document.getElementById('out').innerHTML = '';
document.getElementById('box').innerHTML = '';
};
var now = function () {
var p = window.performance || {},
pn = p.now || p.webkitNow || p.msNow || p.oNow || p.mozNow;
if (typeof pn === 'function') {
now = function () { return pn.call(p); };
} else if (typeof Date.now === 'function') {
now = Date.now;
} else {
now = function () { return (new Date()).getTime(); };
}
return now();
};
var mark = now();
//var seq = new Sequencer();
//t(seq, '{"times":1}');
/*
mark = now();
seq.appendEvent(1000, function () { t(now() - mark, '1000'); })
.appendEvent(2000, function () { t(now() - mark, '2000'); })
.play();
p('(^_^)');
*/
var prn = function (event, msg) {
var s = event.play_state;
var elapsed = s.elapsed;
var scheduled = s.base_time + s.accumulated + event.time;
var diff = elapsed - scheduled;
p(msg + ' ' + elapsed.toPrecision(6) + ' ' + scheduled.toPrecision(6) + ' ' + diff.toPrecision(3));
};
var seq2 = Sequencer({
events: [
{ time: 0, callback: function (e) { prn(e, 'AA'); }, trigger: 'frame' },
{ time: 10, callback: function (e) { prn(e, 'BB'); } },
{ time: 10, callback: function (e) { prn(e, 'CC'); } }
],
times: 2
});
var seq1 = Sequencer({
events: [
{ time: 100, callback: function (e) {
prn(e, 'A ');
prn(e, 'f1:' + this.$.f1());
this.playChild(seq2);
} },
{ time: 200, callback: function (e) { prn(e, 'B '); this.playChild(seq2); } },
{ time: 350, callback: function (e) { prn(e, 'C '); } }
],
onTick: function () {
this.args[0].style.backgroundColor = 'rgba(' + this.args[1] + this.$.alpha() + ')';
this.args[0].innerHTML = this.$.alpha().toString();
},
duration: 5000
});
var seq = Sequencer({
events: [
[3000, function () { this.playChild(seq1, getColoredDiv()); }],
[1000, function (e) {
prn(e, 'f1:' + this.$.f1());
prn(e, 'f2:' + this.$.f2());
this.playChild(seq1, getColoredDiv());
}],
[2000, function () { this.playChild(seq1, getColoredDiv()); }]
],
functionSpace: {
f1: function (t) { return 0.002 * t; },
f2: function (t) { return 2 * t; },
alpha: function (t) { return 0.5 * (Math.sin(t * Math.PI / 100) + 1); }
},
times: 1,
onTick: function () {
this.args[0].style.backgroundColor = 'rgba(' + this.args[1] + this.$.alpha() + ')';
this.args[0].innerHTML = this.$.alpha().toString();
},
fps: 30,
warp: function (t) { return Math.pow(t, 1.3); },
inverse: function (w) { return Math.pow(w, 1 / 1.3); }
});
var getColoredDiv = function () {
var c = function () { return (Math.round(Math.random() * 255)).toString(); };
var rgb = [c(), c(), c(), ''].join(',');
var el = document.createElement('div');
el.style.width = '300px';
el.style.height = '20px';
document.getElementById('box').appendChild(el);
return [el, rgb];
};
var play = function () {
mark = now();
seq.play(getColoredDiv());
p('(^_^)Play(^_^)');
};
var stop = function () {
seq.stop();
p('(^_^)Stop(^_^)');
};
var togglePause = function () {
seq.togglePause();
p('(^_^)Pause(^_^)');
};
</script>
</body>
</html>
@thunder9
Copy link
Author

thunder9 commented Oct 6, 2012

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment