3

I want to do an animation of a reaction-diffusion system with paper.js.

Here is a code which generates just one image:

<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.2/paper-full.min.js"></script>
    <style>
      canvas {
        width: 400px;
        height: 400px;
        border: black 3px solid;
      }
    </style>
  </head>
  <body>
    <script>
      function subm_index(M, x){
        if (x<0)
          return x+M;
        if(x >= M)
          return x-M;
        return x;
      }
      function update_concentrations(X, L, DA, DB, f, k){ 
        var sum_a, sum_b, x1, y1, t;
        var m = X.A.length;
        var n = X.A[0].length;
        var A = new Array(m);
        var B = new Array(m);
        for(var i = 0; i < m; i++){
          A[i] = new Array(n);
          B[i] = new Array(n);
        }        
        for(var x = 0; x < m; x++) {
          for(var y = 0; y < n; y++){
            sum_a = 0.0;
            sum_b = 0.0;
            for(var i = -1; i <= 1; i++){
              for(var j = -1; j <= 1; j++){
                x1 = subm_index(m, x - i);
                y1 = subm_index(n, y - j);
                sum_a = sum_a + L[i+1][j+1] * X.A[x1][y1];
                sum_b = sum_b + L[i+1][j+1] * X.B[x1][y1];
              }
            }
            t = X.A[x][y]*X.B[x][y]*X.B[x][y];
            A[x][y] = X.A[x][y] + DA*sum_a - t + f*(1-X.A[x][y]);
            B[x][y] = X.B[x][y] + DB*sum_b + t - (k+f)*X.B[x][y];
          }
        }
        return {A: A, B: B};
      }
      function iterate_Gray_Scott(X, L, DA, DB, f, k, n){
        for(var i = 0; i < n; i++){
          X = update_concentrations(X, L, DA, DB, f, k); 
        }
        return X;
      }
      var L = [[0.05, 0.2, 0.05], [0.2, -1, 0.2], [0.05, 0.2, 0.05]];
      var DA = 1;
      var DB = 0.5;
      var f = 0.0545;
      var k = 0.062;
    </script>

    <script type="text/paperscript" canvas="quad">
      var pixels = 200;
      var gridSize = 2;
      var rectSize = 2;

      var A = new Array(pixels);
      var B = new Array(pixels);
      for(var i = 0; i < pixels; i++){
        A[i] = new Array(pixels);
        B[i] = new Array(pixels);
        for(var j = 0; j < pixels; j++){
          A[i][j] = 1;
          B[i][j] = Math.random() < 0.98 ? 0 : 1;
        }
      }
      var X = {A: A, B: B};

      X = iterate_Gray_Scott(X, L, DA, DB, f, k, 1000);
      
      for(var y = 0; y < pixels; y++){
        for(var x = 0; x < pixels; x++){
          var color = {
          hue: X.B[x][y] * 360,
          saturation: 1,
          brightness: 1
         };
          var path = new Path.Rectangle(new Point(x, y) * gridSize, new Size(rectSize, rectSize));
          path.fillColor = color;
        }
      }
      project.activeLayer.position = view.center;
    </script>
    <canvas id="quad" resize></canvas>
  </body>
</html>

Now, here is a code which generates the animation:

<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.2/paper-full.min.js"></script>
    <style>
      canvas {
        width: 400px;
        height: 400px;
        border: black 3px solid;
      }
    </style>
  </head>
  <body>
    <script>
      function subm_index(M, x){
        if (x<0)
          return x+M;
        if(x >= M)
          return x-M;
        return x;
      }
      function update_concentrations(X, L, DA, DB, f, k){ 
        var sum_a, sum_b, x1, y1, t;
        var m = X.A.length;
        var n = X.A[0].length;
        var A = new Array(m);
        var B = new Array(m);
        for(var i = 0; i < m; i++){
          A[i] = new Array(n);
          B[i] = new Array(n);
        }        
        for(var x = 0; x < m; x++) {
          for(var y = 0; y < n; y++){
            sum_a = 0.0;
            sum_b = 0.0;
            for(var i = -1; i <= 1; i++){
              for(var j = -1; j <= 1; j++){
                x1 = subm_index(m, x - i);
                y1 = subm_index(n, y - j);
                sum_a = sum_a + L[i+1][j+1] * X.A[x1][y1];
                sum_b = sum_b + L[i+1][j+1] * X.B[x1][y1];
              }
            }
            t = X.A[x][y]*X.B[x][y]*X.B[x][y];
            A[x][y] = X.A[x][y] + DA*sum_a - t + f*(1-X.A[x][y]);
            B[x][y] = X.B[x][y] + DB*sum_b + t - (k+f)*X.B[x][y];
          }
        }
        return {A: A, B: B};
      }
      function iterate_Gray_Scott(X, L, DA, DB, f, k, n){
        for(var i = 0; i < n; i++){
          X = update_concentrations(X, L, DA, DB, f, k); 
        }
        return X;
      }
      var L = [[0.05, 0.2, 0.05], [0.2, -1, 0.2], [0.05, 0.2, 0.05]];
      var DA = 1;
      var DB = 0.5;
      var f = 0.0545;
      var k = 0.062;
    </script>

    <script type="text/paperscript" canvas="quad">
      var pixels = 200;
      var gridSize = 2;
      var rectSize = 2;
      var A = new Array(pixels);
      var B = new Array(pixels);
      var Paths = new Array(pixels);
      for(var i = 0; i < pixels; i++){
        A[i] = new Array(pixels);
        B[i] = new Array(pixels);
        Paths[i] = new Array(pixels);
        for(var j = 0; j < pixels; j++){
          A[i][j] = 1;
          B[i][j] = Math.random() < 0.99 ? 0 : 1;
        }
      }
      var X = {A: A, B: B};

      for(var y = 0; y < pixels; y++){
        for(var x = 0; x < pixels; x++){
          var color = {
          hue: X.B[x][y] * 360,
          saturation: 1,
          brightness: 1
         };
          Paths[x][y] = new Path.Rectangle(new Point(x, y) * gridSize, new Size(rectSize, rectSize));
          Paths[x][y].fillColor = color;
        }
      }

      var nframes = 100;

      var XX = new Array(nframes);
      XX[0] = X;
      for(var frm = 1; frm < nframes; frm++){
        XX[frm] = iterate_Gray_Scott(XX[frm-1], L, DA, DB, f, k, frm);
      }

      project.activeLayer.position = view.center;

      function onFrame(event){
        console.log(event.count);
        if(event.count < nframes){
          for(var y = 0; y < pixels; y++){
            for(var x = 0; x < pixels; x++){
              var color = {
                hue: XX[event.count].B[x][y] * 360,
                saturation: 1,
                brightness: 1
              };
              Paths[x][y].fillColor = color;
            }
          }
        }
      }
    </script>

    <canvas id="quad" resize></canvas>
  </body>
</html>

It works but the animation is not fluid enough. This is due to the double loop in onFrame, which consumes some time.

So I firstly tried to create an array containing nframes Group elements, each group containing pixels*pixels Rectangle elements. But this generated an out-of-memory.

So I tried to use a Symbol to save some memory. The code is below but it does not work, there is not a single image appearing.

<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.2/paper-full.min.js"></script>
    <style>
      canvas {
        width: 400px;
        height: 400px;
        border: black 3px solid;
      }
    </style>
  </head>
  <body>
    <script>
      function subm_index(M, x){
        if (x<0)
          return x+M;
        if(x >= M)
          return x-M;
        return x;
      }
      function update_concentrations(X, L, DA, DB, f, k){ 
        var sum_a, sum_b, x1, y1, t;
        var m = X.A.length;
        var n = X.A[0].length;
        var A = new Array(m);
        var B = new Array(m);
        for(var i = 0; i < m; i++){
          A[i] = new Array(n);
          B[i] = new Array(n);
        }        
        for(var x = 0; x < m; x++) {
          for(var y = 0; y < n; y++){
            sum_a = 0.0;
            sum_b = 0.0;
            for(var i = -1; i <= 1; i++){
              for(var j = -1; j <= 1; j++){
                x1 = subm_index(m, x - i);
                y1 = subm_index(n, y - j);
                sum_a = sum_a + L[i+1][j+1] * X.A[x1][y1];
                sum_b = sum_b + L[i+1][j+1] * X.B[x1][y1];
              }
            }
            t = X.A[x][y]*X.B[x][y]*X.B[x][y];
            A[x][y] = X.A[x][y] + DA*sum_a - t + f*(1-X.A[x][y]);
            B[x][y] = X.B[x][y] + DB*sum_b + t - (k+f)*X.B[x][y];
          }
        }
        return {A: A, B: B};
      }
      function iterate_Gray_Scott(X, L, DA, DB, f, k, n){
        for(var i = 0; i < n; i++){
          X = update_concentrations(X, L, DA, DB, f, k); 
        }
        return X;
      }
      var L = [[0.05, 0.2, 0.05], [0.2, -1, 0.2], [0.05, 0.2, 0.05]];
      var DA = 1;
      var DB = 0.5;
      var f = 0.0545;
      var k = 0.062;
    </script>

    <script type="text/paperscript" canvas="quad">
      var pixels = 50;
      var gridSize = 2;
      var rectSize = 2;
      var A = new Array(pixels);
      var B = new Array(pixels);
      var Paths = new Array(pixels);
      for(var i = 0; i < pixels; i++){
        A[i] = new Array(pixels);
        B[i] = new Array(pixels);
        Paths[i] = new Array(pixels);
        for(var j = 0; j < pixels; j++){
          A[i][j] = 1;
          B[i][j] = Math.random() < 0.99 ? 0 : 1;
        }
      }
      var X = {A: A, B: B};

      var nframes = 50;

      var XX = new Array(nframes);
      XX[0] = X;
      for(var frm = 1; frm < nframes; frm++){
        XX[frm] = iterate_Gray_Scott(XX[frm-1], L, DA, DB, f, k, frm);
      }
      
      var Rects = [];
      for(var x = 0; x < pixels; x++){
        for(var y = 0; y < pixels; y++){
          var rect = new Path.Rectangle(new Point(x, y) * gridSize, new Size(rectSize, rectSize));
          var color = {
            hue: 1,
            saturation: 1,
            brightness: 1
          };
          rect.fillColor = color;
          rect.visible = false;
          Rects.push(rect);
        }
      }

      group = new Group(Rects);

      symbolGroup = new Symbol(group);

      var Groups = new Array(nframes);
      for(var frm = 0; frm < nframes; frm++){
        Groups[frm] = symbolGroup.place(view.center);
        var k = 0;
        for(var x = 0; x < pixels; x++){
          for(var y = 0; y < pixels; y++){
            Groups[frm].definition.definition.children[k].fillColor = {
              hue: XX[frm].B[x][y] * 360,
              saturation: 1,
              brightness: 1
            };
            k = k+1;
          }
        } 
        XX[frm] = null; // to free some memory
      } 

      project.activeLayer.position = view.center;

      function onFrame(event){
        if(event.count < nframes){
          console.log(event.count);
          Groups[event.count].visible = true;
          if(event.count > 0){
            Groups[event.count-1].visible = false; // to free some memory
          }
        }
      }
    </script>

    <canvas id="quad" resize></canvas>
  </body>
</html>

Can you help me to fix this last code?

Stéphane Laurent
  • 75,186
  • 15
  • 119
  • 225
  • Is not clear what you're trying to do, `Symbol`s are meant to be used as keys and you're not supposed to create them using `new`. You can find out more about them from [HERE](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) – Titus Jan 06 '20 at 09:14
  • @Titus Thanks. Sorry I found that [here](http://paperjs.org/tutorials/project-items/working-with-symbols/#symbol-definition). Maybe this is a typo and I should use `SymbolDefinition` instead. But I've just tried with no luck. – Stéphane Laurent Jan 06 '20 at 09:33
  • Oh, it seems that in this case `Symbol` is a function provided by the **paper.js** library not the build in function that I was referring to. – Titus Jan 06 '20 at 10:12

2 Answers2

2

Syntactically, I think there's a small issue here:

Groups[frm].definition.definition.children[k].fillColor
|           |          |          |           ^ changing fill color of Rect
|           |          |          ^ accessing children of group
|           |          ^ should be item*
|           ^ SymbolDefinition
^ array of placed symbols

* [SymbolDefinition.item] http://paperjs.org/reference/symboldefinition/

But I wouldn't spend a lot of time trying to get Symbols to render anyway.

Reading about Symbols, it seems that making changes to the definition of a SymbolDefinition will update all placed instances as they point to a common source. As a technique to speed up your operations I think this will backfire. You are still making the same number of fill operations as you were with the 2D array.

An alternative approach is to use a sort of pseudo double-buffering. Do your fill operations on a set of rectangles that are not on screen, then cache that group to bitmap and display that. Each refresh will avoid unnecessary draw operations and will happen as a unit. Caching as bitmap will speed up the pixel rendering as well.

Paper.js doesn't seem to give you an obvious way to implement this, but you could probably use 2 layers to accomplish it. Since only 1 layer will be "active" at a time, you could draw your rectangles on one, cache it as bitmap, then activate. Then draw the next set of rectangles on the 2nd layer, cache it as bitmap, and activate. Toggling back and forth might get you the performance enhancement you need.

Alternatively, there's a thing called PaperScope which you might be able to leverage for the same technique. PaperScope uses a second canvas element. You'll need to manually activate them as you toggle in the same way as layers.

James Tomasino
  • 3,520
  • 1
  • 20
  • 38
  • As a suppliment, this answer to a question about double-buffering in canvas has a basic code example to illustrate the method. This would closely align with a PaperScope implementation for paper.js - https://stackoverflow.com/a/10357038/1041926 – James Tomasino Jan 11 '20 at 20:28
2

If you are aiming for performance, I that that in this case, you should get rid of Paper.js which adds an overhead to the rendering part.
As a replacement, you can directly work with the Canvas API to render your drawing.
Here is a quick example, based on your code, showing how you could use it.

<html>
<head>
    <style>
        canvas {
            width  : 400px;
            height : 400px;
        }
    </style>
</head>
<body>
<canvas></canvas>

<script>
    function subm_index(M, x) {
        if (x < 0) {
            return x + M;
        }
        if (x >= M) {
            return x - M;
        }
        return x;
    }

    function update_concentrations(X, L, DA, DB, f, k) {
        var sum_a, sum_b, x1, y1, t;
        var m = X.A.length;
        var n = X.A[0].length;
        var A = new Array(m);
        var B = new Array(m);
        for (var i = 0; i < m; i++) {
            A[i] = new Array(n);
            B[i] = new Array(n);
        }
        for (var x = 0; x < m; x++) {
            for (var y = 0; y < n; y++) {
                sum_a = 0.0;
                sum_b = 0.0;
                for (var i = -1; i <= 1; i++) {
                    for (var j = -1; j <= 1; j++) {
                        x1 = subm_index(m, x - i);
                        y1 = subm_index(n, y - j);
                        sum_a = sum_a + L[i + 1][j + 1] * X.A[x1][y1];
                        sum_b = sum_b + L[i + 1][j + 1] * X.B[x1][y1];
                    }
                }
                t = X.A[x][y] * X.B[x][y] * X.B[x][y];
                A[x][y] = X.A[x][y] + DA * sum_a - t + f * (1 - X.A[x][y]);
                B[x][y] = X.B[x][y] + DB * sum_b + t - (k + f) * X.B[x][y];
            }
        }
        return { A: A, B: B };
    }

    function iterate_Gray_Scott(X, L, DA, DB, f, k, n) {
        for (var i = 0; i < n; i++) {
            X = update_concentrations(X, L, DA, DB, f, k);
        }
        return X;
    }

    var L = [[0.05, 0.2, 0.05], [0.2, -1, 0.2], [0.05, 0.2, 0.05]];
    var DA = 1;
    var DB = 0.5;
    var f = 0.0545;
    var k = 0.062;


    var pixels = 200;
    var gridSize = 2;
    var rectSize = 2;
    var A = new Array(pixels);
    var B = new Array(pixels);
    var Paths = new Array(pixels);
    for (var i = 0; i < pixels; i++) {
        A[i] = new Array(pixels);
        B[i] = new Array(pixels);
        Paths[i] = new Array(pixels);
        for (var j = 0; j < pixels; j++) {
            A[i][j] = 1;
            B[i][j] = Math.random() < 0.99 ? 0 : 1;
        }
    }
    var X = { A: A, B: B };

    var nframes = 50;

    var XX = new Array(nframes);
    XX[0] = X;
    for (var frm = 1; frm < nframes; frm++) {
        XX[frm] = iterate_Gray_Scott(XX[frm - 1], L, DA, DB, f, k, frm);
    }



    //
    // New code
    //

    // Get a reference to the canvas element. 
    const canvas = document.querySelector('canvas');
    // Make sure that canvas internal size fits its display size. 
    canvas.width = 400;
    canvas.height = 400;
    // Get canvas context to be able to draw directly on it. 
    const ctx = canvas.getContext('2d');

    // Counter used to swicth between frames.
    let currentFrame = 0;

    // Launch the animation.
    animate();

    function animate() {
        // Draw current frame.
        draw();

        // Update frame counter (make it loop after last frame).
        currentFrame = currentFrame < nframes - 1 ? currentFrame + 1 : 0;

        // Do a recursive call to render next frame.
        requestAnimationFrame(animate);
    }

    function draw() {
        // For each pixel...
        for (var y = 0; y < pixels; y++) {
            for (var x = 0; x < pixels; x++) {
                // ...get the color...
                const hue = Math.round(XX[currentFrame].B[x][y] * 360);
                const color = `hsl(${hue}, 100%, 50%)`;
                // ...use it as canvas fill color...
                ctx.fillStyle = color;
                // ...and draw a scaled rectangle.
                ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize);
            }
        }
    }
</script>

</body>
</html>

Another improvement idea could be to map your current data source to an array of prepared ImageData that you will later load in the canvas rather than looping over the pixels.

sasensi
  • 4,610
  • 10
  • 35