Skip to content

Instantly share code, notes, and snippets.

@KrofDrakula
Created December 28, 2014 19:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KrofDrakula/6cae3ee68d1aaf478946 to your computer and use it in GitHub Desktop.
Save KrofDrakula/6cae3ee68d1aaf478946 to your computer and use it in GitHub Desktop.
CSS Animation Bézier curve generator
(function(global) {
function extend(obj) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var name in source) if (source.hasOwnProperty(name))
obj[name] = source[name];
}
return obj;
}
var EventEmitter = {
on : function(type, handler) {
this._ensureEvent(type);
this._events[type].push(handler);
},
off : function(type, handler) {
if (this._events && this._events[type]) {
this._events[type] = this._events[type].filter(function(h) {
return h !== handler;
});
}
},
emit: function(type) {
var args = Array.prototype.slice.call(arguments, 1);
this._ensureEvent(type);
this._events[type].forEach(function(handler) {
handler.apply(this, args);
});
},
_ensureEvent: function(type) {
if (!this._events) this._events = {};
if (!this._events[type]) this._events[type] = [];
}
};
function Vector2d(x, y) {
this.x = x;
this.y = y;
}
Object.defineProperty(Vector2d.prototype, 'length', {
get : function() { return Math.sqrt(this.x * this.x + this.y * this.y); },
enumerable : true
});
Vector2d.prototype.add = function(v) {
if (v instanceof this.constructor)
return new this.constructor(this.x + v.x, this.y + v.y);
else
return new this.constructor(this.x + v, this.y + v);
};
Vector2d.prototype.sub = function(v) {
if (v instanceof this.constructor)
return this.add(v.neg());
else
return this.add(-v);
};
Vector2d.prototype.neg = function() {
return new this.constructor(-this.x, -this.y);
};
Vector2d.prototype.scale = function(s) {
return new this.constructor(this.x * s, this.y * s);
};
Vector2d.prototype.dot = function(v) {
return this.x * v.x + this.y * v.y;
};
Vector2d.prototype.cross = function(v) {
return this.x * v.y - this.y * v.x;
};
Vector2d.prototype.towards = function(other) {
return other.sub(this).normalize();
};
Vector2d.prototype.normalize = function() {
return this.scale(1 / this.length);
};
Vector2d.random = function(unit) {
var r = Math.random() * Math.PI * 2,
d = unit ? 1 : Math.random();
return new this(
d * Math.cos(r),
d * Math.sin(r)
);
};
Vector2d.randomWithin = function(rectangle) {
return new this(
rectangle.corner.x + Math.random() * rectangle.width,
rectangle.corner.y + Math.random() * rectangle.height
);
};
Vector2d.prototype.rotate = function(r) {
return new this.constructor(
this.x * Math.cos(r) - this.y * Math.sin(r),
this.x * Math.sin(r) + this.y * Math.cos(r)
);
};
Vector2d.prototype.clone = function() {
return new this.constructor(this.x, this.y);
};
function BezierCurve(A, B, C, D) {
this._A = A;
this._B = B;
this._C = C;
this._D = D;
};
extend(BezierCurve.prototype, EventEmitter);
['A', 'B', 'C', 'D'].forEach(function(pt) {
Object.defineProperty(BezierCurve.prototype, pt, {
get: function() { return this['_' + pt]; },
set: function(v) {
this['_' + pt] = v;
this.emit('change', pt);
}
});
});
BezierCurve.prototype.interpolate = function(t) {
var tt = t * t,
ttt = tt * t,
u = 1 - t,
uu = u * u,
uuu = uu * u;
return this.A.scale(uuu).
add(this.B.scale(3 * uu * t)).
add(this.C.scale(3 * u * tt)).
add(this.D.scale(ttt));
};
BezierCurve.prototype.direction = function(t) {
var tt = t * t,
g = (t - 1) * (t - 1),
h = -3 * tt + 4 * t - 1,
i = 3 * tt - 2 * t,
j = - tt;
return this.A.scale(g).
add(this.B.scale(h)).
add(this.C.scale(i)).
add(this.D.scale(j)).
scale(-3);
};
function AnimationGenerator(options) {
this.options = extend({}, this.constructor.defaults, options || {});
}
AnimationGenerator.defaults = {
segments : 128,
orientAlongPath : false,
maxError : 1,
name : 'CUSTOM_ANIMATION'
};
AnimationGenerator.prototype.generate = function(bezier) {
return this.generateAnimation(this.generatePointList(bezier));
};
AnimationGenerator.prototype.generatePointList = function(bezier) {
var increment = 1 / this.options.segments,
points = [],
i, p, t, d, pa, n, minD, minError = 0, idx;
for (var i = 0; i <= this.options.segments; i++) {
var t = i * increment,
p = bezier.interpolate(t);
p.t = t;
p.dir = bezier.direction(t);
p.angle = -Math.atan2(p.dir.x, p.dir.y) / Math.PI * 180;
points.push(p);
}
do {
if (points.length == 2) break;
idx = null;
minError = Infinity;
for (i = 1; i < points.length - 1; i++) {
n = points[i-1].towards(points[i+1]);
pa = points[i-1].sub(points[i]);
d = pa.sub(n.scale(pa.dot(n))).length;
if (d < minError && d < this.options.maxError) {
minError = d;
idx = i;
}
}
if (idx != null) points.splice(idx, 1);
} while (minError <= this.options.maxError)
return points;
}
AnimationGenerator.prototype.generateAnimation = function(pointList) {
var i, rollingSum = 0, total = 0,
animation = ['@-webkit-keyframes ' + this.options.name + ' {'];
pointList[0].l = 0;
for (i = 1; i < pointList.length; i++) {
total += pointList[i].l = pointList[i].sub(pointList[i-1]).length;
}
for (i = 0; i < pointList.length; i++) {
rollingSum += pointList[i].l;
animation.push(this._generateKeyframe(rollingSum / total, pointList[i]));
}
animation.push('}');
return animation.join('\n');
};
AnimationGenerator.prototype._generateKeyframe = function(position, point) {
var extras = '';
if (this.options.orientAlongPath) {
extras = 'rotate(' + point.angle.toFixed(1) + 'deg)';
}
return ' ' + (position * 100).toFixed(4) + '% { -webkit-transform: translate(' + (point.x.toFixed(1)) + 'px, ' + (point.y.toFixed(1)) + 'px) ' + extras + '; }';
};
// exports
global.Vector2d = Vector2d;
global.BezierCurve = BezierCurve;
global.AnimationGenerator = AnimationGenerator;
})((module && module.exports) ? module.exports : window);
var curves = require('./curves');
var b = new curves.BezierCurve(
new curves.Vector2d(10, 10),
new curves.Vector2d(40, 10),
new curves.Vector2d(10, 40),
new curves.Vector2d(40, 40)
);
var g = new curves.AnimationGenerator;
console.log(g.generate(b));
@KrofDrakula
Copy link
Author

Run node test.js to see the generated animation stylesheet.

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