How to insert P5.js into Twine 2 Sugarcube?

Hello IF community :slight_smile:,

I’m a beginner creating a story in Twine 2 (version 2.3.5) using Sugarcube (2.30.0), and I want to insert some P5.js code that maps graphics onto jpg. images as a background canvas to my Twine story. For most passages, the same code can be used but using different jpg. files. For others, I want to use different P5.js code which doesn’t call any images.

After reading up on the subject, I think I have successfully changed my code to instance mode, but I’m stuck on a few things:

  1. I’m not sure if my second tab containing classes etc. needs to be changed to instance mode as well? I’ve assumed that it doesn’t but could well be wrong?

  2. I have no idea where I would save the jpg. files so the code can load them? Probably missing the wood for the trees, here.

  3. I don’t know what to do next?

I’m more than happy to do the homework myself but it’s unclear what my next step is? It seems like there are multiple ways of doing this?

It’d be extremely grateful if someone could please give me a steer in the right direction?

Thank you so much!!! smile: :grinning:

mySketch.js

var sketch = function(p);{

    
p.let segments = [];
p.let numSegments = 60;
p.let segmentResolution = 400;
p.let segmentLength;
p.let segmentSpacing;
p.let heightScale = 150;
p.let fadeIn = 0;
p.let speed = 0.51;
p.let mp = false;
p.let mouseIsDown = false;

p.let loaded = false;
p.let img;
p.let tt = 0;
p.let glitchFreq = 0.5;

p.let now, lastTime = 0;
    
    p.setup = function(){
     p.createCanvas(p.windowWidth, p.windowHeight);   
    }
    
    p.draw = function(){
       p.if (!loaded) p.return;

  p.let dt = (p.millis() - p.lastTime) / 1000;
  p.lastTime = p.millis();
  p.tt += dt;

  p.update(dt);

	p.fill(0, 250);
	p.noStroke();
	p.rect(0,0, p.windowWidth, p.windowHeight);

  p.let p.gridHeight = p.numSegments * p.segmentSpacing;	

  p.push();
	p.heightScale = 60 + (p.sin(p.millis()/1000) / p.TAU) * 50;
  p.translate(p.windowWidth / 2 - img.width, p.windowHeight / 2 - img.height / 2 - 100);
  p.scale(2, 2);
	
	p.if(p.mouseIsDown){
		p.tint(55,0,0);
		p.image(img, 0, 0);
	}
	p.else{
		p.tint(255);
	}
  
  p.let trans = false;

  p.let ss = 0;

  p.segments.forEach(s => {
    p.ss++;
    // let n = noise(millis()/200 + s.dist/500);
    p.let n = 0;

    p.if (ss > 30) {
      p.n = p.noise(p.millis() / 200);
			// ss / numSegments
    }

    /*if (n > glitchFreq && trans === false) {
      push();
      translate(40, 0);
      trans = true;
    }*/
		
    p.s.draw();
  });

  p.if (trans) {
    p.pop();
  }
        
  p.pop();  
    }

}

var myp5 = new p5(sketch);

tab2.js

let id = 0;

class Segment {
  constructor(cfg) {
    Object.assign(this, cfg);

    this.t = 0;
    this.dist = 0;
    this.id = id++;
    this.nextNoise = 0;
    // [x,y, x,y, ...]
    // add 2 for a point starting at the very start of the viewport and one of the end.
    this.vertices = new Float32Array(segmentResolution * 2 + 2);

    this.update(0);
  }

  update(dt) {
    this.t += dt * 1;
    fadeIn += dt * .008;
    fadeIn = constrain(fadeIn, 0, 1);

    let xSpacing = this.len / segmentResolution;

    this.pos.y -= dt * speed;
    this.dist += dt * speed;
    //if (this.pos.y > img.height / segmentSpacing) {
    //  this.pos.y -= img.height / segmentSpacing;
    //}
    if (this.pos.y < 0) {
      this.pos.y += img.height / segmentSpacing;
    }

    for (let i = 0; i < this.vertices.length; i += 2) {

      let x = (i / 2) * xSpacing;
      let y = this.pos.y * segmentSpacing;

      if (mp) {
        let d = dist(x, y, (mouseX - windowWidth / 2 + windowWidth / 4 + 50) / 2, (mouseY - windowHeight / 2 + windowHeight / 4 + 250) / 2);
        if (d < 40) {
          let a = (8 / d) * 8;
          y += (a > 50) ? 50 : a;
        }
      }

      let col = img.get(x, y);
      let intensity = col[0] / 255;

      this.vertices[i + 0] = x;
      this.vertices[i + 1] = y - (intensity) * heightScale;
    }

  }

  draw() {
    strokeWeight(1);

    let waves = (sin(this.pos.y / 2.0 + this.t ) / PI) + 0.5;
    let vignette = sin(this.pos.y * segmentSpacing / img.height * PI);
    stroke(255, 255 * fadeIn * waves * vignette + 50 * vignette);
    // fill(0);
    noFill();

    beginShape();
    for (let i = 0; i < this.vertices.length; i += 2) {
      if (i === 0) {
        vertex(-1000, this.vertices[i + 1]);
      } else if (i + 2 === this.vertices.length) {
        vertex(10000, this.vertices[i + 1]);
      } else {
        vertex(this.vertices[i + 0], this.vertices[i + 1]);

        if (this.id < numSegments - 1) {
          let s = segments[id + 1];
          vertex(this.vertices[i + 0], this.vertices[i + 1]);
        }
      }
    }
    endShape();

You may find this Q&A on P5.js integration helpful.

Thanks @TheMadExile :slight_smile:.

I did spend a fair amount of time reading that thread before posting. I put the following in my Story JavaScript

setup.p5promise = importScripts([
	"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.1/p5.min.js",
	"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.1/addons/p5.dom.min.js",
	"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.6.1/addons/p5.sound.min.js"
]);

But i’m unsure which parts of the passage code you suggested are specific to Colin’s sketch and which are universal to make any P5 code work in a passage.

Also, do I need to minify my code as well as adapt it to instance mode?

Thanks so much for your help and for creating a great program.

The only things truly specific to their situation were the contents of their sketch function.

If your class is calling P5 functions, then it does need to be rewritten for instance mode. Your code does not need to be minified.

Additionally. Since you’re importing P5 and add-ons, rather than bundling them in, you’ll need to use the Promise you stored in setup.p5promise to ensure they’re loaded when attempting to use them. Though, that’s a minor issue.

Note: On my phone, ATM.

1 Like

@weeMan Did you get this figured out?

I didn’t, @TheMadExile. Thanks for checking. I’m still stuck trying to rewrite my code to instance mode, particularly my tab2.js with its class etc.

Is there any simpler way to get a P5 sketch running in Twine?

Hey @TheMadExile,

I finally managed to get all the code converted to instance mode so hopefully I’ve broken the back of it now.

It seems like there are a couple of ways to import it into Twine. Could you offer any guidance on the most efficient one for my needs please?

Essentially my P5 sketch maps graphics onto a jpg image file. In various passages, I want to call the same sketch but with different jpg images. My (highly unreliable) instinct tells me to create several different local versions of the P5 sketch with different jpgs, declare all of them with a tag in the published HTML and call them in the passages I want them. Inspired by this thread.

My questions are:

  1. Is this the best method?
  2. If it is, how do I edit the html file? I appreciate this may already be in a thread but I couldn’t find a relevant one.
  3. Finally, what is the syntax for calling the script tag in a passage? Again my research on this threw up more questions than answers.

Thanks again :slight_smile:

I’m writing this on my phone from bed due to health issues, so please bear with me.


If you’re using importScripts(), then you’re already importing the scripts—you just need to use the returned Promise to ensure it’s completely loaded when you go to use it.

If you wanted to instead have it bundled with your project so it would always be available, then with Twine 2 your best bet would probably be to use the wrapper method explained in that thread. Just paste all three in-order within the wrapper, inside the Story JavaScript.

Either way, you simply use P5 in instanced mode as normal.

As far as the sketch. If it’s really the same sketch function and all you’re changing is the target image, then my first instinct is to simply assign the sketch function to the setup object, as shown in one of the linked threads (see the latter part of this post for details), and then reuse it with the different image targets. I’m not sure how you’re currently accessing the images, so that might require some tinkering on your part.

I really appreciate you taking the time to reply while you’re sick. Don’t worry, i’ll figure it out. Get well soon :slight_smile:

Hey! I’m curious! have you made any progress?

Hey,

Sorry this took me a shockingly long time to reply to. Yes, with some help I managed to get it working. Here is my code from the JS file:

//This section sets up mifir land sketch and Uma people sketch

//"Terrain" and "g̛li̸tch͏" have been modified and converted into instance (or namespace) mode, and wrapped in functions which are then called in passages with 'window.setup'

//dotGUI allows the audience to interact with and configure the properties of the land sketch, further exploring the piece's themes of performative identity

//Mifir land sketch: "Terrain" by Tiagohttp://www.openprocessing.org/sketch/729536Licensed under Creative Commons Attribution ShareAlikehttps://creativecommons.org/licenses/by-sa/3.0https://creativecommons.org/licenses/GPL/2.0/

//Uma people sketch: "g̛li̸tch͏" by Andor Sagahttp://www.openprocessing.org/sketch/725478Licensed under Creative Commons Attribution ShareAlikehttps://creativecommons.org/licenses/by-sa/3.0https://creativecommons.org/licenses/GPL/2.0/

window.setup = window.setup || {};
window.setup.p5Loaded = false;
window.setup.dotGUILoaded = false;

//This wraps it up into a function

(function(){
  var p5 = document.createElement("script");
  var dotGUI = document.createElement("script");

  p5.type ="text/javascript"; 
  dotGUI.type = "text/javascript";

  p5.src = "https://cdn.jsdelivr.net/npm/p5@0.8.0/lib/p5.js";
  dotGUI.src = "https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.5/dat.gui.min.js";

  p5.onload = function() {
    window.setup.p5Loaded = true;
  };

  dotGUI.onload = function() {
  window.setup.dotGUILoaded = true;
  };

  document.head.appendChild(p5);
  document.head.appendChild(dotGUI);
}());

//This creates a constant for the UMA sketch

const UMA = function(imgPath){ 
  return function(p){
    //Variables
    p.img;

    p.segments = [];
    p.segmentLength;
    p.segmentSpacing;

    p.numSegments = 60;
    p.segmentResolution = 400;
    p.heightScale = 150;

    p.fadeIn = 0;
    p.speed = 0.51;

    p.mp = false;
    p.mouseIsDown = false;
    p.loaded = false;

    p.tt = 0;
    p.glitchFreq = 0.5;
    p.now = 0;
    p.lastTime = 0;
    p.id = 0;

		//This initiates preload function and loads images from the web-server
		
    p.preload = function(){
      p.loadImage(imgPath, function(_img) {
        p.img = _img;
        p.img.loadPixels();
        p.loaded = true;
        p.segmentSpacing = (p.img.height / p.numSegments);
        p.segmentLength = p.img.width;
    
        for (let i = 0; i < p.numSegments; i++) {
          p.segments.push(new p.Segment({
            pos: { x: 0, y: i },
            len: p.segmentLength
          }));
        }
      });
    }

    p.setup = function(){
      p.createCanvas(p.windowWidth, p.windowHeight/1.5);
    }

    p.update = function(dt){
      p.segments.forEach(
        s => {
          s.update(dt)
        }
      );
    }

    p.mousePressed = function(){
      p.mouseIsDown = true;
      p.glitchFreq = 0.3;
    }

    p.mouseReleased = ()=>{
      p.mouseIsDown = false;
      p.glitchFreq = 0.5;
    }

    p.draw = function(){
      if(!p.loaded)
        return;
      
      let dt = (p.millis() - p.lastTime) / 1000;
      p.lastTime = p.millis();
      p.tt += dt;

      p.update(dt);

      p.fill(0, 250);
      p.noStroke();
      p.rect(0,0, p.windowWidth, p.windowHeight);

      let gridHeight = p.numSegments * p.segmentSpacing;	

      p.push();
      p.heightScale = 60 + (p.sin(p.millis()/1000) / p.TAU) * 50;
      p.translate(p.windowWidth / 2 - p.img.width, p.windowHeight / 2 - p.img.height / 2 - 100);
      p.scale(2, 1.25);
      
      if(p.mouseIsDown){
        p.tint(156,190,48);
        p.image(p.img, 0, 0);
      }
      else{
        p.tint(255);
      }

      let trans = false;

      let ss = 0;

      p.segments.forEach(s => {
        ss++;
        let n = 0;

        if (ss > 30) {
          n = p.noise(p.millis() / 200);
        }
        
        s.draw();
      });

      if (trans) {
        p.pop();
      }
    
      p.pop();
    }

		//This creates a class that sets the sketch's visual properties
		
    p.Segment = class Segment {
      constructor(cfg) {
        Object.assign(this, cfg);
    
        this.t = 0;
        this.dist = 0;
        this.id = p.id++;
        this.nextNoise = 0;
        // [x,y, x,y, ...]
        // add 2 for a point starting at the very start of the viewport and one of the end.
        this.vertices = new Float32Array(p.segmentResolution * 2 + 2);
    
        this.update(0);
      }
    
      update(dt) {
        this.t += dt * 1;
        p.fadeIn += dt * .008;
        p.fadeIn = p.constrain(p.fadeIn, 0, 1);
    
        let xSpacing = this.len / p.segmentResolution;
    
        this.pos.y -= dt * p.speed;
        this.dist += dt * p.speed;
        //if (this.pos.y > img.height / segmentSpacing) {
        //  this.pos.y -= img.height / segmentSpacing;
        //}
        if (this.pos.y < 0) {
          this.pos.y += p.img.height / p.segmentSpacing;
        }
    
        for (let i = 0; i < this.vertices.length; i += 2) {
    
          let x = (i / 2) * xSpacing;
          let y = this.pos.y * p.segmentSpacing;
    
          if (p.mp) {
            let d = dist(x, y, (p.mouseX - p.windowWidth / 2 + p.windowWidth / 4 + 50) / 2, (p.mouseY - p.windowHeight / 2 + p.windowHeight / 4 + 250) / 2);
            if (d < 40) {
              let a = (8 / d) * 8;
              y += (a > 50) ? 50 : a;
            }
          }
    
          let col = p.img.get(x, y);
          let intensity = col[0] / 255;
    
          this.vertices[i + 0] = x;
          this.vertices[i + 1] = y - (intensity) * p.heightScale;
        }
    
      }
    
      draw() {
        p.strokeWeight(1);
    
        let waves = (p.sin(this.pos.y / 2.0 + this.t ) / p.PI) + 0.5;
        let vignette = p.sin(this.pos.y * p.segmentSpacing / p.img.height * p.PI);
        p.stroke(255, 255 * p.fadeIn * waves * vignette + 50 * vignette);
        // fill(0);
        p.noFill();
    
        p.beginShape();
        for (let i = 0; i < this.vertices.length; i += 2) {
          if (i === 0) {
            p.vertex(-1000, this.vertices[i + 1]);
          } else if (i + 2 === this.vertices.length) {
            p.vertex(10000, this.vertices[i + 1]);
          } else {
            p.vertex(this.vertices[i + 0], this.vertices[i + 1]);
    
            if (this.id < p.numSegments - 1) {
              let s = p.segments[p.id + 1];
              p.vertex(this.vertices[i + 0], this.vertices[i + 1]);
            }
          }
        }
        p.endShape();
      }
    }
  }
}

//This creates a constant for the Mifir land sketch and declares the variables required

const Mifir = function(p){
    p.scale = 20;
    p.cols;
    p.rows;
    p.w = 1400;
    p.h = 1000;

    p.flightPos = 0;
    // let flightSpeed = 0.08;
    // let noiseDelta = 0.16;
    p.terrain = [];
    // let terrainHeight = 112;

    p.Controls = function() {
        this.flightSpeed = 0.06;
        this.noiseDelta = 0.16;
        this.terrainHeight = 112;
    };

    p.controls = new p.Controls();

    p.setup =function() {
        // createCanvas(displayWidth, displayHeight, WEBGL);
        p.createCanvas(p.windowWidth, 720,p.WEBGL);

        let gui = new dat.GUI({width: 295});
        gui.close();
        gui.add(p.controls, 'flightSpeed', 0, 0.4).name("Flight speed").step(0.02);
        gui.add(p.controls, 'noiseDelta', 0.05, 0.4).name("Noise delta").step(0.01);
        gui.add(p.controls, 'terrainHeight', 0, 200).name("Terrain height").step(1);

        p.cols = p.w / p.scale;
        p.rows = p.h / p.scale;
        
        for (let x = 0; x < p.cols; ++x) {
            p.terrain[x] = [];
        }
    }

    p.draw = function() {
        p.flightPos -= p.controls.flightSpeed;
        p.shiftNoiseSpace();

        p.background(0);
        p.stroke(255);
        p.noFill();

        p.rotateX(p.PI / 3);
        p.translate((-p.w / 2) + 1, (-p.h / 2) + 30);

        for (let y = 0; y < p.rows - 1; ++y) {
            p.beginShape(p.TRIANGLE_STRIP);
            for (let x = 0; x < p.cols; ++x) {
                p.vertex(x * p.scale, y * p.scale, p.terrain[x][y]);
                p.vertex(x * p.scale, (y + 1) * p.scale, p.terrain[x][y + 1]);
            }
            p.endShape();
        }
    }

    p.shiftNoiseSpace = function(){
        let yOffset = p.flightPos;
        for (let y = 0; y < p.rows; ++y) {
            let xOffset = 0;
            for (let x = 0; x < p.cols; ++x) {
                p.terrain[x][y] = p.map(p.noise(xOffset, yOffset), 0, 1, -p.controls.terrainHeight, p.controls.terrainHeight);
                xOffset += p.controls.noiseDelta;
            }
            yOffset += p.controls.noiseDelta;
        }
    }
}

let sketches = new Map();

function GenerateSketch(imgPath,canvasID, sketchType)
{
    let sketch = GetSketch(imgPath,sketchType)
    let newSketch = new p5(sketch,canvasID);

    sketches.set(canvasID,newSketch);

	  return newSketch;
}

function GetSketch(ImgPath, SketchType)
{
    switch (SketchType) {
        case "UMA":
            return UMA(ImgPath);
            break;
        case "Mifir":
            return Mifir;
    }
}

function CheckDependeciesStatus()
{
    if(!window.setup.p5Loaded || typeof p5 !== 'function' || !window.setup.dotGUILoaded || typeof dat.GUI !== 'function')
        return false;
    else
        return true;
}

let ProcessDependecies = function(callback)
{
    console.log("Starting to create sketch!");
    
    if(CheckDependeciesStatus())
    {
      callback();
      return;
    }

    console.log("Dependecies are not loaded, waiting for them to laod...");
      
    let waitForDependecies = setInterval(() => {
      if(CheckDependeciesStatus())
      {
        console.log("Dependecies are loaded, creating sketch");
        clearInterval(waitForDependecies);
          
        callback();
      }
    }, 100);
}

//When the audience visits a new passage that calls an image and maps graphics onto it to create a sketch in the user's browser, this section deletes the previous one to prevent it running in the background, taking up valuable memory, and causing the game-play to lag

window.setup.DeleteAllSketches = ()=>{
  for (let value of sketches.values()){
    value.remove();
  }

  sketches.clear();
}

window.setup.RemoveSketch = (id)=>
{
    if(!sketches.has(id))
      return;

    sketches[id].remove();
    sketches.delete(id);
}

window.setup.CreateSketch = {
    UMA: (imgPath, canvasID) => ProcessDependecies(()=> {
        GenerateSketch(imgPath,canvasID,"UMA");
    }),
    Mifir: (canvasID) => ProcessDependecies(()=> {
        GenerateSketch(null,canvasID,"Mifir");
    }),
}



This is the code I would put in passages in which I wanted to execute the ‘Uma People’ sketch properties.

<div id="faceTheGuards"></div><script>window.setup.DeleteAllSketches();window.setup.CreateSketch.UMA("https://racompteur.com/public/images/guards.jpg", "faceTheGuards");</script>

Note: the passage name would need to be “faceTheGuards” and no two passages could have the same name. You’d also need to host images on a server and change the names accordingly.

And this is how I would display the ‘Mifir Land’ sketch:

<div id="powerfulEnemy"></div>
<script>
window.setup.DeleteAllSketches();
window.setup.CreateSketch.Mifir("powerfulEnemy");
</script>

Again, call your passage your equivalent of ‘powerfulEnemy’ here.

Hope this helps someone.