Search code examples
javascriptnpmanimationcanvaspaperjs

Best way to have sequential Canvas Animations In Javascript


So I'm attempting to animate a canvas element(Or PaperJs Path/Text in this case) along a path using PaperJs(PaperJs is how the path and element are made aswell). The way this works is I have a array of user created frames(When I say frame I mean this User Created Frame from here forward) which have 0-10 paths drawn by the user in them. From here I create a number at a position and store the reference to this canvas element in an array called text and the corresponding path in an array called paths. For each frame the code I attached is ran with its list of paths and text. My code works but is very janky and had limitations in the features I want to add. The issue is I want the ability to call a function animate_frame(OBJ_WITH_PATHS[0]) and have it animate the frame and then once its finished animating the first frame call animate_frame(OBJ_WITH_PATHS[1]) so the entire list of OBJ with the paths gets animated in sequential order. This happens currently because of the line

if ((event.time > frame_num*2) && (event.time < ((frame_num*2) + 2))) {.

The reason this line is required is the program makes all of the text and paths for every frame with an onFrame event and without strictly limiting the time that the object is visible and animating all frames go at the same time simultaneously instead of by frame(my frame that has paths). This behavior is why I strictly limit the total time of each frame to 2 seconds. I should also note the reason I change the fill color to black and then transparent are so the elements are only visible while there animating not before or after there 2 second animation period. The relevant code I'm currently using is shown below

  var offset = [];

  for(let i = 0; i < paths.length; i ++) {
    offset[i] = 0;
  }
  var time_sec = 2;   
  for(let i = 0; i < paths.length; i ++) {
      text[i].onFrame = function (event) {
          var speed = paths[i].length/time_sec;
          if ((event.time > frame_num*2) && (event.time < ((frame_num*2) + 2))) {
              text[i].fillColor = "black";          
              if (offset[i] < paths[i].length){
                  text[i].position = paths[i].getPointAt(offset[i]);
                  offset[i] += event.delta*speed; // speed - 150px/second
               } else {
                  offset[i] = paths[i].length;
               }
          } else {
              text[i].fillColor = new paper.Color(0.5,0);
          }
      paper.view.draw();
    }
  }

My code limits me in the features I want to add such as

  • Pause
  • Skip to next Frame/back a frame
  • Play frame (then pause until they hit play again then play a single frame)
  • Variable speed (Ie one frame takes 2 seconds the second takes 4 seconds)

I also want to touch on I've tried implementing waits for 2 seconds between calling animate_frame but the onFrame(event) From PaperJS does not happen during this wait period so It does not work), I also tried a flag var and a while loop but that did not work either.

So I'm wondering if there is a better way to do this. I would love something like I create an animation object where that object contains the paths and canvas elements to be moved/animated and the animation could be called by like animation.play(); which then also has the ability to wait for a previous animation to finish so I could have an array of animated frames. For the entire Project at this point I am married to PaperJs but I could get the path position data into a regular JS array and pass that into a new package, I could also animate elements on top of the canvas if there is a package that would fit what I'm trying to do. Thank you for you assistance/Ideas!


Solution

  • For complex animation scenarios, I would advice you to use a proper animation library which will make the whole thing easier.
    For example, GSAP could be a great help for all the play/pause... handling.
    Here's a simple fiddle demonstrating how it could be used to control a Paper.js based animation.

    You should be able to adapt it to your specific use case.

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Debug Paper.js</title>
        <script src="https://unpkg.com/acorn"></script>
        <script src="https://unpkg.com/paper"></script>
        <script src="https://unpkg.com/gsap"></script>
        <style>
            html,
            body {
                margin   : 0;
                overflow : hidden;
                height   : 100%;
            }
    
            canvas[resize] {
                width  : 100%;
                height : 100%;
            }
    
            #buttons {
                position : fixed;
                top      : 50px;
                left     : 50px;
                display  : flex;
                gap      : 15px;
            }
        </style>
    </head>
    <body>
    <div id="buttons">
        <button id="play">play</button>
        <button id="pause">pause</button>
        <button id="reverse">reverse</button>
    </div>
    <canvas id="canvas" resize></canvas>
    <script>
        paper.setup('canvas');
    
        const initialPosition = paper.view.center;
    
        const circle = new paper.Path.Circle({
            center: initialPosition,
            radius: 50,
            fillColor: 'orange'
        });
    
        function update(t) {
            circle.position = initialPosition.add([t * 100, 0]);
        }
    
        const obj = { value: 0 };
        var timeLine = gsap.timeline({
            onUpdate: (event) => {
                update(obj.value);
            }
        });
        timeLine.to(obj, { value: 1, duration: 1 });
        timeLine.pause();
    
        document.querySelector('#play').addEventListener('click', () => timeLine.play());
        document.querySelector('#pause').addEventListener('click', () => timeLine.pause());
        document.querySelector('#reverse').addEventListener('click', () => timeLine.reverse());
    </script>
    </body>
    </html>