Set allowed drawing area in JavaScript canvas API

Viewed 128

I'm using the JavaScript canvas API for free drawing. I'm stuck at masking the area that is allowed to be drawn on - in my example it should only be the speechbubble area. I'm using this Vue component: https://github.com/sametaylak/vue-draw/blob/master/src/components/CanvasDraw.vue

draw(event) {
  this.drawCursor(event);
  if (!this.isDrawing) return;
  if (this.tools[this.selectedToolIdx].name === 'Eraser') {
    this.canvasContext.globalCompositeOperation = 'destination-out';
  } else {
    this.canvasContext.globalCompositeOperation = 'source-over';
    this.canvasContext.strokeStyle = this.tools[this.selectedToolIdx].color;
  }
  this.canvasContext.beginPath();
  this.canvasContext.moveTo(this.lastX, this.lastY);
  this.canvasContext.lineTo(event.offsetX, event.offsetY);
  this.canvasContext.stroke();
  [this.lastX, this.lastY] = [event.offsetX, event.offsetY];
},
drawCursor(event) {
  this.cursorContext.beginPath();
  this.cursorContext.ellipse(
    event.offsetX, event.offsetY,
    this.brushSize, this.brushSize,
    Math.PI / 4, 0, 2 * Math.PI
  );
  this.cursorContext.stroke();
  setTimeout(() => {
    this.cursorContext.clearRect(0, 0, this.width, this.height);
  }, 100);
},

2 Answers

There is a built-in clip() method which sets a path as the clipping region.

var ctx=document.getElementById("cnv").getContext("2d");
ctx.lineWidth=2;

ctx.strokeStyle="red";
ctx.moveTo(0,0);
ctx.lineTo(100,100);
ctx.stroke();                // 1.

ctx.strokeStyle="black";
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(100,10);
ctx.lineTo(100,60);
ctx.lineTo(30,60);
ctx.lineTo(10,80);
ctx.closePath();
ctx.stroke();                // 2.
ctx.clip();                  // 3.

ctx.strokeStyle="green";
ctx.beginPath();
ctx.moveTo(0,100);
ctx.lineTo(100,0);
ctx.stroke();                // 4.
<canvas id="cnv"></canvas>

  1. red line is drawn between 0,0 and 100,100, without clipping
  2. bubble is drawn in black
  3. bubble is set as clipping region
  4. green line is drawn between 0,100 and 100,0, and correctly clipped into the bubble.

In practice you may want to have the clipping region one pixel inside the bubble, so a separate path (which is not stroke()-d, just clip()-ped), so drawing can not modify the bubble itself. If you zoom in now as it is, you will see that the green line actually overdraws the inner pixels of the bubble (linewidth is 2 pixels, and the outer one is "unharmed").

Establishing if a given point belongs to polygon's area is a quite tricky and solved problem in Computer Science.

In this concrete scenario, where you have canvas with above image set as background and 500x300 dimension you don't really need to use ray casting algorithm.
You can for example divide speech bubble area to a rectangle and a triangle, and then using event.offsetX, event.offsetY check if any given point lies inside of any of these two figures.

Code example:

 isPointInArea(event) {
      const x = event.offsetX;
      const y = event.offsetY;
       // For rectangle it is straightforward
      if (x >= 60 && x <= 325 && y >= 60 && y <= 215) { 
          return true;
      }
      /* Since two sides of this triangle are parallel to canvas
         It is enough to check y coordinate with one linear function of a third one
         in form of y = ax + b */
      if(x >= 60 && x <= 120 && y >= 215) {
        const boundaryY = -0.81818181818 * x + 313.181818182;
        if (y <= boundaryY) {
          return true;
        }
      }
       return false;
    }

In draw function of CanvasDraw.vue

draw(event) {
     if(!this.isPointInArea(event)) {
       return;
     }

     this.drawCursor(event);
     if (!this.isDrawing) return;
     ...

Working example on codesandbox

Result: Result gif

Edit: As pointed out by @tevemadar you can also use simply clip() method of Canvas API. If there is nothing else you want to render (And since it is not a game, so probably that is the case), then you can just execute clip() once and you are all set. Otherwise remember to use save() method (and then of course restore(), so that you can render stuff also outside of speech bubble clipping region.

Related