Search code examples
cssuibezierpathbezier

Animating a Custom Bezier Curve based on Progress of certain event


How can we animate a closed parabolic bezier curve based on the progress of a certain event?

More Context ::

  1. Platform: HTML5 Canvas
  2. Curve constructed with the help of 2 parabolas intersecting each other ( When the progress is 100% )
  3. But when the progress is let's say 10%, how we can create a bezier curve for this situation?

PS: There are other SO questions which show how to build a circular progress bar but what I'm looking at is a parabolic progress bar.


Solution

  • You can fake filling any SVG curve in canvas with a bit of a mix of SVG and Canvas to calculate some things:

    const path = document.querySelector( '#computing-path' );
    const canvas = document.querySelector( 'canvas' );
    const progress = document.querySelector( '#progress' );
    const bezierPoints = [
      [50, 50],
      [0, 30],
      [20, 180],
      [180, 150]
    ];
    const ctx = canvas.getContext( '2d' );
    
    /* We need to set the SVG path to compute its length */
    path.setAttribute( 'd', `M${bezierPoints[0].join()} C ${bezierPoints[1].join()} ${bezierPoints[2].join()} ${bezierPoints[3].join()}` );
    
    const pathLength = path.getTotalLength();
        
    /* `p` will be a normalised value between 0 and 1 */
    function draw( p = 0 ){
        
        ctx.clearRect( 0, 0, canvas.width, canvas.height );
        
        if( p <= 0 ) return;
        
        ctx.save();
        ctx.lineWidth = 10;
        ctx.strokeStyle = 'white';
        
        /** This is the crucial bit - the second value is very large because we want the empty space of the dash to stretch to at least the end. */
        ctx.setLineDash([ p * pathLength, pathLength * 10 ]);
        
        ctx.beginPath();
        ctx.moveTo( ...bezierPoints[0] );
        ctx.bezierCurveTo(
          ...bezierPoints[1],
          ...bezierPoints[2],
          ...bezierPoints[3]
        );
        ctx.stroke();
        ctx.restore();
      
    }
    
    draw( progress.value );
    
    progress.addEventListener( 'input', event => draw( progress.value ));
    html, body { height: 100%; }
    body { background: gray; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px;  }
    canvas { border: 1px solid white; }
    <svg xmlns="http://www.w3.org/2000/svg" style="visibility:hidden;position:fixed;top:0;left:0;transform:translate(-100%,-100%)"><path id="computing-path" /></svg>
    <canvas width="200" height="200"></canvas>
    <input id="progress" type="range" min="0" max="1" value=".5" step=".01" />