Trouble implementing JS from Codepen in Twine Sugarcube 2.34.1

Twine Version: 2.3.13
Story Format: 2.34.1

Hey all! first post here, and very green when it comes to all things Twine/code. I want to take this piece of javascript/CSS and have it be the background to a passage (JS/CSS copied below). I’ve put the CSS in my Story Stylesheet, the JS in my story Javascript, and the HTML content into the desired passage. Every time I try and play my story I get this error:

Error: [tw-user-script-0] bad evaluation: Cannot read properties of null (reading 'getContext')

I’m not sure what could be going wrong here as the script seems to run fine on the Codepen site.
Any help would be appreciated. Let me know if there’s anything else I can provide.

HTML:

<html>
  <head />
  <body>
    <div id="container">
      <div id="canvas">
        <canvas id="maincanvas" width="200" height="200" />
      </div>
      <div id="panel">
        <button id="startstop">Start/Stop</button>
        <button id="reset">Reset</button>
        <div>Frames: <div id="counter">0</div></div>
      </div>
    </div>
  </body>
</html>

CSS

body
{
  background-color: rgb(29,31,32);
  color: white;
  font-weight: normal;
  font-family: arial;
  font-size: 0.9em;
}

#container
{
  margin: 0 auto;
  width: 500px;
}

#canvas
{
  width: 200px;
  height: 200px;
  background-color: rgb(32,34,35);
}

#panel
{
  width: 180px;
  margin-top: 10px;
}

button
{
  color: white;
  background-color: rgb(29,31,32);
  border: 1px solid grey;
  border-radius: 10px;
  height: 20px;
  min-width: 80px;
  display: inline-block;
  margin-bottom: 10px;
}

button:active
{
  border: 1px solid white;
}

button:focus
{
  outline: 0px;
}

#counter
{
  display: inline-block;
}

Javascript:

// get canvas drawing context
var canvas = document.getElementById("maincanvas");
var context = canvas.getContext('2d');

// dimensions of simulation grid are taken from canvas
var dimX = canvas.offsetWidth;
var dimY = canvas.offsetHeight;

// get the pixel data from the canvas for faster rendering
var pixelData = context.getImageData(0, 0, dimX, dimY);

// variables to handle the start/stop/reset state of the simulation
var isRunning = false;
var theSim = null;
var currentCount = 0;
var dt = 40;

// function to perform an iteration of the algorithm, draw
// the results and schedule another iteration
var draw = function()
{
  theSim.draw();

  currentCount++;
  $('#counter').html(currentCount);

  if (isRunning) {
    setTimeout(function() { draw(); }, dt);
  }
};

var reset = function()
{
  theSim.reset();
  currentCount = 0;
};

// add button handlers
$('#startstop').click(function() {
  if (!isRunning) {
    isRunning = true;
    draw();
  }
  else {
    isRunning = false;
  }
});

$('#reset').click(reset);

// main class for the simulation data and algorithm
var simulation = function()
{
  var createArray = function()
  {
    var array = new Array(dimX);
    for (var i = 0; i < dimX; i++)
    {
      array[i] = new Array(dimY);
    }
    return array;
  };

  // arrays for the values of A, B and C
  var a = [createArray(), createArray()];
  var b = [createArray(), createArray()];
  var c = [createArray(), createArray()];

  // the above arrays connsist of 2 buffers that are
  // flipped over after each iteration - one buffer contains
  // the input data to the algorithm and the other contains
  // the output at the end of the iteration
  var readBuffer = 0;
  var writeBuffer = 1;
  var resetOnNextDraw = false;

  this.reset = function()
  {
    resetOnNextDraw = true;
  };

  var doReset = function()
  {
    for (var x = 0; x < dimX; x++)
    {
      for (var y = 0; y < dimY; y++)
      {
        a[readBuffer][x][y] = Math.random();
        b[readBuffer][x][y] = Math.random();
        c[readBuffer][x][y] = Math.random();
      }
    }
  };
  doReset();

  // BIZARRE!!! If this empty loop is removed, performance decreases dramatically!!!
  for (var asd = 0; asd < 0; asd++)
  {
  }

  // function to perform 1 iteration of the algorithm and render the results to canvas
  this.draw = function()
  {
    if (resetOnNextDraw)
    {
      doReset();
      resetOnNextDraw = false;
    }

    var aRead = a[readBuffer];
    var bRead = b[readBuffer];
    var cRead = c[readBuffer];

    var aWrite = a[writeBuffer];
    var bWrite = b[writeBuffer];
    var cWrite = c[writeBuffer];

    var xPlus, xMinus, yPlus, yMinus;
    var aVal, bVal, cVal;
    var aValNew, bValNew, cValNew;
    var pixelIndex, x, y;
    var pixelArray = pixelData.data;

    for (x = 0; x < dimX; x++)
    {
      xMinus = (x === 0) ? dimX - 1 : x - 1;
      xPlus = (x === dimX - 1) ? 0 : x + 1;

      var aReadxMinus = aRead[xMinus];
      var bReadxMinus = bRead[xMinus];
      var cReadxMinus = cRead[xMinus];
      var aReadx = aRead[x];
      var bReadx = bRead[x];
      var cReadx = cRead[x];
      var aReadxPlus = aRead[xPlus];
      var bReadxPlus = bRead[xPlus];
      var cReadxPlus = cRead[xPlus];
      
      for (y = 0; y < dimY; y++)
      {
        yMinus = (y === 0) ? dimY - 1 : y - 1;
        yPlus = (y === dimY - 1) ? 0 : y + 1;

        aVal = aReadxMinus[yMinus]
             + aReadxMinus[y]
             + aReadxMinus[yPlus]
             + aReadx[yMinus]
             + aReadx[y]
             + aReadx[yPlus]
             + aReadxPlus[yMinus]
             + aReadxPlus[y]
             + aReadxPlus[yPlus];

        bVal = bReadxMinus[yMinus]
             + bReadxMinus[y]
             + bReadxMinus[yPlus]
             + bReadx[yMinus]
             + bReadx[y]
             + bReadx[yPlus]
             + bReadxPlus[yMinus]
             + bReadxPlus[y]
             + bReadxPlus[yPlus];

        cVal = cReadxMinus[yMinus]
             + cReadxMinus[y]
             + cReadxMinus[yPlus]
             + cReadx[yMinus]
             + cReadx[y]
             + cReadx[yPlus]
             + cReadxPlus[yMinus]
             + cReadxPlus[y]
             + cReadxPlus[yPlus];

        aVal *= 0.111111111;
        bVal *= 0.111111111;
        cVal *= 0.111111111;

        aValNew = aVal*(1.0 + bVal - cVal);
        bValNew = bVal*(1.0 + cVal - aVal);
        cValNew = cVal*(1.0 + aVal - bVal);

        aValNew = Math.min(1.0, Math.max(aValNew, 0.0));
        aWrite[x][y] = aValNew;
        bWrite[x][y] = Math.min(1.0, Math.max(bValNew, 0.0));
        cWrite[x][y] = Math.min(1.0, Math.max(cValNew, 0.0));

        pixelIndex = (y * dimX + x) * 4;
        
        var r1 = 32, g1 = 34, b1 = 35;
        var r2 = 81, g2 = 226, b2 = 65;

        pixelArray[pixelIndex] = r1 - Math.floor(aValNew * (r1 - r2));
        pixelArray[pixelIndex + 1] = g1 - Math.floor(aValNew * (g1 - g2));
        pixelArray[pixelIndex + 2] = b1 - Math.floor(aValNew * (b1 - b2));
        pixelArray[pixelIndex + 3] = 255;
      }
    }

    context.putImageData(pixelData, 0, 0);

    if (readBuffer === 0)
    {
      readBuffer = 1;
      writeBuffer = 0;
    }
    else
    {
      readBuffer = 0;
      writeBuffer = 1;
    }
  };
};

theSim = new simulation();



I’m pretty sure you shouldn’t declare html in a passage, as the whole file is already an html.
That’s probably the same with body, though I’m less certain.

1 Like

@souppilouliouma is right. You should not include htlm and body tag. The html you write in the passage is going to be injected into an existing html page when the game is compiled.

But that’s not your problem right now. I don’t know how sugarcube works exactly. But what is happening is that you’re trying to get the element “maincanvas” before it exists on the page.

1 Like

As long as this isn’t in the first passage in the game, it’s fairly easy to tweak this to work in Twine/SugarCube.

First, copy the CSS into your game’s Stylesheet section. I’d remove the “body { ... }” part of the CSS, since it’s not really needed. Additionally, you don’t want to overwrite the default SugarCube button look, so change all of the “button” CSS to target “button.cust” instead, that way the CSS only affects buttons with the “cust” class. You’ll also need to add “padding: 0;” to the “button.cust” CSS. (Note: You may need to tweak the CSS a bit more to get it to look exactly how you want, since SugarCube modifies the CSS itself as well.)

Next, in the passage where this will appear (again, not the first passage, otherwise it requires further tweaking), put the HTML found inside the <body> element above within a <<nobr>> macro instead (to prevent extra line breaks). You’ll also need to add “ class="cust"” to each of the <button> elements there so that the above CSS will be applied to them.

Then, in that same passage right after the <</nobr>>, put the JavaScript within this:

<<script>>
	$(document).one(":passageend", function (event) {
		...JavaScript goes here...
	});
<</script>>

You have to do it that way, because SugarCube renders passages to a “virtual” context first, then adds that to the document, and this technique causes the JavaScript code in the :passageend event handler to wait until the current passage is added to the document before it executes. If you don’t use that technique, then the code won’t be able to find the HTML elements it’s trying to use, because they’re not yet part of the document at the time that that code executes. (See the :passageend event and the jQuery .one() method for details.)

Enjoy! :slight_smile:

1 Like

You’re a legend– this got it working. Thanks so much