idyll-p5

Embed p5.js sketches in idyll!

Embed p5js sketches (relatively) easily in idyll with this custom component! Source code for the component and this page in this github repository

[Sketch webGL:1 ratio:`3/1` sketch:`(p5) => {
  p5.draw = () => {
    const frame = p5.frameCount;
    p5.background(250);
    p5.rotateY(frame * 0.01);

    for(var j = 0; j < 5; j++){
      p5.push();
      for(var i = 0; i < 80; i++){
        p5.translate(
          p5.sin(frame * 0.001 + j) * 100,
          p5.sin(frame * 0.001 + j) * 100,
          i * 0.1
        );
        p5.rotateZ(frame * 0.002);
        p5.push();
        p5.sphere(8, 6, 4);
        p5.pop();
      }
      p5.pop();
    }
  }
}` /]

(WebGL example taken from p5js examples website)

To embed a sketch, we have to use instance mode. The code is passed as a string representing the function body of an instance mode sketch. So:

function sketch(p5){
  p5.setup = () => { /* setup code */  };
  p5.draw = () => { /* draw code */ };
  // etc.
}

Becomes:

[Sketch sketch:`(p5, options) => {
  p5.setup = () => {
    /* DO NOT USE createCanvas here! */
  };
  p5.draw = () => { /* draw code */ };
  // etc.
}` /]

There are a few more subtle differences with regular p5 code, and a few added conveniences to play nice with idyll. First, the Sketch component handles the sketch size. That is probably worth emphasizing:

IMPORTANT: DO NOT USEcreateCanvasin p5.setup! The size depends on the container that holds the sketch, and the Sketch component handles this logic. This is explained in more detail below, at Sketch size and ratio.

Because this function body does not have access to the global browser scope, it is passed variables via the options parameter to make life little easier:

[Sketch sketch:`(p5, { width, height }) => {
  p5.setup = () => {
    /* DO NOT USE createCanvas here! */
  };
  p5.draw = () => { /* draw code */ };
  // etc.
}` /]

any variables that are defineed in the Idyll document are automatically in the scope of the sketch function. So

[var name:"x" value:10 ]
[Sketch sketch:`(p5, { width, height }) => {
  p5.draw = () => {
    // you can reference x here
    const xSquared = x * x;
  };
}` /]

The sketch unmounts and resets in response to resize events to keep a correct size. To try this out, resize the browser window, or switch between portrain/landscape if you are on mobile.

If the sketch needs to run specific code before unmounting the component, define a p5.unmount function. If will be triggered just before the component unmounts.

Background Color: Line Color:

Code for the above demo:

[var name:"bgColor" value:5 /]
[var name:"lineColor" value:250 /]

[Sketch
  bgColor:bgColor
  sketch:`(p5, { width, height }) => {
    let size = 25;
    p5.setup = () => {
      // no createCanvas required!
    }

    p5.draw = () => {
      p5.fill(bgColor, 16);
      p5.noStroke();
      p5.rect(0, 0, width, height);
      size = 40 + 10*p5.sin(p5.frameCount * p5.TAU / 60);
      p5.stroke(lineColor);
      p5.strokeWeight(size);
      p5.line(p5.mouseX, p5.mouseY, p5.pmouseX, p5.pmouseY);
    };

    p5.unmount = () => {
      console.log('The sketch was unmounted. Width was ' +
      width + ', height was ' + height);
    }
}` /]

Background Color: [Range min:0 max:255 value:bgColor /]
Line Color: [Range min:0 max:255 value:lineColor /]

The sketch can use the updateProps option to trigger an update in an Idyll variable. Click on the sketch to make it invert its colors.

[mathisonian note]:

Right now you have to pass in the property explicity if you want Idyll to be able to modify it. Thats why clickBgColor and clickLineColor are provided to the Sketch component. I think this is something that should be updated in Idyll.

[jobleonard note]: I added a sketch that shows some issues with the current approach. The main issue is that derived variables reset the sketch.
[var name:"clickBgColor" value:0 /]
[var name:"clickLineColor" value:255 /]

[Sketch
  clickBgColor:clickBgColor
  clickLineColor:clickLineColor
  ratio:`4/1`
  sketch:`(p5, { width, height, updateProps }) => {
    let size = 25;
    p5.draw = () => {
      p5.fill(clickBgColor, 16);
      p5.noStroke();
      p5.rect(0, 0, width, height);
      p5.fill((128+clickLineColor)/2);
      let size = 300 - 300*p5.cos(p5.frameCount * p5.TAU / 240);
      p5.ellipse(width/2, height/2, size, size);
    };

    p5.mouseClicked = () => {
      updateProps({
        clickBgColor: 255 - clickBgColor,
      });
    }
}` /]

[Sketch
  clickBgColor:clickBgColor
  clickLineColor:clickLineColor
  ratio:`4/1`
  sketch:`(p5, { width, height, updateProps }) => {
    // because this sketch is derived each time,
    // frame is reset each time. This is not
    // expected behaviour (from p5 users POV).
    let frame = 0;
    p5.draw = () => {
      p5.noStroke();

      p5.fill((128+clickBgColor)/2, 16);
      p5.rect(0, 0, width, height);

      p5.fill(clickLineColor);
      let size = 300 - 300*p5.cos(frame * p5.TAU / 240);
      p5.ellips
      frame++;
    };

    p5.mouseClicked = () => {
      updateProps({
        clickLineColor: 255 - clickLineColor
      });
    }
}` /]
[jobleonard note]: A few demos of the new and improved mouse and keyboard listening events.
[var
  name:"mouseSketch"
  value:`(p5, { width, height, updateProps }) => {
    let clickedX = 0, clickedY = 0;
    let pressedX = 0, pressedY = 0;
    let releasedX = 0, releasedY = 0;
    let movedX = 0, movedY = 0;
    let draggedX = 0, draggedY = 0;
    let wheelVal = 0;
    p5.draw = () => {
      p5.background(0);
      p5.noStroke();
      p5.textSize(16);
      p5.fill(0, 0, 255);
      p5.text('clicked: ' + clickedX + ', ' + clickedY, 32, 20);
      p5.fill(255, 255, 0);
      p5.text('pressed: ' + pressedX + ', ' + pressedX, 32, 40);
      p5.fill(255, 0, 255);
      p5.text('released: ' + releasedX + ', ' + releasedY, 32, 60);
      p5.fill(0, 255, 0);
      p5.text('moved: ' + movedX + ', ' + movedY, 32, 80);
      p5.fill(255, 0, 0);
      p5.text('dragged: ' + draggedX + ', ' + draggedY, 32, 100);
      p5.fill(0, 255, 255);
      p5.text('wheel: ' + wheelVal, 32, 120);
    };

    p5.mouseClicked = () => {
      clickedX = p5.mouseX;
      clickedY = p5.mouseY;
    }
    p5.mousePressed = () => {
      pressedX = p5.mouseX;
      pressedY = p5.mouseY;
    }
    p5.mouseReleased = () => {
      releasedX = p5.mouseX;
      releasedY = p5.mouseY;
    }
    p5.mouseMoved = () => {
      movedX = p5.mouseX;
      movedY = p5.mouseY;
    }
    p5.mouseDragged = () => {
      draggedX = p5.mouseX;
      draggedY = p5.mouseY;
    }
    p5.mouseWheel = (event) => {
      wheelVal += event.delta;
      // prevent scrolling when
      // mouse is on sketch
      // return false;
    }

}` /]

[Sketch
  ratio:`4/1`
  sketch:mouseSketch /]

[Sketch
  ratio:`4/1`
  alwaysListen:1
  sketch:mouseSketch /]
[jobleonard note]: I can't seem to get the touch events to behave right (and they only work on Chrome for me, not on Firefox). It's probably easier to just advice people to rely on the mouse triggers.
[var
  name:"touchSketch"
  value:`(p5, { width, height, updateProps }) => {
    let start = 0, ended = 0;
    let movedX = 0, movedY = 0;
    p5.draw = () => {
      p5.background(0);
      p5.noStroke();
      p5.textSize(16);
      p5.fill(0, 0, 255);
      p5.text('touch start: ' + start, 32, 20);
      p5.fill(255, 255, 0);
      p5.text('touch moved: ' + movedX + ', ' + movedY, 32, 40);
      p5.fill(255, 0, 255);
      p5.text('ended: ' + ended, 32, 60);
      p5.fill(0, 255, 0);
    };

    p5.touchStarted = () => {
      start++;
    }
    p5.touchMoved = () => {
      movedX = p5.touches[0].x;
      movedY = p5.touches[0].y;
    }
    p5.touchEnded = () => {
      ended++;
    }


}` /]

[Sketch
  ratio:`4/1`
  sketch:touchSketch /]

[Sketch
  ratio:`4/1`
  alwaysListen:1
  sketch:touchSketch /]
[var
  name:"keySketch"
  value:`(p5, { width, height, updateProps }) => {
    let _keyPressed = '';
    let _keyReleased = '';
    let _keyTyped = '';

    p5.draw = () => {
      p5.background(0);
      p5.fill(255);
      p5.textSize(16);
      p5.text('keyPressed: ' + _keyPressed, 20, 20);
      p5.text('keyReleased: ' + _keyReleased, 20, 40);
      p5.text('keyTyped: ' + _keyTyped, 20, 60);
    };

    p5.keyPressed = () => {
      _keyPressed += p5.key;
    };

    p5.keyReleased = () => {
      _keyReleased += p5.key;
    };

    p5.keyTyped = () => {
      _keyTyped += p5.key;
    };

} ` /]


[Sketch
  ratio:`4/1`
  sketch:keySketch /]

[Sketch
  ratio:`4/1`
  alwaysListen:1
  sketch:keySketch /]

Sketch size and ratio

By default, the width of the sketch is equal to column width of the text, and height will be half of the width.

You can pass a value to width and height to override this. The value can be the number of pixels like 100, or a CSS-valid string like "50%" or "2em".

By default, height depends on width, but width does not depend on height. Passing a value to width will make height half of the new width. Passing a value to height will not affect width, which will still be as wide as the text column.

To enforce a ratio, you can pass a number to ratio, i.e. the expression ratio:`3/1` will produce a sketch with a width three times the height.

If no width is defined, but a height is, width depends on height (as you can see below, this part is still a bit buggy). If a ratio, width and height are defined, height is overridden to depend on width.

[var name:"sketch_ratio" value:`(p5) => {
  p5.setup = () => {
    p5.noLoop();
  };

  p5.draw = () => {
    p5.background(0);
  };
}` /]

[Sketch
  sketch:sketch_ratio /]

[Sketch
  ratio:`4/1`
  sketch:sketch_ratio /]

[Sketch
  height:50
  sketch:sketch_ratio /]

[Sketch
  ratio:`2/1`
  width:"50%"
  sketch:sketch_ratio /]

[Sketch
  ratio:`2/1`
  width:200
  height:200
  sketch:sketch_ratio /]

[Sketch
  ratio:`2/1`
  height:50
  sketch:sketch_ratio /]

Manually triggered resets

A watchedVal triggers a reset of the sketch whenever the value it watches is changed:

[var name:"resetSketch" value:0 /]
[var name:"sketch_reset" value:`(p5, {width, height}) => {
  let x = width/2, y = height/2, dx = 0;

  p5.draw = () => {
    p5.fill(0, 4);
    p5.noStroke();
    p5.rect(0, 0, width, height);
    p5.stroke(256);
    const dx_scaled = dx / (1<<4);
    p5.strokeWeight(20 + dx_scaled);
    p5.line(x, y, x+dx_scaled, y);
    x = (x+dx_scaled)%width;
    p5.line(x-dx_scaled, y, x, y);
    dx++;
  };
 }` /]

[Sketch
  height:100
  watchedVal:resetSketch
  sketch:sketch_reset /]
[Button onClick:`resetSketch++`]Reset Sketch![/Button]