|
/*************************************************/ |
|
/************* EventStream ***********************/ |
|
/*************************************************/ |
|
function EventStream(binder) { |
|
this.binder = binder; |
|
this.reset(); |
|
} |
|
|
|
EventStream.prototype.reset = function() { |
|
this.lastTime = 0; |
|
this.times = []; |
|
this.events = []; |
|
} |
|
|
|
EventStream.prototype.activate = function() { |
|
var me = this; |
|
this.off = binder(function(ev) { |
|
me.lastTime = Date.now(); |
|
me.times.push(me.lastTime); |
|
me.events.push(ev); |
|
}); |
|
} |
|
|
|
EventStream.prototype.deactivate = function() { |
|
this.off(); |
|
this.reset(); |
|
} |
|
|
|
EventStream.prototype.map = function(f) { |
|
var parent = this, |
|
str = new EventStream(); |
|
str.before = function(t) { |
|
var parOcc = parent.before(t); |
|
return parOcc ? f(parOcc) : null; |
|
} |
|
return str; |
|
} |
|
|
|
EventStream.prototype.before = function(t) { |
|
if(t <= this.times[0]) |
|
return null; |
|
else { |
|
var idx = this.searchEv(t, 0, this.times.length-1); |
|
return { time: this.times[idx], data: this.events[idx] }; |
|
} |
|
} |
|
|
|
EventStream.prototype.searchEv = function(t, start, end) { |
|
if(end-start < 2) |
|
return start; |
|
var mid = start + Math.floor((end-start)/2); |
|
return (t <= this.times[mid]) ? this.searchEv(start, mid) : this.searchEv(mid, end); |
|
} |
|
|
|
/*************************************************/ |
|
/************* Behavior **************************/ |
|
/*************************************************/ |
|
|
|
function curry(fn) { |
|
var len = fn.length, ctx; |
|
return makeLambda(0, []); |
|
|
|
function makeLambda(i, args, ctx) { |
|
return i < len ? |
|
function(a) { |
|
var newCtx = (this && this !== window) ? this : ctx, |
|
newArgs = Array.prototype.slice.call(arguments); |
|
return makeLambda(i+newArgs.length, args.concat(newArgs), newCtx); |
|
} : |
|
fn.apply(ctx, args) |
|
} |
|
} |
|
|
|
function toRadians (angle) { |
|
return angle * (Math.PI / 180); |
|
} |
|
|
|
function Behavior(getter) { |
|
this.getter = getter; |
|
} |
|
|
|
Behavior.prototype.at = function(t) { |
|
return this.getter(t); |
|
} |
|
|
|
Behavior.prototype.ap = function(xb) { |
|
return new Behavior( function(t) { |
|
return b.at(t)( xb.at(t) ); |
|
}); |
|
} |
|
|
|
Behavior.prototype.until = function until(evStream) { |
|
var me = this; |
|
return new Behavior(function(t) { |
|
var occ = evStream.before(t); |
|
return occ ? occ.data.at(t) : me.at(t); |
|
}); |
|
} |
|
|
|
|
|
function B(getter) { |
|
return new Behavior(getter); |
|
} |
|
|
|
B.const = function(x) { |
|
return B(function() { return x; }); |
|
} |
|
|
|
B.tmap = function(b, tb) { |
|
return B(function(t) { |
|
return b.at( tb.at(t) ); |
|
}); |
|
} |
|
|
|
B.lift0 = function(a) { |
|
return (a instanceof Behavior) ? a : B.const(a); |
|
}; |
|
|
|
B.lift = function(fn) { |
|
var me = this; |
|
fn = curry(fn); |
|
return function(a,b,c,d) { |
|
switch (arguments.length) { |
|
case 0: return lift0(fn); |
|
case 1: return lift0(fn).ap(a); |
|
case 2: return lift0(fn).ap(a).ap(b); |
|
case 3: return lift0(fn).ap(a).ap(b).ap(c); |
|
case 4: return lift0(fn).ap(a).ap(b).ap(c).ap(d); |
|
} |
|
} |
|
} |
|
|
|
B.liftO = function(obj) { |
|
var res = {}, |
|
keys = Object.keys(obj); |
|
for(var i = 0, len = keys.length; i < len; i++) { |
|
var key = keys[i]; |
|
res[key] = this.lift0( obj[key] ); |
|
} |
|
return res; |
|
} |
|
|
|
B.timeB = B(function(t) { return t; }); |
|
|
|
B.sin = function(deg) { |
|
return Math.sin( toRadians(deg) ) |
|
} |
|
|
|
B.cos = function(deg) { |
|
return Math.cos( toRadians(deg) ) |
|
} |
|
|
|
/*************************************************/ |
|
/************* Toy *******************************/ |
|
/*************************************************/ |
|
|
|
function Toy( canvas, graphics ) { |
|
this.time = B.time; |
|
this.canvas = canvas instanceof Element ? canvas : document.getElementById(canvas); |
|
this.ctx = this.canvas.getContext('2d');; |
|
this.eventMap = {}; |
|
this.graphics = new graphics(); |
|
} |
|
|
|
Toy.prototype.on = function(ev) { |
|
var canvas = this.canvas; |
|
return this.eventMap[event] || (this.eventMap[event] = new EventStream(bindEvStrem(event))) |
|
|
|
function bindEvStrem(ev) { |
|
return function(cb) { |
|
canvas.addEventListener(ev, cb); |
|
} |
|
} |
|
} |
|
|
|
Toy.prototype.start = function() { |
|
var me = this, |
|
ctx = this.ctx; |
|
|
|
me.stopped = false; |
|
me.startTime = Date.now(); |
|
update(); |
|
|
|
function update() { |
|
var t = ( Date.now() - me.startTime ); |
|
ctx.globalCompositeOperation = 'destination-over'; |
|
ctx.clearRect(0,0, me.canvas.width , me.canvas.height); |
|
ctx.fillStyle = '#FFFFFF'; |
|
ctx.strokeStyle = '#000000'; |
|
|
|
var keys = Object.keys(me.graphics); |
|
for(var i = 0, len = keys.length; i < len; i++) { |
|
var g = me.graphics[keys[i]]; |
|
g.render(me.ctx, t); |
|
} |
|
if(!me.stopped) |
|
requestAnimationFrame(update); |
|
} |
|
} |
|
|
|
Toy.prototype.stop = function() { |
|
this.stopped = true; |
|
var keys = Object.keys(this.eventMap); |
|
for(var i = 0, len = keys.length; i < len; i++) { |
|
var key = keys[i]; |
|
this.eventMap[key].reset(); |
|
} |
|
} |
|
|
|
function graphicB(cfg) { |
|
return new GraphicB( B.liftO(cfg) ); |
|
} |
|
|
|
var G = {}; |
|
|
|
G.rectB = function(cfg) { |
|
cfg.type = 'r'; |
|
return graphicB( cfg ); |
|
} |
|
|
|
G.circleB = function(cfg) { |
|
cfg.type = 'c'; |
|
return graphicB( cfg ); |
|
} |
|
|
|
G.textB = function(cfg) { |
|
cfg.type = 't'; |
|
cfg.fill = cfg.fill || 'black'; |
|
cfg.stroke = cfg.stroke || 'black'; |
|
return graphicB( cfg ); |
|
} |
|
|
|
G.pathB = function(cfg) { |
|
cfg.type = 'path'; |
|
return graphicB( cfg ); |
|
} |
|
|
|
function GraphicB(config) { |
|
this.config = config; |
|
} |
|
|
|
GraphicB.prototype.bigger = function(scale) { |
|
var cfg = Object.create(this.config); |
|
cfg.type = 'bigger'; |
|
cfg.graphic = this; |
|
cfg.scale = scale; |
|
return graphicB( cfg ); |
|
} |
|
|
|
GraphicB.prototype.rotate = function(angle, origin) { |
|
var cfg = Object.create(this.config); |
|
cfg.type = 'rotate'; |
|
cfg.graphic = this; |
|
cfg.angle = angle; |
|
cfg.origin = origin; |
|
return graphicB( cfg ); |
|
} |
|
|
|
GraphicB.prototype.render = function(ctx, t) { |
|
var cfg = this.config, |
|
type = cfg.type.at(t), |
|
x = cfg.x ? cfg.x.at(t) : 0, |
|
y = cfg.y ? cfg.y.at(t) : 0; |
|
|
|
ctx.save(); |
|
|
|
cfg.stroke && (ctx.strokeStyle = cfg.stroke.at(t)); |
|
cfg.fill && (ctx.fillStyle = cfg.fill.at(t)); |
|
|
|
if(type == 'r') { |
|
var width = cfg.width ? cfg.width.at(t) : 50, |
|
height = cfg.height ? cfg.height.at(t) : 50; |
|
cfg.fill && ctx.fillRect(x, y, width, height); |
|
cfg.stroke && ctx.strokeRect(x, y, width, height); |
|
} else if(type == 'c') { |
|
var r = cfg.radius ? cfg.radius.at(t) : 50; |
|
|
|
ctx.beginPath(); |
|
ctx.arc(x, y, r, 0, 2 * Math.PI); |
|
|
|
cfg.fill && ctx.fill(); |
|
cfg.stroke && ctx.stroke(); |
|
} else if (type == 'p') { |
|
var path = new Path2D( cfg.path.at(t) ); |
|
|
|
cfg.fill && ctx.fill(path); |
|
cfg.stroke && ctx.stroke(path); |
|
} else if(type == 't') { |
|
var text = cfg.text.at(t), |
|
w = cfg.maxWidth ? cfg.x.maxWidth(t) : 100; |
|
|
|
cfg.font && (ctx.font = cfg.font.at(t)); |
|
ctx.fillText(text, x, y, w); |
|
ctx.strokeText(text, x, y, w); |
|
} else if(type == 'bigger') { |
|
var graphic = cfg.graphic.at(t), |
|
scale = cfg.scale.at(t) |
|
|
|
ctx.scale(scale, scale); |
|
graphic.render(ctx, t); |
|
} |
|
else if(type == 'rotate') { |
|
var graphic = cfg.graphic.at(t), |
|
angle = cfg.angle.at(t), |
|
origin = cfg.origin ? cfg.origin.at(t) : null; |
|
|
|
if(origin) |
|
ctx.translate(origin[0], origin[1]); |
|
ctx.rotate(angle); |
|
graphic.render(ctx, t); |
|
} |
|
|
|
ctx.restore(); |
|
} |