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?
@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>
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.