skip to Main Content

The problem line is in the handleLineEnd function.
I ripped off all the lines not necessary to show the problem.

Without the line: I can draw multiple rectangles on the ctx.

With the line: Each rectangle is overwritten by the next rectangle.

The code runs fine in older Chrome (118.0.5993.71) vs not working in version 119.0.6045.106

function handleLineEnd(evt) {
  {
    startDrawing = false;
    var dummy = ctx.getImageData(3, 3, 5, 5); // problem line
    ctx.drawImage(canvaslayer, 0, 0);
    ctxlayer.clearRect(0, 0, el.width, el.height);
  }
}
var el = document.getElementById("canvasID");
var elcontainer = document.getElementById("containerID");
var ctx = el.getContext('2d');
var startpuntX, startpuntY, eindpuntX, eindpuntY;
var startDrawing = false;
var container = el.parentNode; // Add the temporary canvas.
var containerHandletje = document.getElementById('containerID');
var ongoingTouches = new Array();
var huidigeKleur = 'black';
var huidigeDikte = 2;
var selectie = document.getElementById('drawingtoolID');
elcontainer.setAttribute("style", 'width: ' + (window.innerWidth - 12) + 'px; height: ' + (window.innerHeight - 80) + 'px; overflow: auto;');
el.height = window.innerHeight - 90;
el.width = window.innerWidth - 42;
canvaslayer = document.createElement('canvas');
canvaslayer.id = 'imageTemp';
canvaslayer.width = el.width;
canvaslayer.height = el.height;
canvaslayer.style = 'touch-action:none;';
container.appendChild(canvaslayer);
ctxlayer = canvaslayer.getContext('2d');
ctxlayer.lineCap = 'round';
ctx.lineWidth = 1;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.fillStyle = 'white'
ctx.fillRect(0, 0, el.width, el.height);
ctx.lineWidth = huidigeDikte;
canvaslayer.addEventListener('pointerdown', handleLineStart, false);
canvaslayer.addEventListener('pointerup', handleLineEnd, false);
canvaslayer.addEventListener('pointermove', handleLineMove, false);

function handleLineStart(evt) {
  startpuntX = evt.clientX - el.offsetLeft + pageXOffset + containerHandletje.scrollLeft;
  startpuntY = evt.clientY - container.offsetTop + pageYOffset + containerHandletje.scrollTop;
  ctxlayer.lineWidth = huidigeDikte;
  ctxlayer.strokeStyle = huidigeKleur;
  ctxlayer.fillStyle = huidigeKleur;
  startDrawing = true;
  ctxlayer.stroke();
  ctxlayer.beginPath();
  handleLineMove(evt);
}

function handleLineMove(evt) {
  var x, y, w, h, hokDx, xi, yi, asX, asY, asXeind, asYeind, fontsize, dzx, dzy, situatie;
  if (startDrawing) {
    eindpuntX = evt.clientX - el.offsetLeft + pageXOffset + containerHandletje.scrollLeft;
    eindpuntY = evt.clientY - container.offsetTop + pageYOffset + containerHandletje.scrollTop;
    ctxlayer.clearRect(0, 0, el.width, el.height);
    ctxlayer.beginPath();
    ctxlayer.moveTo(startpuntX, startpuntY);
    x = Math.min(startpuntX, eindpuntX);
    y = Math.min(startpuntY, eindpuntY);
    w = Math.abs(startpuntX - eindpuntX);
    h = Math.abs(startpuntY - eindpuntY);
    ctxlayer.strokeRect(x, y, w, h);
    ctxlayer.stroke();
    ctxlayer.beginPath();
  }
}

function handleLineEnd(evt) {
  {
    startDrawing = false;
    var dummy = ctx.getImageData(3, 3, 5, 5); // problem line
    ctx.drawImage(canvaslayer, 0, 0);
    ctxlayer.clearRect(0, 0, el.width, el.height);
  }
}
#containerID {
  position: relative;
}

#canvasID {
  border: 1px solid #000;
}

#imageTemp {
  position: absolute;
  top: 1px;
  left: 11px;
}
<div id="containerID" style="touch-action:none; width: 100px; height: 500px; overflow: auto;">
  <canvas id='canvasID' width='80' height='80' style='margin-left:10px;background:white;border:solid black 1px; touch-action:none; '>
    Your browser does not support canvas element.
  </canvas>
</div>

2

Answers


  1. I get a feeling you are reinventing the wheel here, there are libraries that will make all that and much more, and your code will look a lot simpler, for example http://fabricjs.com/ that will allow you to add shapes and give the user control of location, rotation and dimensions, you can also group shapes or make it not selectable.

    Look at my sample below, I added 3 rectangles to a group and added an event listener, double click will add a new rectangle that user can modify later.

    canvas = new fabric.Canvas("a")
    
    objs = []
    objs.push(new fabric.Rect({left: 50, top: 50, height: 50, width: 50, fill: "red" }))
    objs.push(new fabric.Rect({left: 90, top: 60, height: 50, width: 50, fill: "green"}))
    objs.push(new fabric.Rect({left: 70, top: 80, height: 50, width: 50, fill: "blue" }))
    
    group = new fabric.Group(objs, { left: 30, top: 0, opacity: 0.8, selectable: false })
    canvas.add(group)
    
    canvas.add(new fabric.Rect({left: 400, top: 40, height: 20, width: 80 }))
    canvas.renderAll()
    
    canvas.on('mouse:dblclick', (evt) => {
        rect = new fabric.Rect({
          left: evt.pointer.x, 
          top: evt.pointer.y, 
          height: 40, 
          width: 40, 
          stroke: "black", 
          fill: "" 
        })
        canvas.add(rect)
        canvas.setActiveObject(rect);
    })
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
    <canvas id="a" width="600" height="160"></canvas>
    Login or Signup to reply.
  2. This is a known and fixed Chrome bug (CRBUG 1500272 and CRBUG 1499539) that was caused by this CL. Basically they’re rewriting their canvas layer logic and this messed up with when these get flushed. Roughly, calling getImageData() does move the layer around from the GPU to the CPU and that change made the bridge not handle that moving correctly, leaving the GPU side without a resource provider.

    Note that we can reduce the bug repro down to:

    const el = document.querySelector("canvas");
    const ctx = el.getContext('2d');
    const canvaslayer = document.createElement('canvas');
    const ctxlayer = canvaslayer.getContext('2d');
    
    const draw = (pos) => {
      ctxlayer.fillRect(pos, pos, 30, 30);
      ctx.getImageData(3, 3, 5, 5); // problem line
      ctx.drawImage(canvaslayer, 0, 0);
      ctxlayer.clearRect(0, 0, el.width, el.height);
    };
    
    draw(30);
    // We need to wait at least  after the next painting frame
    // But 1s makes it clearer somethign is wrong.
    setTimeout(() => draw(60), 1000);
    <canvas></canvas>

    Of possible workarounds:

    Do not use an accelerated canvas. By requesting your 2D context with the willReadFrequently option, you’ll avoid your canvas is moved around. If you are going to do many readbacks on your canvas, you may really want to consider this option, even without the bug. Moving the canvas around is very slow and the little you may have won through hardware acceleration (which might not even be much depending on what you draw), will be lost in these calls.

    const el = document.querySelector("canvas");
    // Since we're going to do read-backs on this context
    // let's deaccelerate it.
    const ctx = el.getContext('2d', { willReadFrequently: true });
    const canvaslayer = document.createElement('canvas');
    const ctxlayer = canvaslayer.getContext('2d');
    
    const draw = (pos) => {
      ctxlayer.fillRect(pos, pos, 30, 30);
      ctx.getImageData(3, 3, 5, 5); // problem line
      ctx.drawImage(canvaslayer, 0, 0);
      ctxlayer.clearRect(0, 0, el.width, el.height);
    };
    
    draw(30);
    setTimeout(() => draw(60), 1000);
    <canvas></canvas>

    If you really think you must use an accelerated canvas, then you could use an OffscreenCanvas instead of the canvaslayer <canvas> element. OffscreenCanvas aren’t causing that bug (for whatever reasons…).

    const el = document.querySelector("canvas");
    const ctx = el.getContext('2d');
    const canvaslayer = new OffscreenCanvas(300, 300);
    const ctxlayer = canvaslayer.getContext('2d');
    
    let pos = false;
    const draw = (pos) => {
      ctxlayer.fillRect(pos, pos, 30, 30);
      ctx.getImageData(3, 3, 5, 5); // problem line
      ctx.drawImage(canvaslayer, 0, 0);
      ctxlayer.clearRect(0, 0, el.width, el.height);
    };
    
    draw(30);
    setTimeout(() => draw(60), 1000);
    <canvas></canvas>

    From your code though, since you need the layercanvas to be visible, you’d use layercanvas.transferControlToOffscreen() instead of the constructor, so that the <canvas> element becomes a placeholder canvas and still paints its content to screen.

    var el = document.getElementById("canvasID");
    var elcontainer = document.getElementById("containerID");
    var ctx = el.getContext('2d');
    var startpuntX, startpuntY, eindpuntX, eindpuntY;
    var startDrawing = false;
    var container = el.parentNode; // Add the temporary canvas.
    var containerHandletje = document.getElementById('containerID');
    var ongoingTouches = new Array();
    var huidigeKleur = 'black';
    var huidigeDikte = 2;
    var selectie = document.getElementById('drawingtoolID');
    elcontainer.setAttribute("style", 'width: ' + (window.innerWidth - 12) + 'px; height: ' + (window.innerHeight - 80) + 'px; overflow: auto;');
    el.height = window.innerHeight - 90;
    el.width = window.innerWidth - 42;
    const canvaslayerEl = document.createElement('canvas');
    canvaslayerEl.id = 'imageTemp';
    canvaslayerEl.width = el.width;
    canvaslayerEl.height = el.height;
    canvaslayerEl.style = 'touch-action:none;';
    container.appendChild(canvaslayerEl);
    // Use an OffscreenCanvas to avoid Chrome bug 1500272
    const canvaslayer = canvaslayerEl.transferControlToOffscreen();
    const ctxlayer = canvaslayer.getContext('2d');
    ctxlayer.lineCap = 'round';
    ctx.lineWidth = 1;
    ctx.lineJoin = 'round';
    ctx.lineCap = 'round';
    ctx.fillStyle = 'white'
    ctx.fillRect(0, 0, el.width, el.height);
    ctx.lineWidth = huidigeDikte;
    canvaslayerEl.addEventListener('pointerdown', handleLineStart, false);
    canvaslayerEl.addEventListener('pointerup', handleLineEnd, false);
    canvaslayerEl.addEventListener('pointermove', handleLineMove, false);
    
    function handleLineStart(evt) {
      startpuntX = evt.clientX - el.offsetLeft + pageXOffset + containerHandletje.scrollLeft;
      startpuntY = evt.clientY - container.offsetTop + pageYOffset + containerHandletje.scrollTop;
      ctxlayer.lineWidth = huidigeDikte;
      ctxlayer.strokeStyle = huidigeKleur;
      ctxlayer.fillStyle = huidigeKleur;
      startDrawing = true;
      ctxlayer.stroke();
      ctxlayer.beginPath();
      handleLineMove(evt);
    }
    
    function handleLineMove(evt) {
      var x, y, w, h, hokDx, xi, yi, asX, asY, asXeind, asYeind, fontsize, dzx, dzy, situatie;
      if (startDrawing) {
        eindpuntX = evt.clientX - el.offsetLeft + pageXOffset + containerHandletje.scrollLeft;
        eindpuntY = evt.clientY - container.offsetTop + pageYOffset + containerHandletje.scrollTop;
        ctxlayer.clearRect(0, 0, el.width, el.height);
        ctxlayer.beginPath();
        ctxlayer.moveTo(startpuntX, startpuntY);
        x = Math.min(startpuntX, eindpuntX);
        y = Math.min(startpuntY, eindpuntY);
        w = Math.abs(startpuntX - eindpuntX);
        h = Math.abs(startpuntY - eindpuntY);
        ctxlayer.strokeRect(x, y, w, h);
        ctxlayer.stroke();
        ctxlayer.beginPath();
      }
    }
    
    function handleLineEnd(evt) {
      {
        startDrawing = false;
        var dummy = ctx.getImageData(3, 3, 5, 5); // problem line
        ctx.drawImage(canvaslayer, 0, 0);
        ctxlayer.clearRect(0, 0, el.width, el.height);
      }
    }
    #containerID {
      position: relative;
    }
    
    #canvasID {
      border: 1px solid #000;
    }
    
    #imageTemp {
      position: absolute;
      top: 1px;
      left: 11px;
    }
    <div id="containerID" style="touch-action:none; width: 100px; height: 500px; overflow: auto;">
      <canvas id='canvasID' width='80' height='80' style='margin-left:10px;background:white;border:solid black 1px; touch-action:none; '>
        Your browser does not support canvas element.
      </canvas>
    </div>

    But that’s an ugly hack-around, for a bug that’s already been fixed and which should disappear in the next Chrome release (current Chrome Beta has the fix already).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search