Search code examples
javascriptpaperjs

paper.js - create array of draggable shapes


I am trying to create a set of shapes in paper.js. Each shape should be draggable and the index of that shape be given during dragging.

I have tried the following code, but I just get the message "Uncaught TypeError: Cannot read property 'position' of undefined".

Any help gratefully received.

<canvas id = "canvas_1" > < /canvas>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.11/paper-full.min.js" > < /script>

<script type = "text/paperscript" canvas = "canvas_1" >
  var path = [];

  for (i = 0; i < 5; i++) {

    path[i] = new Path.Circle({
      center: view.center,
      radius: 30,
      fillColor: 'blue'
    });

    path[i].onMouseDrag = function(event) {
      path[i].position += event.delta;
      console.log("shape " + i + " is being dragged.")
    }

  }

</script>

Solution

  • Problem

    Your error comes down to the way variable scoping works in Javascript: variables are scoped to the containing function or script. By the time you come along and drag the circle, i === 5 from the for loop, and thus path[i] is undefined (out of bounds) in the drag function. Hence, your error.

    If you would like to learn more, this answer provides an in-depth look into this "closure inside loop" problem.

    Solutions

    This answer provides some common solutions to this problem.

    ES6 Let

    The ES6 let statement is the modern solution to this problem, avoiding many of the scope "quirks" of the var statement. Variables scope to the containing block, so each pass of the loop gets its own instance of i.

    To use ES6 features within PaperScript, you first need to load a newer version of the Acorn parser (thanks sasensi and Geddes). Then simply declare let i in the loop:

    var path = [];
    
    for (let i = 0; i < 5; i++) {
      path[i] = new Path.Circle({
        center: view.center,
        radius: 30,
        fillColor: 'blue'
      });
      path[i].onMouseDrag = function(event) {
        path[i].position += event.delta;
        console.log("shape " + i + " is being dragged.")
      };
    }
    

    Here is a working Stackblitz.

    Closure

    Without modifying the PaperScript parser, you could use a closure to capture the index in a new function scope:

    var path = [];
    
    function createMouseDragHandler(index) {
      return function(event) {
        path[index].position += event.delta;
        console.log("shape " + index + " is being dragged.")
      };
    }
    
    for (var i = 0; i < 5; i++) {
      path[i] = new Path.Circle({
        center: view.center,
        radius: 30,
        fillColor: 'blue'
      });
      path[i].onMouseDrag = createMouseDragHandler(i);
    }
    

    Here is a working Stackblitz.