Search code examples
javascripthtml5-canvas

HTML5 Canvas stroke() changing color between draws


I've created a very simple JavaScript snippet to illustrate a weird HTML5 canvas behavior that I have been experiencing.

I keep drawing the same set of strokes, just in a different order, every 100ms. Why is it that the color of some of the strokes keeps changing? It only happens when I shuffle the draw order between calls, even though the lines are drawn in the same location and same color each frame.

const canvasWidth = 500;
const gapBetweenLines = 5;
const nbrLines = canvasWidth / gapBetweenLines;
const canvasHeight = 500;

const canvas = document.getElementById('map');
canvas.width = canvasWidth;
canvas.height = canvasHeight;

// create an array of line objects, each with a with random color
let lines = [];
for (let i = 0; i < nbrLines; i++) {
  lines.push({
    index: i,
    x: i * gapBetweenLines,
    color: '#' + Math.floor(Math.random() * 16777215).toString(16)
  });
}

// function to shuffle the given array in place
function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}

// draw lines on the canvas at specific intervals with the random colors
function drawLines() {
  const shuffledLines = [...lines];
  shuffle(shuffledLines);

  let ctx = canvas.getContext('2d');
  for (let i = 0; i < nbrLines; i++) {
    const line = shuffledLines[i];
    ctx.strokeStyle = line.color;
    // ctx.save();
    ctx.beginPath();
    ctx.moveTo(line.x, 0);
    ctx.lineTo(line.x, canvasHeight);
    ctx.stroke();
    // ctx.restore();
  }
}

// call the drawLines function every 100ms
setInterval(drawLines, 100);
<!DOCTYPE html>
<html>
  <body>
    <h1>Flickering Lines</h1>
    <canvas id="map"></canvas>
    <div id="lineinfo"></div>
  </body>
</html>

The past day had me spending an embarrassing amount of time trying to narrow down the cause of this. Is there something about HTML5 Canvas drawing that I simply misunderstand?

Saving and restoring the context between each stroke does not make any difference.


Solution

  • The problem is in your color generation.

    color: '#' + Math.floor(Math.random() * 16777215).toString(16)
    

    Number#toString(16) does not pad the returned string with zeroes:

    console.log(12..toString(16)) // "c", not "0C"

    This means that in your code, some of your lines may have their color property set to values that aren't a valid HEX format (e.g a five or two chars HEX is invalid).

    To fix that, you can force your color generator to be always 6 length by padding as many zeroes as needed using the String#padStart() method.

    const canvasWidth = 500;
    const gapBetweenLines = 5;
    const nbrLines = canvasWidth / gapBetweenLines;
    const canvasHeight = 500;
    
    const canvas = document.getElementById('map');
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    
    // create an array of line objects, each with a with random color
    let lines = [];
    for (let i = 0; i < nbrLines; i++) {
      lines.push({
        index: i,
        x: i * gapBetweenLines,
        color: '#' + Math.floor(Math.random() * 16777215).toString(16)
          // force always 6 length
          .padStart(6, "0")
      });
    }
    
    // function to shuffle the given array in place
    function shuffle(array) {
      for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
      }
    }
    
    // draw lines on the canvas at specific intervals with the random colors
    function drawLines() {
      const shuffledLines = [...lines];
      shuffle(shuffledLines);
    
      let ctx = canvas.getContext('2d');
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      for (let i = 0; i < nbrLines; i++) {
        const line = shuffledLines[i];
        ctx.strokeStyle = line.color;
        // ctx.save();
        ctx.beginPath();
        ctx.moveTo(line.x, 0);
        ctx.lineTo(line.x, canvasHeight);
        ctx.stroke();
        // ctx.restore();
      }
    }
    
    // call the drawLines function every 100ms
    setInterval(drawLines, 100);
    <!DOCTYPE html>
    <html>
      <body>
        <h1>Flickering Lines</h1>
        <canvas id="map"></canvas>
        <div id="lineinfo"></div>
      </body>
    </html>