2

I've been trying to learn HTML5 canvas for a little while, and I've made a short script that allows for strings to be passed into an object which then displays it to a canvas. However, its performance is very inconsistent, and I've got a few ideas why.

When using fillRect, does it benefit performance to use beginPath and endPath? As far as I can tell, they're really only for lines.

More importantly, it looks like there's some serious memory leak issues, and I'm guessing that when garbage collection kicks in, that's when the program chugs. I've tried to read up on how javascript handles memory allocation, but to be honest, it's all kind of beyond me. I don't have any variable declarations inside loops (except at the start of for loops, but I tried changing that and I saw no improvement), but the object I have for drawing on the canvas is inside a function, so maybe it's making a bunch of new variables for each time that object's called?

In short, I don't know how to make javascript work faster and handle memory more intelligently.

Here's a link to the application itself.

Here's the code that takes a string and displays it to the canvas, followed by the html and js that creates the object and passes the display info into it:

/* screen.js */
function screen(id, pSize, w, h) {

  var pixelSize;
  var screenWidth;
  var screenHeight;
  var canvas;
  var context;
  var palette = new Array(16);

  palette = [
    "#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777",
    "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF"
  ];

  this.setScreenAttributes = function(pSize, w, h) {
    pixelSize = pSize;
    screenWidth = w;
    screenHeight = h;
    canvas.setAttribute("width", screenWidth * pixelSize);
    canvas.setAttribute("height", screenHeight * pixelSize);
    console.log("set screen attributes to " + screenWidth * pixelSize + "x" + screenHeight * pixelSize);
  };

  this.buildScreen = function(id, pSize, w, h) {
    canvas = document.getElementById(id);
    context = canvas.getContext("2d");
    this.setScreenAttributes(pSize, w, h);
    console.log("built screen " + id);
  };

  this.setPalette = function(pal) {
    if (pal.length == 16) {
      for (var i = 0; i < 16; i++)
        palette[i] = pal[i];
    } else
      console.log("error, palettes must contain 16 colors");
  };

  this.returnWidth = function() {
    return screenWidth;
  };

  this.returnHeight = function() {
    return screenHeight;
  };

  this.draw16 = function(str) {
    if (str.length == screenWidth * screenHeight) {
      context.clearRect(0, 0, screenWidth * pixelSize, screenHeight * pixelSize);
      for (var y = 0; y < screenHeight; y++) {
        for (var x = 0; x < screenWidth; x++) {
          switch (str.charAt((y * screenWidth) + x)) {
            case '0':
              context.fillStyle = palette[0];
              break;
            case '1':
              context.fillStyle = palette[1];
              break;
            case '2':
              context.fillStyle = palette[2];
              break;
            case '3':
              context.fillStyle = palette[3];
              break;
            case '4':
              context.fillStyle = palette[4];
              break;
            case '5':
              context.fillStyle = palette[5];
              break;
            case '6':
              context.fillStyle = palette[6];
              break;
            case '7':
              context.fillStyle = palette[7];
              break;
            case '8':
              context.fillStyle = palette[8];
              break;
            case '9':
              context.fillStyle = palette[9];
              break;
            case 'a':
            case 'A':
              context.fillStyle = palette[10];
              break;
            case 'b':
            case 'B':
              context.fillStyle = palette[11];
              break;
            case 'c':
            case 'C':
              context.fillStyle = palette[12];
              break;
            case 'd':
            case 'D':
              context.fillStyle = palette[13];
              break;
            case 'e':
            case 'E':
              context.fillStyle = palette[14];
              break;
            case 'f':
            case 'F':
              context.fillStyle = palette[15];
              break;
            default:
              rgba(255, 0, 0, 0);
              console.log("error, wrong character in string passed to draw16!");
              break;
          }
          context.fillRect(0 + pixelSize * x, 0 + pixelSize * y, pixelSize, pixelSize);
        }
      }
      //console.log("drew to screen");
    } else {
      console.log("incorrect length of string passed into draw16!");
      console.log("length is " + str.length + ", but should be " + (screenWidth * screenHeight) + "!");
    }
  };

  this.buildScreen(id, pSize, w, h); // constructor
}

/* inline-js */
// Palettes to pass to screen
var p0 = ["#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF"];
var p1 = ["#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000"];
var p2 = ["#222222", "#333333", "#444444", "#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111"];
var p3 = ["#333333", "#444444", "#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222"];
var p4 = ["#444444", "#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333"];
var p5 = ["#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444"];
var p6 = ["#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444", "#555555"];
var p7 = ["#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666"];
var p8 = ["#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777"];
var p9 = ["#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777", "#888888"];
var pA = ["#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777", "#888888", "#999999"];
var pB = ["#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA"];
var pC = ["#CCCCCC", "#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB"];
var pD = ["#DDDDDD", "#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC"];
var pE = ["#EEEEEE", "#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD"];
var pF = ["#FFFFFF", "#000000", "#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777", "#888888", "#999999", "#AAAAAA", "#BBBBBB", "#CCCCCC", "#DDDDDD", "#EEEEEE"];

var t;
var i = 0;
var running = true;
var command = '(x * y / 2) % 16';
var testStr = "";
var s = new screen("mycanvas", 5, 160, 100);

function drawTest() { // Creates the string
  testStr = "";
  for (var y = 0; y < s.returnHeight(); y++) {
    for (var x = 0; x < s.returnWidth(); x++) {
      switch (parseInt(eval(command))) {
        case 0:
          testStr = testStr.concat('0');
          break;
        case 1:
          testStr = testStr.concat('1');
          break;
        case 2:
          testStr = testStr.concat('2');
          break;
        case 3:
          testStr = testStr.concat('3');
          break;
        case 4:
          testStr = testStr.concat('4');
          break;
        case 5:
          testStr = testStr.concat('5');
          break;
        case 6:
          testStr = testStr.concat('6');
          break;
        case 7:
          testStr = testStr.concat('7');
          break;
        case 8:
          testStr = testStr.concat('8');
          break;
        case 9:
          testStr = testStr.concat('9');
          break;
        case 10:
          testStr = testStr.concat('A');
          break;
        case 11:
          testStr = testStr.concat('B');
          break;
        case 12:
          testStr = testStr.concat('C');
          break;
        case 13:
          testStr = testStr.concat('D');
          break;
        case 14:
          testStr = testStr.concat('E');
          break;
        case 15:
          testStr = testStr.concat('F');
          break;
        default:
          testStr = testStr.concat('X');
          console.log("incorrect entry in testStrPass: " + temp);
          break;
      }
    }
  }
};

function runTest() { // Cycles through palettes
  if (running == true) {
    if (i >= 16) {
      i = 0
    };
    switch (i) {
      case 0:
        s.setPalette(p0);
        break;
      case 1:
        s.setPalette(p1);
        break;
      case 2:
        s.setPalette(p2);
        break;
      case 3:
        s.setPalette(p3);
        break;
      case 4:
        s.setPalette(p4);
        break;
      case 5:
        s.setPalette(p5);
        break;
      case 6:
        s.setPalette(p6);
        break;
      case 7:
        s.setPalette(p7);
        break;
      case 8:
        s.setPalette(p8);
        break;
      case 9:
        s.setPalette(p9);
        break;
      case 10:
        s.setPalette(pA);
        break;
      case 11:
        s.setPalette(pB);
        break;
      case 12:
        s.setPalette(pC);
        break;
      case 13:
        s.setPalette(pD);
        break;
      case 14:
        s.setPalette(pE);
        break;
      case 15:
        s.setPalette(pF);
        break;
    }
    s.draw16(testStr); // Draws to screen
    i++;
    t = setTimeout(runTest, 16);
  }
};

function stop() {
  clearTimeout(t);
  running = false;
};

function restart() {
  if (running == false) {
    running = true;
    runTest();
  }
};

document.getElementById("stopButton").addEventListener("click", function() {
  stop()
});

document.getElementById("restartButton").addEventListener("click", function() {
  restart()
});

document.getElementById("evalButton").addEventListener("click", function() {
  command = document.getElementById('code').value;
  s.setScreenAttributes(document.getElementById('pSize').value, document.getElementById('x').value, document.getElementById('y').value);
  testStr = "";
  drawTest();
  restart()
});

document.addEventListener("DOMContentLoaded", function() {

  drawTest();
  runTest();

});
body{margin-bottom: 50vh;}
<canvas id="mycanvas" style="border: 1px solid #000000"></canvas>
<br />
<button id="stopButton">stop</button>
<button id="restartButton">restart</button>
<br /><br /> pixel size: <input type="text" id="pSize" size="2" value="5" /> x: <input type="text" id="x" size="2" value="160" /> y: <input type="text" id="y" size="2" value="100" />
<br />
<input type="text" id="code" value="(x * y / 2) % 16" /><button id="evalButton">evaluate</button>

For some reason the stackoverflow snippet thing says that the constructor isn't right, but it works in Chrome, Firefox, Edge, and iOS Safari, so I don't know what it thinks is wrong.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
cscx
  • 21
  • 1
  • What constructor ? – dustytrash Sep 18 '18 at 02:20
  • You should draw common images onto a 'cache canvas', then draw that canvas onto your visible canvas. Your canvas drawings will be the most expensive related: https://stackoverflow.com/questions/8205828/html5-canvas-performance-and-optimization-tips-tricks-and-coding-best-practices – dustytrash Sep 18 '18 at 02:24
  • @dustytrash At the bottom of screen, I have "this.buildScreen(id, pSize, w, h);", which as far as I can tell acts as a constructor for the screen object. – cscx Sep 18 '18 at 02:25
  • @cscx Consider using [es6 classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) until you're comfortable using functions to express methods, constructors, inheritance, etc., directly. This code does not use [prototypes](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes) and is unlikely your intent. – Rafael Sep 18 '18 at 02:25
  • @dustytrash could you please elaborate on what you mean by cache canvases? I looked through the link and didn't find anything like that. Though I think I might be doing that already- I have each pixel to be displayed represented by a character in a string, and then the screen object reads that string to determine what to print out. The idea being that in the future, anything that writes to the screen instead writes to the string, and only once it's time to display something does that string get passed to the screen object. – cscx Sep 18 '18 at 02:34
  • @Rafael Thanks! I didn't know about either classes or prototypes before, I'll definitely read up on those. I thought js was pretty much 100% functions. – cscx Sep 18 '18 at 02:35
  • I haven't looked too closely at your code, but for example if you were drawing a square very often, and to do so you were drawing 4 lines onto your canvas, you should instead draw the square onto an invisible (off screen) canvas. That way you can draw the canvas onto your main canvas instead of the 4 lines. – dustytrash Sep 18 '18 at 02:50
  • What you are asking is really broad... There are many things that could be done to improve the performances here, and you go asking questions that don't seem related to your code. Yes, *beginPath* is useless for with *fillRect*, because *fillRect* creates an out-of-current-drawing sub-path. But you don't use it in your code. (and *endPath* is doesn't exists...). Also note that a single sub-path generally renders better than multiple fillRect, (you'd have to merge these by fillStyle), [...] – Kaiido Sep 18 '18 at 03:14
  • [...] but for what you are doing, a single ImageData manipulation is actually probably the best call: You create a single ImageData, the size of one of the patterns, then put it, and draw it upscaled. You can see both ideas implemented in this [Q/A](https://stackoverflow.com/questions/52335088/strange-results-when-pre-rendering-vs-rendering-in-real-time-canvas/52341493#52341493). Also, an important thing regarding performances, is that you should replace your setTimeout loop with a requestAnimationFrame one. You can't be sure that setTimeout doesn't fire twice in the same frame. – Kaiido Sep 18 '18 at 03:18
  • LOL. I don't know why I think this is a joke. Must be a joke! Everything, every bit of code can be improved. @cscx is testing us all. – enxaneta Sep 18 '18 at 08:43

0 Answers0