Canvas create an image mask that is transparent

2019-06-08 13:54发布

问题:

I have a canvas where the user can draw with a brush tool I created (on mousedown or touch). I would like the drawing area to be restricted by a shape on the canvas, for example, the shape of a Chinese character.

The shape is not a simple shape, so using CanvasRenderingContext2D.clip() is not really an option?

My next solution is to draw the masking image as a PNG with drawImage onto the canvas, then change the CanvasRenderingContext2D.globalCompositeOperation to 'source-atop', so that the brush is drawn within the masking image only.

Here is the requirement which I am stuck at - In the final output of the canvas, I do not want to see the masking image.

I could accept setting the masking image to a very very low alpha such that is is virtually invisible.

Therefore, I tried to set the global alpha before placing the image, then restoring it when the brush starts to draw.

However, the overlap drawn also has the same alpha as the masking image, but I would like it to be at full.

Is there another way to achieve this I am not aware of?

回答1:

@markE's solution is fine for a small set of predefined characters, that you could work on before.

Unfortunately, while texts are vector drawing operations, the only two methods in canvas Context2d are fillText and strokeText, which doesn't allow us to include texts in a Path2D object, needed by the clip() method.

So if you can't do this pre-processing, I think that globalCompositeOperation is a good alternative for clip(). This will involve the use of 2 canvases, one for the user's drawings, and the other one for the clipping/rendering. (Here I even use a third one for the clipping-mask, but it could also just be an <img> tag with a raster png with transparency).

source-in mode of the globalCompositeOperation will only draw the newly drawn pixels where there were already pixels drawn. So the first shape (the clipping mask) won't be visible.

var ctx = canvas.getContext('2d');

// create a buffer canvas for the paintings
var paint = canvas.cloneNode(true);
var paint_ctx = paint.getContext('2d');

// create another buffer canvas for the clipping-mask
// (it could also be a raster png in an <img> tag
var clip_mask = canvas.cloneNode(true);
var clip_ctx = clip_mask.getContext('2d');

var clip = function(){
  // clear the visible canvas
  ctx.clearRect(0,0,canvas.width, canvas.height);
  // draw the clipping mask
  ctx.drawImage(clip_mask, 0,0);
  // change the gCO
  ctx.globalCompositeOperation = "source-in";
  // draw the user paintings
  ctx.drawImage(paint, 0,0);
  // always reset the default gCO
  ctx.globalCompositeOperation = "source-over";

  // show the stroke of the clipping area
  if(stroke.checked){
    strokeMask();
    }
  };

//
// The user painting methods
//

var doPaint = function(x, y){
  paint_ctx.beginPath();
  paint_ctx.arc(x-3.5, y-3.5, 7, Math.PI, -Math.PI);
  paint_ctx.fill();
  clip();
  };


canvas.onmousemove = function(evt){
  var rect = this.getBoundingClientRect();
  var x = evt.clientX-rect.left;
  var y = evt.clientY-rect.top;
  doPaint(x,y);
  };

//
// the clipping mask methods
//

// init the clipping-mask
var initClip = function(){
  clip_ctx.font = "150px sans-serif";
  clip_ctx.textBaseline = "top";
  clip_ctx.textAlign = "center";
  updateClip();
  };

// update the clipping-mask
var updateClip = function(){
  var val = char.value;
  clip_ctx.clearRect(0, 0, clip_mask.width, clip_mask.height);
  var x = (clip_mask.width/2);
  clip_ctx.fillText(val, x, 10);
  paint_ctx.clearRect(0, 0, paint.width, paint.height);
  clip();
  };

// listen to the text input
char.oninput = char.onchange = function(){
  var val = this.value;
  // restrict to 1 character
  if(val.length>1){
    this.value = val[val.length-1];
    }
  updateClip();
  };

// show the stroke of the mask
var strokeMask = function(){
  ctx.font = "150px sans-serif";
  ctx.textBaseline = "top";
  ctx.textAlign = "center";
  ctx.strokeStyle = "rgba(255,255,255,.3)";
  ctx.strokeText(char.value, canvas.width/2, 10);
  }
stroke.onchange = clip;

// lets go!
initClip();
body{background-color: skyblue;}
canvas{border:1px solid;}
input{max-width:1em;}
span{font-size:.7em;}
<input type="text" id="char" value="试"/>
<span>show the clipping-stroke</span><input type="checkbox" id="stroke" name="s"/><br>
<canvas id="canvas" width="200" height="150"></canvas>



回答2:

How to create an "invisible" mask If you want to avoid your masks from showing you can set your css background to white and fill your masks with white. That way the masks will be "invisible". If you need transparency, then when all drawing is done you can use context.getImageData to make all surviving white pixels transparent.

But... Compositing affects only existing vs new drawings, so the problem is if your app incrementally adds pixels, you will have to do quite a bit of saving + redrawing to use compositing to restrict pixels. That's because previously new pixels from the last draw will become existing pixels during the current compositing cycle.

So... Take another look at context.clip which restricts new drawings (brush strokes) into a defined clipping path. It's not that difficult to reduce even complex shapes to canvas paths. If you have a large volume of characters to make paths from, then consider Adobe Illustrator plus Mike Swanson's plugin that exports svg paths as canvas paths.