Skip to content

Instantly share code, notes, and snippets.

@omnizach
Last active February 29, 2016 20:59
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save omnizach/cbddbec7a8ca22aca587 to your computer and use it in GitHub Desktop.
Animate Along Path III

Animate Along Path III

This example shows how you can create a Bezier spline and animate an object along its path. It uses my small Bezier library.

Unlike previous examples, this example is a little tougher to accomplish.

First, the orientation of the arrow along the curve uses the tangent function to calculate the direction of the curve. Second, the color and speed are governed by curvature. This is a rudimentary version of how a car would drive along the curve, needing to slow down based on the sharpness of the turn.

(function(){var t,n,r,e,i,h,o,p,s=function(t,n){return(+t%(n=+n)+n)%n},u=[].slice;i=function(){var t,n,r,e,i,h,o;for(i=function(){var n,r,e;for(e=[],n=0,r=arguments.length;r>n;n++)t=arguments[n],e.push(t.length);return e}.apply(this,arguments),e=Math.min.apply(Math,i),o=[],n=r=0,h=e;h>=0?h>r:r>h;n=h>=0?++r:--r)o.push(function(){var r,e,i;for(i=[],r=0,e=arguments.length;e>r;r++)t=arguments[r],i.push(t[n]);return i}.apply(this,arguments));return o},Function.prototype._getter=function(t,n){return Object.defineProperty(this.prototype,t,{get:n,configurable:!0})},t=1-1e-6,r=function(){function t(n,r){return this instanceof t?(this.x=n,void(this.y=r)):new t(n,r)}return t}(),n=function(){function t(n,r,e,i){return this instanceof t?(this.p0=n,this.p1=r,this.p2=e,void(this.p3=i)):new t(n,r,e,i)}return t.penPath=function(t){return"M "+t.p0.x+", "+t.p0.y+" C "+t.p1.x+", "+t.p1.y+" "+t.p2.x+", "+t.p2.y+" "+t.p3.x+", "+t.p3.y},t.paintPath=function(t){var n;return n=t/2,function(t){var r,e;return r=t.normal(0),e=t.normal(1),"M "+(t.p0.x-r.x*n)+", "+(t.p0.y-r.y*n)+" L "+(t.p0.x+r.x*n)+", "+(t.p0.y+r.y*n)+" L "+(t.p3.x+e.x*n)+", "+(t.p3.y+e.y*n)+" L "+(t.p3.x-e.x*n)+", "+(t.p3.y-e.y*n)+" Z"}},t.prototype._findT=function(t,n){var r;return t=Math.min(t,this.length),n=n||t/this.length,r=(this.lengthAt(n)-t)/this.length,Math.abs(r)<1e-4?n:this._findT(t,n-r/2)},t._base3=function(t,n,r,e,i){var h,o;return h=-3*n+9*r-9*e+3*i,o=t*h+6*n-12*r+6*e,t*o-3*n+3*r},t.prototype.lengthAt=function(n){var r,e,i;return null==n&&(n=1),n=n>1?1:0>n?0:n,i=n/2,e=function(n,r){var e;return e=i*n[0]+i,n[1]*Math.sqrt(Math.pow(t._base3(e,r.p0.x,r.p1.x,r.p2.x,r.p3.x),2)+Math.pow(t._base3(e,r.p0.y,r.p1.y,r.p2.y,r.p3.y),2))},i*function(){var t,n,i,h;for(i=[[-.1252,.2491],[.1252,.2491],[-.3678,.2335],[.3678,.2335],[-.5873,.2032],[.5873,.2032],[-.7699,.1601],[.7699,.1601],[-.9041,.1069],[.9041,.1069],[-.9816,.0472],[.9816,.0472]],h=[],t=0,n=i.length;n>t;t++)r=i[t],h.push(e(r,this));return h}.call(this).reduce(function(t,n){return t+n})},t._getter("length",function(){return null!=this._length?this._length:this._length=this.lengthAt(1)}),t.prototype.x=function(t){return null==t&&(t=0),Math.pow(1-t,3)*this.p0.x+3*Math.pow(1-t,2)*t*this.p1.x+3*(1-t)*Math.pow(t,2)*this.p2.x+Math.pow(t,3)*this.p3.x},t.prototype.y=function(t){return null==t&&(t=0),Math.pow(1-t,3)*this.p0.y+3*Math.pow(1-t,2)*t*this.p1.y+3*(1-t)*Math.pow(t,2)*this.p2.y+Math.pow(t,3)*this.p3.y},t.prototype.point=function(t){return null==t&&(t=0),r(this.x(t),this.y(t))},t.prototype.pointAtLength=function(t){return null==t&&(t=0),this.point(this._findT(t))},t.prototype.firstDerivative=function(t){return null==t&&(t=0),new r(3*Math.pow(1-t,2)*(this.p1.x-this.p0.x)+6*(1-t)*t*(this.p2.x-this.p1.x)+3*Math.pow(t,2)*(this.p3.x-this.p2.x),3*Math.pow(1-t,2)*(this.p1.y-this.p0.y)+6*(1-t)*t*(this.p2.y-this.p1.y)+3*Math.pow(t,2)*(this.p3.y-this.p2.y))},t.prototype.secondDerivative=function(t){return null==t&&(t=0),new r(6*(1-t)*(this.p2.x-2*this.p1.x+this.p0.x)+6*t*(this.p3.x-2*this.p2.x+this.p2.x),6*(1-t)*(this.p2.y-2*this.p1.y+this.p0.y)+6*t*(this.p3.y-2*this.p2.y+this.p2.y))},t.prototype.curvature=function(t){var n,r;return null==t&&(t=0),n=this.firstDerivative(t),r=this.secondDerivative(t),(n.x*r.y-n.y*r.x)/Math.pow(Math.pow(n.x,2)+Math.pow(n.y,2),1.5)},t.prototype.tangent=function(t){var n,e;return null==t&&(t=0),e=this.firstDerivative(t),n=Math.sqrt(e.x*e.x+e.y*e.y)||1,new r(e.x/n,e.y/n)},t.prototype.normal=function(t){var n;return null==t&&(t=0),n=this.tangent(t),new r(-n.y,n.x)},t.prototype.pointTransform=function(t){var n,r,e,i;return null==t&&(t=0),n=[this.x(t),this.y(t)],e=n[0],i=n[1],r=this.tangent(t),"translate("+e+", "+i+") rotate("+180*Math.atan2(r.y,r.x)/Math.PI+")"},t}(),e=function(){function e(t,n){var r;return null==n&&(n=!1),this instanceof e?(this.closed=n,this.curves=e.computeSpline(t.map(function(t){return t.x}),t.map(function(t){return t.y}),n),this.startLengths=function(){var t,n,e,i;for(e=this.curves,i=[],t=0,n=e.length;n>t;t++)r=e[t],i.push(r.startLength);return i}.call(this),this.endLengths=function(){var t,n,e,i;for(e=this.curves,i=[],t=0,n=e.length;n>t;t++)r=e[t],i.push(r.endLength);return i}.call(this),void(this.length=this.endLengths.slice(-1)[0])):new e(t,n)}return e.computeControlPoints=function(t){var n,r,e,i,h,o,p,s,u,a,l,c,f,y,v;for(n=function(t){return 0>=t?0:t>=s-1?2:1},e=function(t){return t>=s-1?0:1},s=t.length-1,a=new Array(s-1),l=new Array(s-1),r=function(){var t,n,r;for(r=[],i=t=0,n=s;n>=0?n>t:t>n;i=n>=0?++t:--t)r.push(4);return r}(),r[0]=2,r[s-1]=7,c=function(){var n,r,e;for(e=[],i=n=0,r=s-1;r>=0?r>n:n>r;i=r>=0?++n:--n)e.push(4*t[i]+2*t[i+1]);return e}(),c[0]=t[0]+2*t[1],c.push(8*t[s-1]+t[s]),i=h=0,f=r.length-1;f>=0?f>h:h>f;i=f>=0?++h:--h)p=n(i+1)/r[i],r[i+1]-=p*e(i),c[i+1]-=p*c[i];for(a[s-1]=c[s-1]/r[s-1],i=o=y=r.length-2;0>=y?0>=o:o>=0;i=0>=y?++o:--o)a[i]=(c[i]-e(i)*a[i+1])/r[i];for(i=u=0,v=r.length-1;v>=0?v>u:u>v;i=v>=0?++u:--u)l[i]=2*t[i+1]-a[i+1];return l[s-1]=.5*(t[s]+a[s-1]),{p1:a,p2:l}},e.computeSpline=function(t,h,o){var p,a,l,c,f,y,v,g,x,m,w,M,d,_,L,C,b,A,D,P,T;for(c=12,o&&(y=function(t){var n,r,e,i;for(i=[],n=r=0,e=c;e>=0?e>r:r>e;n=e>=0?++r:--r)i.push(t[s(n,t.length)]);return i},f=function(t){var n,r,e,i;for(i=[],n=r=e=c;0>=e?0>r:r>0;n=0>=e?++r:--r)i.push(t[s(t.length-n,t.length)]);return i},t=u.call(f(t)).concat(u.call(t),u.call(y(t))),h=u.call(f(h)).concat(u.call(h),u.call(y(h)))),a=e.computeControlPoints(t),l=e.computeControlPoints(h),T=0,o&&(t=t.slice(c,+-c+1||9e9),h=h.slice(c,+-c+1||9e9),a.p1=a.p1.slice(c,+-c+1||9e9),l.p1=l.p1.slice(c,+-c+1||9e9),a.p2=a.p2.slice(c,+-c+1||9e9),l.p2=l.p2.slice(c,+-c+1||9e9)),A=i(t.slice(0,-1),h.slice(0,-1),a.p1,l.p1,a.p2,l.p2,t.slice(1),h.slice(1)),P=[],v=g=0,x=A.length;x>g;v=++g)D=A[v],m=D[0],w=D[1],M=D[2],d=D[3],_=D[4],L=D[5],C=D[6],b=D[7],p=new n(new r(m,w),new r(M,d),new r(_,L),new r(C,b)),p.startLength=T,p.endLength=T+p.length,p.segmentOffset=v/(t.length-1),p.index=v,T+=p.length,P.push(p);return P},e._marshalCurve=function(n){return function(r){var e;return r=(0>r?0:r>t?t:r)*this.curves.length||0,e=Math.trunc(r),this.curves[e][n](r-e)}},e.prototype.x=e._marshalCurve("x"),e.prototype.y=e._marshalCurve("y"),e.prototype.point=e._marshalCurve("point"),e.prototype.pointAtLength=function(t){var n,r;return null==t&&(t=0),n=function(t,r,e,i){var h;switch(h=e+i>>>1,!1){case!((t[h-1]||0)<=r&&r<=t[h]):return h;case!(r<(t[h-1]||0)):return n(t,r,e,h);default:return n(t,r,h+1,i)}},r=n(this.endLengths,Math.min(t,this.endLengths[this.endLengths.length-1]),0,this.endLengths.length),this.curves[r].pointAtLength(t-this.startLengths[r])},e.prototype.firstDerivative=e._marshalCurve("firstDerivative"),e.prototype.secondDerivative=e._marshalCurve("secondDerivative"),e.prototype.curvature=e._marshalCurve("curvature"),e.prototype.tangent=e._marshalCurve("tangent"),e.prototype.normal=e._marshalCurve("normal"),e.prototype.pointTransform=e._marshalCurve("pointTransform"),e.prototype.normalize=function(t,n){var r,i;return null==t&&(t=this.length/n||1),null==n&&(n=Math.ceil(this.length/t)),i=function(){var e,i,h;for(h=[],r=e=0,i=n;i>=0?i>e:e>i;r=i>=0?++e:--e)h.push(this.pointAtLength(r*t));return h}.call(this),e(i,this.closed)},e}(),o=function(t){return t.x.bind(t)},p=function(t){return t.y.bind(t)},h=function(t){return t.pointTransform.bind(t)},this.bezier={Point:r,Curve:n,Spline:e,interpolateX:o,interpolateY:p,interpolateTransform:h}}).call(this);
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.bezier-curve {
stroke-width: 5px;
fill: none;
}
.vehicle {
fill: #c00;
stroke: #600;
stroke-width: 2px;
}
.label rect {
fill: #fff;
stroke: #000;
stroke-width: 2px;
}
.label text {
font-family: Verdana;
font-size: 10pt;
text-anchor: middle;
alignment-baseline: middle;
}
.label .tick {
stroke: #000;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="bezier.min.js"></script>
<script>
var formatLabel = d3.format('.2f');
var points = [
[250, 300],
[150, 280],
[155, 250],
[150, 220],
[250, 200],
[650, 200],
[750, 220],
[745, 250],
[750, 280],
[650, 300]
];
var width = 960,
height = 500;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
function maxSpeed(k) {
return Math.min(Math.sqrt(Math.abs(1 / k)), 30);
}
var spline = bezier.Spline(points.map(function(p) { return bezier.Point(p[0], p[1]); }), true)
.normalize();
var curvatureRange = d3.extent(d3.range(0, 1, 1/spline.length).map(function(d) {
return maxSpeed(spline.curvature(d));
})),
color = d3.scale.linear()
.domain([0, 30])
.range(['#f00', '#090'])
.interpolate(d3.interpolateHsl);
svg.append('g')
.selectAll('.bezier-curve')
.data(spline.curves)
.enter()
.append('path')
.classed('bezier-curve', true)
.attr('d', bezier.Curve.penPath)
.style('stroke', function(d) {
var x = maxSpeed(d.curvature());
return color(x);
});
var labels = svg.append('g')
.selectAll('.label')
.data(d3.range(0, 1, 0.05))
.enter()
.append('g')
.classed('label', true);
labels.append('rect')
.attr('x', function(d) { return spline.x(d)-30 - spline.normal(d).x*40; })
.attr('y', function(d) { return spline.y(d)-10 - spline.normal(d).y*20; })
.attr('width', 60)
.attr('height', 20);
labels.append('path')
.classed('tick', true)
.attr('d', function(d) {
var n = spline.normal(d),
p = spline.point(d);
return 'M' + (p.x - n.x*5) + ',' + (p.y - n.y*5) +
'L' + (p.x + n.x*5) + ',' + (p.y + n.y*5);
});
labels.append('text')
.attr('x', function(d) { return spline.x(d) - spline.normal(d).x*40; })
.attr('y', function(d) { return spline.y(d) - spline.normal(d).y*20; })
.text(function(d, i) { return 't = ' + formatLabel(d); });
var positions = d3.range(0, 1, 0.001).reduce(function(p, c, i) {
return p.concat(p[i] + maxSpeed(spline.curvature(c)));
}, [0]).map(function(d, i, a) {
return d / a[a.length-1];
});
function ease(t) {
t = t * (positions.length-1);
var i = Math.trunc(t),
d = t-i,
a = positions[i],
b = positions[i+1] || 1;
return (1-d) * a + d * b;
}
svg.append('g')
.append('path')
.datum(spline)
.classed('vehicle', true)
.attr('d', 'M0,0 l-20,10 l5,-10 l-5,-10 z')
.attr('transform', spline.pointTransform(0))
.transition()
.delay(1000)
.ease('linear')
.duration(10000)
.each(lap);
function lap() {
var c = d3.select(this);
(function repeat() {
c = c.transition()
.attrTween('transform', bezier.interpolateTransform)
.ease(ease)
.each('end', repeat);
})();
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment