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>
Related
In P5js I want to overlay a shape to a video, but they don't share the same dom element.
Is there any way to do it?
Code test here: The video should be visible through the triangular contour that cuts the shape.
The code:
let video;
function preload() {
video = createVideo("video.mp4");
}
function setup() {
createCanvas(400, 300);
background("gray");
video.size(400,400);
video.loop();
var w = width * 0.7;
var h = height * 0.7;
translate((width/2) - (w/2), (height/2) - (h/2));
fill("lightgray");
noStroke();
beginShape();
vertex(0, 0);
vertex(w, 0);
vertex(w, h);
vertex(0, h);
beginContour();
vertex(w * 0.2, h * 0.4);
vertex(w * 0.5, h * 0.8);
vertex(w * 0.8, h * 0.4);
endContour();
endShape();
noLoop();
}
I see here that hiding the video and using image (i.e. image(video, 10, 10)) it is possible to draw a single frame. Alas I use noLoop() and in my case there would be no automatic refresh of the video in draw().
"The video should be visible through the triangular contour that cuts the shape."
Below is a result I got from a quick play-around with your code. Maybe it will be useful to you in some way (eg: gives new ideas on how to proceed).
The code's logic is to simply create 2 layers.
Bottom layer 1 is video and top layer 2 is triangle (the canvas).
Other ideas include maybe using BlendModes like Screen or Multiply:
example: canv.blendMode(SCREEN);
Where SCREEN makes whites transparent, and MULTIPLY makes blacks transparent).
The example testing code (makes two layers and also removed background("gray");)
let video; let canv;
function preload()
{ video = createVideo("video.mp4"); }
function setup()
{
translate(0, 0);
video.size(400,300);
video.style("z-index: 1"); //# is default in P5.JS
video.position(0, (width * 0.7) );
video.loop();
canv = createCanvas(400, 400);
canv.style("z-index: 2");
canv.position(0, 0); //# important to have a setting
//# Not needed ....
//background("gray");
var w = width * 1;
var h = height * 1;
translate((width/2) - (w/2), (height/2) - (h/2));
fill("lightgray");
noStroke();
beginShape();
vertex(0, 0);
vertex(w, 0);
vertex(w, h);
vertex(0, h);
beginContour();
vertex(w * 0.2, h * 0.4);
vertex(w * 0.5, h * 0.8);
vertex(w * 0.8, h * 0.4);
endContour();
endShape();
noLoop();
}
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'm try to show the entirety of the green circle, and only the intersecting point of the pink circle.
context
Anyone have any idea if this is possible in Javascript?
function setup() {
createCanvas(500,500);
}
function draw() {
noStroke();
fill(3, 252, 102);
ellipse(100, 100, 100, 100);
fill(255, 46, 206, 100);
ellipse(130,130, 100, 100);
}
I am trying to present this shape as a stimulus in a behavioral experiment. I wantthe image to have a random contrast level between 0 and 1. I am trying to use Math.random() for that but when I run this in Chrome the shape flickers when it is presented on the screen. Is there a way to present a stable shape with randomly generated contrast levels?
drawFunc: function gabor() {
context = jsPsych.currentTrial().context;
context.beginPath();
const gradLength = 100;
const my_gradient = context.createLinearGradient(850, 0, 1050, 0);
my_gradient.addColorStop(0,'rgb(0, 0, 0)');
my_gradient.addColorStop(0.05,'rgb(255, 255, 255)');
my_gradient.addColorStop(0.1,'rgb(0, 0, 0)');
my_gradient.addColorStop(0.15,'rgb(255, 255, 255)');
my_gradient.addColorStop(0.2,'rgb(0, 0, 0)');
my_gradient.addColorStop(0.25,"rgb(255, 255, 255)");
my_gradient.addColorStop(0.3,"rgb(0, 0, 0)");
my_gradient.addColorStop(0.35,"rgb(255, 255, 255)");
my_gradient.addColorStop(0.4,"rgb(0, 0, 0)");
my_gradient.addColorStop(0.45,"rgb(255, 255, 255)");
my_gradient.addColorStop(0.5,"rgb(0, 0, 0)");
my_gradient.addColorStop(0.55,"rgb(255, 255, 255)");
my_gradient.addColorStop(0.6,"rgb(0, 0, 0)");
my_gradient.addColorStop(0.65,"rgb(255, 255, 255)");
my_gradient.addColorStop(0.7,"rgb(0, 0, 0)");
my_gradient.addColorStop(0.75,"rgb(255, 255, 255)");
my_gradient.addColorStop(0.8,"rgb(0, 0, 0)");
my_gradient.addColorStop(0.85,"rgb(255, 255, 255)");
my_gradient.addColorStop(0.9,"rgb(0, 0, 0)");
my_gradient.addColorStop(0.95,"rgb(255, 255, 255)");
my_gradient.addColorStop(1,"rgb(0, 0, 0)");
var result1 = Math.random();
context.filter = 'contrast('+ result1 +')';
context.fillStyle=my_gradient;
context.fillRect(950,300,gradLength,gradLength);
context.stroke();
Use a setup function to get the constants
Create the gradient once
Get the context once.
Create contrast once using a function. See example
Remove setup code from draw function.
Remove random contrast value from draw function.
Remove redundant code
No need for context.beginPath(); as you draw the using fillRect
No need for context.stroke(). Not sure why you are calling stroke as you have not defined a path after the beginPath call
Why assign a named function gabor to object property drawFunc Do you call the function using its name or the property. Which ever you use makes the other redundent.
Example
setup() {
this.ctx = jsPsych.currentTrial().context;
const g = this.gradient = this.ctx.createLinearGradient(850, 0, 1050, 0);
const bands = 10, colors = ["#000", "#FFF"];
const stops = bands * colors.length;
var pos = 0;
while (pos <= stops) {
g.addColorStop(pos / stops, colors[pos % colors.length]);
pos++;
}
},
randomContrast() {
this.contrast = Math.Random();
this.drawFunc();
},
drawFunc() {
const ctx = this.ctx;
ctx.filter = 'contrast('+ this.contrast +')';
ctx.fillStyle = this.gradient;
ctx.fillRect(950, 300, 100, 100);
},
Call setup once, then call randomContrast to change the contrast. If you need to redraw the gradient without changing the contrast call drawFunc
I am using ctx.translate(x, y) to move Camera in canvas game. But for some reason, that doesn't work.
This is what I am using:
setCameraPos: function(x, y) {
//ctx.save()
ctx.translate(x, y)
ctx.setTransform(1, 0, 0, 1, 0, 0)
//ctx.restore()
}
It doesn't work at all. It does not change position of camera.
Any errors? No errors at all.
I am using Electron 3.0.3 Beta.
I accept any libraries.
const canvas = document.getElementById('main')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 30, 30)
// This doesn't work | VVV
ctx.translate(20, 20)
ctx.setTransform(1, 0, 0, 1, 0, 0)
#main {
background-color: black;
}
<canvas id="main">
</canvas>
From what you gave, the translate operation won't work anywhere, not just in Electron.
ctx.setTransform() method sets the transformation matrix to absolute values, the current matrix is discarded and the passed values are the ones to which your matrix will get set.
1, 0, 0, 1, 0, 0 are the values of the native matrix transform (i.e untransformed).
So calling ctx.setTransform(1, 0, 0, 1, 0, 0) will reset your tranform matrix to its default and make all calls to relative translate(), rotate() or transform() useless.
These methods are meant to be relative because they add up to the current matrix values. For instance,
ctx.translate(10, 10);
// here next drawing will be offset by 10px in both x and y direction
ctx.translate(40, -10);
// this adds up to the current 10, 10, so we are now offset by 30, 0
If you want your translate to work, don't call setTransform here, or even replace it with setTransform(1, 0, 0, 1, 20, 20)
Also, in your snippet, you are setting the transformation matrix after you did draw. The transformations will get applied only on next drawings, not on previous ones.
Now, you might be in an animation loop, and need your matrix to get reset at every loop.
In this case, call ctx.setTransform(1,0,0,1,0,0) either at the beginning of your drawing loop, either as the last op, and call translate() before drawing.
const canvas = document.getElementById('main');
const ctx = canvas.getContext('2d');
let x = 0;
ctx.fillStyle = 'red'
anim();
function draw() {
// reset the matrix so we can clear everything
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
//set the transform before drawing
ctx.translate(x - 30, 20)
//which is actually the same as
//ctx.setTransform(1, 0, 0, 1, x, 20);
ctx.fillRect(0, 0, 30, 30);
}
function anim() {
x = (x + 2) % (canvas.width + 60);
draw();
requestAnimationFrame(anim);
}
#main {
background-color: black;
}
<canvas id="main"></canvas>