Related
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 want to make a simple project in which the user has to select which text color looks better on a particular background. After clicking on the right background color, I should log the color and corresponding text color (0 for black, 1 for white) to a file.
This is what I have so far:
function setup(){
createCanvas(1200, 600);
}
function draw(){
background(51);
fill(0);
wBol = ellipse(300, 300, 350, 350);
fill(0);
bBol = ellipse(900, 300, 350, 350);
textSize(64);
fill(0);
text('text', 250, 320);
fill(255);
text('text', 850, 320);
}
function mousePressed(){
var d1 = dist(300, 300, mouseX, mouseY);
var d2 = dist(900, 300, mouseX, mouseY);
if(d1 <= 350/2){
console.log(wBol.fill());
wBol.fill(random(0,255), random(0,255), random(0,255));
bBol.fill(random(0,255), random(0,255), random(0,255));
}
if(d2 <= 350/2){
console.log(wBol.fill());
wBol.fill(random(0,255), random(0,255), random(0,255));
bBol.fill(random(0,255), random(0,255), random(0,255));
}
}
This doesn't work and I know 'fill' isn't supposed to work like this, but I don't have a single clue on how I should go about this.
Thanks in advance.
In p5.js, ellipses (and other shapes) aren't objects. The ellipse() function has no output, it simply draws an ellipse on the screen. Similarly with fill(), the function has no output, it simply sets the fill color for the next things you draw. So if you want to save the fill color, you should do it in another variable. You could do something like this:
let wBol = { xpos: 300, ypos: 300, xwid: 350, ywid: 350, fill: 0};
let bBol = { xpos: 900, ypos: 300, xwid: 350, ywid: 350, fill: 0};
function draw() {
background(51);
fill(wBol.fill);
ellipse(wBol.xpos, wBol.ypos, wBol.xwid, wBol.ywid);
fill(bBol.fill);
ellipse(bBol.xpos, bBol.ypos, bBol.xwid, bBol.ywid);
//... the rest
}
I'm using p5.js to draw a bottle shape (cylinder) and to put a string on that cylinder. Until now, I could only put the string horizontally and I'm trying to put my string vertically (as if you had to turn your head to be able to read)
I searched a lot and I tried with using rotate() and translate() functions, which are in the p5.js documentation.
let angle = 0;
function setup() {
createCanvas(600, 400, WEBGL);
graphics = createGraphics(200, 200);
graphics.background(255);
test = createGraphics(300, 100);
test.background(144, 214, 199);
test.fill(255);
test.textAlign(CENTER);
test.textSize(32);
test.text('QWERTY', 150, 50);
rotate(90);
}
function draw() {
background(255);
graphics.fill(255, 0, 255);
graphics.ellipse(mouseX, mouseY, 20);
ambientLight(100);
directionalLight(255, 255, 255, 0, 0, 1);
noStroke();
rotateY(angle * 0.1);
texture(test);
cylinder(50, 230);
angle += 0.07;
}
You can find the codepen on the link below:
https://codepen.io/smickael/pen/bPMEOP
You're so super close !
You simply need to rotate the PGraphics buffer you're drawing the text into, not the sketch itself, by 90 degrees (using radians (HALF_PI)).
Additionally it would help to translate after depending on how you want to align the text
let angle = 0;
function setup() {
createCanvas(600, 400, WEBGL);
graphics = createGraphics(200, 200);
graphics.background(255);
test = createGraphics(300, 100);
test.background(144, 214, 199);
// rotate the buffer by 90 degrees (remember rotate() works with radians)
test.rotate(radians(90));
// similar to passing 75,0 to text, but keeping transformations grouped for easy tweaking
test.translate(75,0);
test.fill(255);
test.textAlign(CENTER);
test.textSize(32);
test.text('QWERTY', 0, 0);
}
function draw() {
background(255);
graphics.fill(255, 0, 255);
graphics.ellipse(mouseX, mouseY, 20);
ambientLight(100);
directionalLight(255, 255, 255, 0, 0, 1);
noStroke();
rotateY(angle * 0.25);
texture(test);
cylinder(50, 230);
angle += 0.07;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
I am creating a piano using p5.js. I need help with the color change. When a user presses a key, I want the key to flash a quick color change to let them know that they pressed the key.
In my code, the color does change when you click on the first key, however, when I click a little bit outside the first key, the first key still changes color.
Is my distance a little off? Or is there a more effective way to do this?
function setup() {
createCanvas(990, 600);
}
function draw() {
background(220);
fill(255);
rect(0, 300, 70, 400);
rect(70, 300, 70, 400);
rect(140, 300, 70, 400);
rect(210, 300, 70, 400);
rect(280, 300, 70, 400);
rect(350, 300, 70, 400);
rect(420, 300, 70, 400);
rect(490, 300, 70, 400);
rect(560, 300, 70, 400);
rect(630, 300, 70, 400);
rect(700, 300, 70, 400);
rect(770, 300, 70, 400);
rect(840, 300, 70, 400);
rect(910, 300, 70, 400);
fill(0);
rect(50, 300, 38, 180);
rect(120, 300, 38, 180);
rect(260, 300, 38, 180);
rect(330, 300, 38, 180);
rect(400, 300, 38, 180);
rect(540, 300, 38, 180);
rect(610, 300, 38, 180);
rect(750, 300, 38, 180);
rect(820, 300, 38, 180);
rect(890, 300, 38, 180);
text("mouse x: "+mouseX+" mouse y:"+mouseY, width/2,height-30);
}
function mousePressed() {
cursor(HAND);
}
function mouseReleased() {
cursor(ARROW);
let d = dist(mouseX, mouseY, 0, 300);
if (d < 300) {
fill(0);
rect(0, 300, 70, 400);
}
}
You're checking whether the mouse position is withing 300 pixels of the point 0,300. This sets up a circle with a center of 0,300 and a radius of 300. Try drawing that in your scene to see where your clickable area is.
Your keys are rectangles, so you should be using point-rectangle intersection to check whether the mouse is inside a particular key. Google is your friend here, but basically you want to check whether mouseX is between the left and right edges, and mouseY is between the top and bottom edges.
Finally, note that you're only ever showing the "flash" for a single frame. I personally can't even see it. You probably want to show the flash for longer, using the millis() function or the frameCount variable for timing. (Again, Google is your friend!)
Shameless self-promotion: here is a tutorial on collision detection, including point-rectangle intersection. It's for Processing, but the ideas are the same in P5.js.
Im working my way through the Khanacademy site and a a bit confused on how to make randomly sized(within certain parameters), and colored fish after mouseClick. I cant even get mouseClick working.
background(89, 216, 255);
var mouseClicked = function(drawFish){
};
^^^^^^^^^^^^^^^ What am I missing here? ^^^^^^^^^^
var drawFish = function(centerX, centerY, bodyLength, bodyHeight, bodyColor, tailWidth,
tailHeight, eyeColor,tailColor, eyeWidth, eyeHeight){
noStroke();
fill(bodyColor);
// body
ellipse(centerX, centerY, bodyLength, bodyHeight);
// tail
fill(tailColor);
triangle(centerX-bodyLength/2, centerY,
centerX-bodyLength/2-tailWidth, centerY-tailHeight,
centerX-bodyLength/2-tailWidth, centerY+tailHeight);
// eye
fill(eyeColor);
ellipse(centerX+bodyLength/3, centerY, eyeWidth, eyeHeight);
};
drawFish(296, 281, -57,52, color(245, 227, 30),-15, 60,color(13, 12, 12),color(66, 58, 58),4,4); // yellowFish
drawFish(290, 80, 180, 140, color(255, 0, 0), 57, 45,color(46, 66, 194), color(255, 204, 0),32,8); // redFish
drawFish(146,233, 218, 141, color(62, 110, 67), 30, 10, color(245, 240, 245), color(0, 51, 255),12,48); // greenFish
drawFish(233, 370, 322, 36, color(133, 34, 199), 61,15, color(255, 0, 0), color(34, 255, 0),67,20); // purpFish
Any other pointers or recommendations would be greatly appreciated.
Thanks guys!
Cool fishes! I'm learning Processing too. I ran your code and came up with this function for having random fishes appear when you click.
void mouseReleased() {
var c1 = color(random(0,255),random(0,255),random(0,255),random(0,255))
var c2 = color(random(0,255),random(0,255),random(0,255),random(0,255))
var c3 = color(random(0,255),random(0,255),random(0,255),random(0,255))
var s1 = random(10,100)
var s2 = random(10,100)
var s3 = random(10,100)
var s4 = random(10,100)
var s5 = random(5,s1)
var s6 = random(5,s2)
drawFish(mouseX, mouseY, s1, s2, c2, s3,s4, c2, c3,s5,s6); //randFish
}
To make an animation, I think you will want to use the draw() function, use variables for the x,y locations and redraw the background each time. This example has the red fish follow the mouse around.
void draw(){
background(89, 216, 255);
drawFish(mouseX, mouseY, 180, 140, color(255, 0, 0), 57, 45,color(46, 66, 194), color(255, 204, 0),32,8); // redFish
}
I hope your aquarium comes out really great!