js-2dmath
Version:
Fast 2d geometry math: Vector2, Rectangle, Circle, Matrix2x3 (2D transformation), Circle, BoundingBox, Line2, Segment2, Intersections, Distances, Transitions (animation/tween), Random numbers, Noise
434 lines (354 loc) • 11.2 kB
JavaScript
/*
* Stability: 2 (fixes / performance improvements)
*
* @TODO expand all function, do not generate with loops
*/
var array = require("array-enhancements"),
Beizer = require("./beizer.js"),
beizer_cubic = Beizer.cubic,
beizer_quadric = Beizer.quadric,
beizer_solve = Beizer.solve,
pow = Math.pow,
sin = Math.sin,
acos = Math.acos,
cos = Math.cos,
PI = Math.PI,
t = {},
k,
Transitions = {},
CHAIN = 1,
STOP = 2,
IGNORE = 3,
CANCEL = 4;
function createCubic(cp1x, cp1y, cp2x, cp2y) {
var curve = beizer_cubic(0, 0, cp1x, cp1y, cp2x, cp2y, 1, 1),
aux = [0, 0];
return function(t) {
beizer_solve(aux, curve, t);
return aux[1];
}
}
function createQuadric(cp1x, cp1y) {
var curve = beizer_quadric(0, 0, cp1x, cp1y, 1, 1),
aux = [0, 0];
return function(t) {
beizer_solve(aux, curve, t);
return aux[1];
}
}
function Pow(pos, x) {
return pow(pos, (x && x[0]) || 6);
}
function Expo(pos) {
return pow(2, 8 * (pos - 1));
}
function Circ(pos) {
return 1 - sin(acos(pos));
}
function Sine(pos) {
return 1 - cos(pos * PI / 2);
}
function Back(pos, x) {
x = (x && x[0]) || 1.618;
return pow(pos, 2) * ((x + 1) * pos - x);
}
function Bounce(pos) {
var value, a, b;
for (a = 0, b = 1; true; a += b, b /= 2) {
if (pos >= (7 - 4 * a) / 11) {
value = b * b - pow((11 - 6 * a - 11 * pos) / 4, 2);
break;
}
}
return value;
}
function Elastic(pos, x) {
return pow(2, 10 * --pos) * cos(20 * pos * PI * ((x && x[0]) || 1) / 3);
}
/**
* Just return what you sent
* @param {Number} pos
*/
function linear(pos) {
return pos;
}
/**
* Wrap your transaction with In/Out/InOut modifiers.
* @param {String} name
* @param {Function} transition
*/
function create(name, transition) {
//Transitions[name] = function (pos) {
// return transition(pos);
//};
//Transitions[name + "In"] = Transitions[name];
Transitions[name] = transition;
Transitions[name + "In"] = transition;
Transitions[name + "Out"] = function (pos) {
return 1 - transition(1 - pos);
};
Transitions[name + "InOut"] = function (pos) {
return (pos <= 0.5 ? transition(2 * pos) : (2 - transition(2 * (1 - pos)))) / 2;
};
}
t = {
Pow: Pow,
Expo: Expo,
Circ: Circ,
Sine: Sine,
Back: Back,
Bounce: Bounce,
Elastic: Elastic
};
for (k in t) {
create(k, t[k]);
}
["Quad", "Cubic", "Quart", "Quint"].forEach(function (transition, i) {
create(transition, function (p) {
return pow(p, i + 2);
});
});
// tween function
function _def_render(obj, prop, value) {
obj[prop] = value;
}
function _def_parser(obj, prop) {
return parseFloat(obj[prop], 10);
}
function _def_factor(k0, k1, rfactor) {
return ((k1 - k0) * rfactor) + k0;
}
Transitions.LINK_CHAIN = CHAIN;
Transitions.LINK_STOP = STOP;
Transitions.LINK_IGNORE = IGNORE;
Transitions.LINK_CANCEL = CANCEL;
function _normalize(obj, input) {
//get all props
var keys = Object.keys(input).sort(function (a, b) { return parseFloat(a) - parseFloat(b); }),
i,
j,
prop,
key,
fkey,
prop_list = [],
props = {},
last;
for (i = 0; i < keys.length; ++i) {
prop_list = array.add(prop_list, Object.keys(input[keys[i]]));
}
prop_list = array.unique(prop_list);
for (j = 0; j < prop_list.length; ++j) {
prop = prop_list[j];
props[prop] = {};
for (i = 0; i < keys.length; ++i) {
key = keys[i];
fkey = parseFloat(keys[i]);
// first of the sorted list and is not 0%
// set current value
if (i === 0 && key !== "0%") {
props[prop][0] = obj[prop];
}
if (input[key][prop] !== undefined) {
props[prop][fkey] = last = input[key][prop];
}
}
// check that has 100% if not set the last known value
if (props[prop]["100"] === undefined) {
props[prop][100] = last;
}
}
return props;
}
/**
* Animate object properties.
*
* *obj* must be writable or at least have defined $__tween
* *prop* property name to animate
* *values* keys are numbers from 0 to 100, values could be anything
* *ioptions*
* **mandatory**
* * **time**: <number> in ms
*
* **optional**
* * **transition** Transition.XXX, or a valid compatible function Default: linear
* * **link** Transition.LINK_XXX Default: CHAIN
* * **render** function(obj, property, new_value) {}
* * **parser** function(obj, property) { return <value>; }
* * **tickEvent** <string> event name Default: "tick"
* * **endEvent** <string> event name Default: "animation:end"
* * **startEvent** <string> event name Default: "animation:star"
* * **chainEvent** <string> event name Default: "animation:chain"
*
* @param {Object} obj
* @param {String|Number} prop
* @param {Array|Object} values
* @param {Object} ioptions
*/
function animate(obj, prop, values, ioptions) {
// lazy init
obj.$__tween = obj.$__tween || {};
//console.log("options", JSON.stringify(options), JSON.stringify(values));
// <debug>
if ("function" !== typeof obj.on) {
throw new Error("obj must be an event-emitter");
}
if ("function" !== typeof obj.removeListener) {
throw new Error("obj must be an event-emitter");
}
if ("number" !== typeof ioptions.time) {
throw new Error("options.time is mandatory");
}
// </debug>
//soft clone and defaults
var options = {
render: ioptions.render || _def_render,
parser: ioptions.parser || _def_parser,
applyFactor: ioptions.applyFactor || _def_factor,
transition: ioptions.transition || Transitions.linear,
link: ioptions.link || CHAIN,
tickEvent: ioptions.tickEvent || "tick",
endEvent: ioptions.endEvent || "animation:end",
startEvent: ioptions.startEvent || "animation:start", // first emit
chainEvent: ioptions.chainEvent || "animation:chain",
time: ioptions.time,
start: Date.now(),
current: 0
},
chain_fn,
kvalues = Object.keys(values),
fvalues = kvalues.map(function (val) { return parseFloat(val) * 0.01; }),
update_fn;
//console.log("options", JSON.stringify(options), JSON.stringify(values));
update_fn = function (delta) {
//console.log(prop, "tween @", delta, options, values);
if (!delta) {
throw new Error("trace");
}
options.current += delta;
var factor = options.current / options.time,
tr_factor,
i,
found = false,
max = kvalues.length,
k0,
k1,
rfactor;
//clamp
if (factor > 1) { // end
factor = 1;
tr_factor = 1;
} else {
tr_factor = options.transition(factor);
}
for (i = 0; i < max && !found; ++i) {
k0 = fvalues[i];
if (k0 <= tr_factor) {
if (i === max - 1) {
// last element
found = true;
k0 = fvalues[i - 1];
k1 = fvalues[i];
} else {
k1 = fvalues[i + 1];
if (k1 > tr_factor) {
found = true;
}
}
if (found === true) {
//console.log(prop, "ko", k0, "k1", k1);
//console.log(prop, tr_factor);
if (tr_factor === 1) {
options.render(obj, prop, values["100"]);
// this is the end, my only friend, the end...
obj.removeListener(options.tickEvent, obj.$__tween[prop]);
delete obj.$__tween[prop];
obj.emit(options.endEvent, options);
} else {
rfactor = (tr_factor - k0) / (k1 - k0);
//console.log(prop, i, rfactor);
//console.log(prop, rfactor, "k0", values[k0], "k1", values[k1]);
options.render(obj, prop,
options.applyFactor(values[kvalues[i]], values[kvalues[i + 1]], rfactor)
);
}
}
}
}
};
if (obj.$__tween[prop]) {
// link will told us what to do!
switch (options.link) {
case IGNORE:
return IGNORE;
case CHAIN:
chain_fn = function () {
if (!obj.$__tween[prop]) {
obj.$__tween[prop] = update_fn;
obj.on(options.tickEvent, obj.$__tween[prop]);
obj.removeListener(options.endEvent, chain_fn);
}
};
obj.on(options.endEvent, chain_fn);
obj.emit(options.chainEvent, options);
return CHAIN;
case STOP:
obj.removeListener(options.tickEvent, obj.$__tween[prop]);
delete obj.$__tween[prop];
return STOP;
case CANCEL:
obj.removeListener(options.tickEvent, obj.$__tween[prop]);
delete obj.$__tween[prop];
// and attach!
obj.$__tween[prop] = update_fn;
obj.on(options.tickEvent, obj.$__tween[prop]);
break;
}
} else {
obj.$__tween[prop] = update_fn;
obj.on(options.tickEvent, obj.$__tween[prop]);
}
return true;
}
/**
* @param {Object} obj
* @param {Object} params
* @param {Object} options
*/
function tween(obj, params, options) {
// <debug>
if (!params.hasOwnProperty("100%")) {
throw new Error("100% params must exists");
}
if ("function" !== typeof obj.on) {
throw new Error("obj must be an event-emitter");
}
if ("function" !== typeof obj.removeListener) {
throw new Error("obj must be an event-emitter");
}
if ("number" !== typeof options.time) {
throw new Error("options.time is mandatory");
}
// </debug>
options = options || {};
// set defaults
options.render = options.render || _def_render;
options.parser = options.parser || _def_parser;
options.transition = options.transition || Transitions.linear;
options.link = options.link || CHAIN;
options.tick = options.tick || "tick";
// set config
obj.$__tween = obj.$__tween || {};
var plist = _normalize(obj, params),
i;
// animate each property
for (i in plist) {
Transitions.animate(obj, i, plist[i], options);
}
}
Transitions.tween = tween;
Transitions.animate = animate;
Transitions.linear = linear;
Transitions.create = create;
Transitions.createCubic = createCubic;
Transitions.createQuadric = createQuadric;
module.exports = Transitions;