I'm trying to figure out a way to punch holes into a thing, but without the hole also going through whatever is in the background already.
the hole is made of a few arbitrary shapes, and is not a simple path I can use to clip.
The hole is only punched through the foreground shapes, and not all the way into the background (the background should stay as-is).
I figured a way to do this with an external context, and then bringing it in.
My questions: is there a way to do it on my default canvas, and avoid the complications that might arise from the external context (extra memory, color differences etc)?
Here's a working (p5.js) example, which is using a new context:
function setup() {
createCanvas(600,600);
background(255, 0, 0);
noStroke();
}
function draw() {
//blue: stuff in the background that should not change
fill ("blue");
rect (20,20,500,500);
//draw on external canvas
pg = createGraphics(600,600);
//yellow+green foreground shapes
pg.fill("green");
pg.rect(100, 100, 200, 200);
pg.fill("yellow");
pg.rect(80, 80, 100, 300);
//punch a hole in the shapes
pg.fill(0, 0, 255);
pg.blendMode(REMOVE);
pg.circle(140, 140, 150);
pg.circle(180, 180, 150);
//bring in the external canvas with punched shapes
image(pg, 0, 0);
noLoop();
}
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
There is no easy or built in way to do this without the technique you've already discovered. The only alternative would be to implement boolean geometry operations like subtraction and intersection on arbitrary shapes and splines. That would allow you to make arbitrary bezier splines that represent the composites of multiple complex shapes and then draw those directly. This approach would have different behavior with regards to stroke than the removal approach.
Just FYI, there are also a pair of methods in p5js erase() and noErase() which have a similar behavior to the blendMode(REMOVE) approach. I don't think there's any technical benefit, but it might be more idiomatic to use them rather than blend mode.
I agree, as Paul(+1) mentions as well, using multiple p5.Graphics instances (external contexts as you call them) is the most straight forward/readable method.
You could explicitly uses p5.Image and mask(), however there are few more operations involved and the could would be a little less readable. Here's an example:
function setup() {
createCanvas(600,600);
background(255, 0, 0);
noStroke();
}
function draw() {
//blue: stuff in the background that should not change
fill ("blue");
rect (20,20,500,500);
//draw on external canvas
pg = createGraphics(600,600);
//yellow+green foreground shapes
pg.fill("green");
pg.rect(100, 100, 200, 200);
pg.fill("yellow");
pg.rect(80, 80, 100, 300);
//punch a hole in the shapes
let msk = createGraphics(600, 600);
msk.background(0);
msk.erase();
msk.noStroke();
msk.circle(140, 140, 150);
msk.circle(180, 180, 150);
let mskImage = msk.get();
pgImage = pg.get();
pgImage.mask(mskImage);
image(pgImage, 0, 0);
noLoop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
A (very) hacky workaround would be to do the same thing with one canvas.
This would leave the areas inside the circles completely transparent so make them appear blue, simply make the background element behind the blue:
function setup() {
createCanvas(600,600);
background(255, 0, 0);
noStroke();
}
function draw() {
//blue: stuff in the background that should not change
fill ("blue");
rect (20,20,500,500);
//draw on external canvas
// pg = createGraphics(600,600);
//yellow+green foreground shapes
fill("green");
rect(100, 100, 200, 200);
fill("yellow");
rect(80, 80, 100, 300);
//punch a hole in the shapes
fill(0, 0, 255);
blendMode(REMOVE);
circle(140, 140, 150);
circle(180, 180, 150);
//bring in the external canvas with punched shapes
// image(pg, 0, 0);
noLoop();
}
body{
/* make the HTML background match the canvas blue */
background-color: #00F;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
This might not be flexible enough though.
Now, assuming your foreground is made of the yellow and green shapes and the background is blue, another option would be manually accessing the pixels[] array and updating pixel values. In your example the masks are circular so you could check if:
the distance between the current pixel and the circle's centre is smaller than the circle's radius: this means the pixel is inside the circle
also, if the colour inside the circle is a foreground colour (e.g. green or yellow in your case)
If both conditions match then you could replace this pixel with a background colour (blue in your case)
Here's an example of that:
function setup() {
createCanvas(600,600);
pixelDensity(1);
background(255, 0, 0);
noStroke();
}
function draw() {
//blue: stuff in the background that should not change
fill ("blue");
rect (20,20,500,500);
//draw on external canvas
//yellow+green foreground shapes
fill("green");
rect(100, 100, 200, 200);
fill("yellow");
rect(80, 80, 100, 300);
//punch a hole in the shapes
fill(0, 0, 255);
// make pixels available for reading
loadPixels();
// apply each circle "mask" / bg color replacement
// yellow , green , bg blue to replace fg with
circleMask(140, 140, 150, [255, 255, 0], [0, 0x80, 0], [0, 0, 255]);
circleMask(180, 180, 150, [255, 255, 0], [0, 0x80, 0], [0, 0, 255]);
// once all "masks" are applied,
updatePixels();
noLoop();
}
function circleMask(x, y, radius, fg1, fg2, bg){
// total number of pixels
let np = width * height;
let np4 = np*4;
//for each pixel (i = canvas pixel index (taking r,g,b,a order into account)
// id4 is a quarter of "i"
for(let i = 0, id4 =0 ; i < np4; i+=4, id4++){
// compute x from pixel index
let px = id4 % width;
// compute y from pixel index
let py = id4 / width;
// if we're within the circle
if(dist(px, py, x, y) < radius / 2){
// if we've found foreground colours to make transparent
// ([0][1][2] = r, g, b)
if((pixels[i] == fg1[0] || pixels[i] == fg2[0]) &&
(pixels[i+1] == fg1[1] || pixels[i+1] == fg2[1]) &&
(pixels[i+2] == fg1[2] || pixels[i+2] == fg2[2])){
// "mask" => replace fg colour matching pixel with bg pixel
pixels[i] = bg[0];
pixels[i+1] = bg[1];
pixels[i+2] = bg[2];
}
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
There are a few things to notice here.
pixels[] is faster than set(x, y, clr) but it means you need to remember a few details:
call loadPixels() before accessing pixels[] to read/populate the array
make all the pixels changes required (in this case the circle "masks" / pixels inside circle colour replacement)
call updatePixels() after pixels[] have been updated
Also notices it takes a bit of time to execute.
There could be a few speed improvements such as only iterating over the pixels inside the bounding box of the circle before checking distance and checking squared distance instead of dist(), however this would also make the code less readable.
I have a circle with x=50, y=30 and radius = 20.
This circle should move in 1000 ms to a new position x=150, y=170 and radius = 30.
These are my objects:
paper.circle(50, 30, 20);
paper.newCircle(150, 170, 30);
Now I want to animate a movement, so it looks like the first circle moves to the second one but with a bigger radius.
Much like the first example here (click on the arrow) but it should have a bigger radius after the animation.
This should be pretty simple but I can not figure out how to do this.
You can pass in an object to the animate method, and pass it a duration, like follows.
var c = paper.circle(50, 30, 20);
c.animate({ r: 30, cx: 150, cy: 170 }, 1000);
I'm starting to use svg.js on a project, and as I was playing with masks, I couldn't manage to invert them.
I drew a rectangle, and a circle, using the circle as a mask for the rectangle, only showing the part of the rectangle that's inside the mask.
var leftRect = draw.rect(300, 200);
var maskTest = draw.circle(100);
leftRect.attr({
x: 100,
y: 100,
fill: '#FF64F9'
});
maskTest.transform({
x: 150,
y: 150
});
leftRect.clipWith(maskTest);
Now I want the opposite, I want to mask to display anything that is not inside it. Is there any way to do that with svg.js or Snap.svg?
I found the solution.
I'm creating a group containing a black object, and a white object.
Using the group as a mask, the black part will be hidden, while the white part will be displayed.
var maskTest = draw.circle(100).fill("#000");
var maskRect = draw.rect(200, 100).fill("#fff");
var group = draw.group();
group.add(maskRect);
group.add(maskTest);
maskTest.transform({
x: 100,
y: 150
});
maskRect.transform({
x: 150,
y: 150
});
leftRect.maskWith(group);
I have a canvas drawn in Fabric.js that i am adding a group of rectangles to, i want to limit the edges of those rectangles as a group to not go outside a certain area.
Imagine making a stripy t-shirt, the stripes are make by using a series of rectangles and i need to keep them to the shape of the t-shirt.
I think its better to clip the entire canvas to the shape of the t shirt, so anything i add to it remains within the t-shirt but i am stuck. So far i am only clip to basic circles and rectangles.
Thanks
You can just render a shape inside canvas.clipTo :)
I just loaded a random SVG shape in kitchensink and did this:
var shape = canvas.item(0);
canvas.remove(shape);
canvas.clipTo = function(ctx) {
shape.render(ctx);
};
As you can see, entire canvas is now clipped by that SVG shape.
You may also try this one: http://jsfiddle.net/ZxYCP/198/
var clipPoly = new fabric.Polygon([
{ x: 180, y: 10 },
{ x: 300, y: 50 },
{ x: 300, y: 180 },
{ x: 180, y: 220 }
], {
originX: 'left',
originY: 'top',
left: 180,
top: 10,
width: 200,
height: 200,
fill: '#DDD', /* use transparent for no fill */
strokeWidth: 0,
selectable: false
});
You can simply use Polygon to clip. Answer is based on #natchiketa idea in this question Multiple clipping areas on Fabric.js canvas
If a lot of my drawings are going to be within a particular area of my larger canvas (in this case, in the center), is there a way to just say that you're working within that particular 'sub-canvas' instead of having to add/subtract the margins every time you want to draw? It just makes my code look a lot more complicated every time I'm specifying coordinates.
You can change the coordinates' origin using translate().
First, save the original origin using save(). Then, find the origin that suits the centre of your screen's drawing area and call translate(x, y). Do your drawing, and then use restore() to get your previous origin back.
jsFiddle.
Kinetic.js, a popular library for Canvas allows you to create a Group layer. You can specify the x, y coordinates, height and width of this Group. You can also add shapes and draw other things within this group.
Here's and example:
var stage = new Kinetic.Stage({
container: 'container',
width: 578,
height: 200
});
var shapesLayer = new Kinetic.Layer();
/*
* create a group which will be used to combine
* multiple simple shapes. Transforming the group will
* transform all of the simple shapes together as
* one unit
*/
var group = new Kinetic.Group({
x: 220,
y: 40,
rotationDeg: 20
});
var colors = ['red', 'orange', 'yellow'];
for(var n = 0; n < 3; n++) {
// anonymous function to induce scope
(function() {
var i = n;
var box = new Kinetic.Rect({
x: i * 30,
y: i * 18,
width: 100,
height: 50,
name: colors[i],
fill: colors[i],
stroke: 'black',
strokeWidth: 4
});
group.add(box);
})();
}
shapesLayer.add(group);
stage.add(shapesLayer);
Here's a tutorial on how to add Groups
You can use drawimage to draw an offscreen canvas to a certain part of another canvas.
Create a new canvas object and draw all your stuff to that. In the end draw that canvas to your onscreen canvas with drawimage at some coordinates.