4

I was given a design today that is a circle moving along a curved line. I created a JSBin with the progress I have made so far with pure css but I feel I'm on the wrong direction. I think maybe this would be better done with canvas but I'm not sure where to begin. This is not just drawing along a line its also filling the bars.

Fiddle

Here is the design:

enter image description here

Here is how close I have got so far with CSS:

enter image description here

Community
  • 1
  • 1
zmanc
  • 5,201
  • 12
  • 45
  • 90
  • 1
    someone please reopen this so I can add bounty and reward @markE – zmanc Apr 16 '16 at 02:20
  • Still duplicate of **[this answer](http://stackoverflow.com/questions/17083580/i-want-to-do-animation-of-an-object-along-a-particular-path)** no? The bounty could have been awarded to the original answer fyi. –  Apr 16 '16 at 11:32
  • 1
    It has some similarities but the fill path is also very challenging for me. Having that part made the answer complete. – zmanc Apr 16 '16 at 13:41

2 Answers2

10

Here's how to animate your circle along your curved line (which is a Cubic Bezier Curve).

  • Draw your curve using canvas's context.bezierCurveTo method.

  • Close your rainbow path using a series of canvas's context.lineTo method.

  • To fill only the curved path with your rainbow colors, you can use context.clip to restrict drawings to display only inside the path. Then you can use context.fillRect to fill with your multi-colored bands.

  • Use requestAnimationFrame to create an animation loop that draws your ball at increasing waypoints along your curve.

  • Calculate waypoints along your curve using De Casteljau's Algorithm

Here's example code and a Demo:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
   
var colors=[[229,133,50],[251,183,50],[133,206,63],[22,155,116],[26,160,219]];
var points=[35,120,317,511,709,792];
var p0={x:37,y:144};
var p1={x:267,y:143};
var p2={x:651,y:129};
var p3={x:794,y:96};
var waypoints=cubicBezierPoints(p0,p1,p2,p3);
var currentIndex=0;
var radius=10;
//
requestAnimationFrame(animate);

// draw the rainbow curve thing
function drawCurve(){
    ctx.save();
    ctx.moveTo(37,144);
    ctx.bezierCurveTo(267,143,651,129,794,96);
    ctx.lineTo(794,158);
    ctx.lineTo(37,158);
    ctx.closePath();
    ctx.fill(); 
    ctx.globalCompositeOperation='source-atop';
    for(var i=0;i<points.length-1;i++){
        var c=colors[i];
        ctx.fillStyle='rgb('+c[0]+','+c[1]+','+c[2]+')';
        ctx.fillRect(points[i],0,points[i+1],ch);
    }
    ctx.restore();    
}
//
function drawBall(){
    var pt=waypoints[currentIndex];
    ctx.beginPath();
    ctx.arc(pt.x,pt.y,radius,0,Math.PI*2);
    ctx.fillStyle='white';
    ctx.fill();
    ctx.strokeStyle='black'
    ctx.lineWidth=3;
    ctx.stroke();
}

// the animation loop
function animate(){
    ctx.clearRect(0,0,cw,ch);
    drawCurve();
    drawBall();
    ctx.beginPath();
    currentIndex++;
    if(currentIndex<waypoints.length){
        requestAnimationFrame(animate);
    }
}

// calculate the waypoints
function cubicBezierPoints(p0,p1,p2,p3){
    var ticksPerSecond=60;
    var seconds=4;
    var totalTicks=ticksPerSecond*seconds;
    var pts=[];
    for(var t=0;t<totalTicks;t++){
        pts.push(getCubicBezierXYatT(p0,p1,p2,p3,t/totalTicks));
    }
    return(pts);
}

// De Casteljau's algorithm which calculates points along a cubic Bezier curve
// plot a point at interval T along a bezier curve
// T==0.00 at beginning of curve. T==1.00 at ending of curve
// Calculating 100 T's between 0-1 will usually define the curve sufficiently
function getCubicBezierXYatT(startPt,controlPt1,controlPt2,endPt,T){
    var x=CubicN(T,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
    var y=CubicN(T,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
    return({x:x,y:y});
}
// cubic helper formula at T distance
function CubicN(T, a,b,c,d) {
    var t2 = T * T;
    var t3 = t2 * T;
    return a + (-a * 3 + T * (3 * a - a * T)) * T
    + (3 * b + T * (-6 * b + b * 3 * T)) * T
    + (c * 3 - c * 3 * T) * t2
    + d * t3;
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=820 height=200></canvas>
markE
  • 102,905
  • 11
  • 164
  • 176
  • 2
    Tested and verified. However I am not going to accept it for a few days because I want to add bounty and give it to you. This is the single best answer I have ever received. Not only was it through but well explained. Thank you very much. – zmanc Apr 15 '16 at 04:36
  • Now, what are the odds for two people being from the same place [Atlanta](https://web.archive.org/web/20131226051535/http://stackoverflow.com/users/411591/marke), working in the same industry (accounting/ADP) and planning bounty for a answer which has been answered [many](https://stackoverflow.com/search?tab=votes&q=user%3a411591%20bezier%20along%20path) times before? :) – torox Apr 15 '16 at 05:42
  • @torox. No relation...really! I lived in Atlanta for 7 years until our kids went off to College and Vocational School respectively. Now we are back in sunny Central Florida. :-) Cheers! – markE Apr 15 '16 at 06:14
  • As much as it seems similar the fill beneath the curve also adds complexity. And I have never met mark, also I have not worked for adp for 2 weeks now. I'm at careerbuilder now. Need to update my profile. – zmanc Apr 15 '16 at 09:55
  • How did you get the curve to match so perfectly? Is there a tool that helps to do the plotting? Also the line is a little jagged how do I increase the resolution so it's smoother? – zmanc Apr 15 '16 at 21:28
  • @zmanc. I do have a small tool that lets me import an image, draw cubic Bezier curves and view the controls points for those curves. But with a little practice you can use `context.bezierCurveTo` with the outside control points on both endpoints of the curve and "eyeball" the inside control points. And yes, `context.clip` does leave a bit of jagginess, so I've edited my answer to (1) remove the red accent line and (2) use compositing instead of clipping to reduce the jagginess. Good luck with your project! :-) – markE Apr 15 '16 at 23:20
  • Should you have control over where the ball stops? Or do you only want it to animate once, from beginning to end? – Gust van de Wal Apr 23 '16 at 11:25
  • You can control by setting the stoppoint in the animation block. @markE do you have a github or twitter. Want to reach out to you for some tweaks. – zmanc Apr 24 '16 at 18:24
  • @zmanc. You can reach me at marksStackoverflowAddress@gmail.com. I can tweak as my "day job" permits :-)) – markE Apr 24 '16 at 18:29
4

Great answer by MarkE (and he deserves the bounty) but when I saw the De Casteljau's algorithm and had a close look it struck me as a mathematician writing software, not a programmer doing math.

Using the passed arguments as intermediates in the calculation there are a few operations that can be dropped thus improving the algorithm. It is the same math function diverging by no more than +/- 1e-14 (which in Javascript floating point is as close as it gets)

For want of a better name cubicQ

function cubicQ(t, a, b, c, d) {
    a += (b - a) * t;
    b += (c - b) * t;
    c += (d - c) * t;
    a += (b - a) * t;
    b += (c - b) * t;
    return a + (b - a) * t; 
}

And incase there is a need for the second order polynomials required by the ctx.quadraticCurveTo

function quadQ(t, a, b, c){
    a += (b - a) * t;
    b += (c - b) * t;
    return a + (b - a) * t;
}

Where a,b,c are the x or y points on the curve with b the control point. t is the position 0 <= t <= 1

And just for the interest the linear version

function linearQ(t, a, b){
    return a + (b - a) * t;
}

As you can see it is just a line. The quadratic comprised of linear interpolations 3 (lines), and the cubic is 6.

For this question the 15% increase in performance is trivial and inconsequential, but for more intensive need 15% is well worth the few extra lines of code, not to mention that it just looks better.. LOL

Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • @Blindman67. LOL! You're right! I'm an accountant in my "day job" so yes, I tend to (overly?) focus on precision -- people with $107.35 frown when a bank reports they have "about" a hundred dollars in their account! I like your faster approximation of the traditional Cubic & Quadratic intervals. Do you mind if I add your code to my "toolkit"? IMHO, canvas should implement something similar to SVG's `getPointAtLength` so we don't have to shim that functionality back in. Cheers! – markE Apr 25 '16 at 17:40
  • 1
    @markE I would not post if I did not want everyone to take what they need. What gets me is all the ongoing effort put into defining SVG, with W3C drafting SVG2 and nothing for canvas. Canvas is everywhere, it killed flash, almost all advertising is canvas based. You really have to hunt for SVG and when you see it, only static images, it really is a waste of resources. Canvas so badly needs a few minor tweaks to really unlock the GPU, and some native functions ie `getPointAtLength`, but W3C says, all done! Why must they flog that dead horse (SVG) when the thoroughbred is just warming up. – Blindman67 Apr 25 '16 at 18:56
  • 1
    @Blindman67. Yep, I agree that SVG is getting most of the W3C attention. Too bad since canvas is a great option ... I think of it as Flash + Photoshop -- now that's a **great combo**! And(!), when Path2D is cross-browser compatible, we can take all those SVG data-paths and draw them directly onto canvas. Having said that (and being an accountant), D3 (based on SVG) is an amazing data visualization tool that I couldn't do without. :-) – markE Apr 25 '16 at 19:02
  • 1
    @markE I come from the other direction, games, simulations, and have stayed away from business apps for years now. Easy to forget the business world's data driven needs. – Blindman67 Apr 25 '16 at 20:11