Skip to content

Instantly share code, notes, and snippets.

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

Animate Along Path IV

This example shows how you can create a Bezier spline and animate an object along its path. The speed is related to the curvature, so the arrow has to slow down as the path becomes more curved. It uses my small Bezier library.

It uses d3.timer instead of trying to precompute the speeds and easing to make the trasition work properly (error prone as the previous example shows). This is a little more straight-forward and works a lot more reliably.

(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)), 50);
}
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 t = 0,
v = 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))
d3.timer(function(elapsed) {
t = (t + maxSpeed(spline.curvature(t)) / spline.length / 20) % 1;
v.attr('transform', spline.pointTransform(t));
});
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment