p5.js WebGL 3d graphics covered by 2d background when rotated - javascript

I have two things that I want to display with p5, one is a 2D background and the other is a 3D WebGL foreground, both generated by p5. What I noticed is that even if I draw the 2D background before the 3D stuff in the draw() function, the 3D stuff will still be partially covered by the background when rotateX() or rotateY() is called. It looks kind of like this:
I suspect what's happening is that the 2d and 3d stuff are both on the same z-plane, therefore when the foreground is rotated some of it gets covered by the background which now is in the front compared to the covered parts.
So my question is how can I keep the background completely in the back (i.e. not covering foreground regardless of the rotation)?
Below is my current implementation, the 2d background is generated in an offscreen canvas then put onto the main canvas with image() where the 3d stuff is generated, but I'll take any other approaches.
let bg;
p.setup = () => {
p.createCanvas(width,height,p.WEBGL);
bg = p.createGraphics(width,height);
}
p.draw = () => {
... // draw background bg
p.image(bg,x,y); // draw background on canvas
... // draw foreground
p.rotateX(degrees);//rotate
}

The best way to accomplish this is by clearing the WebGL depth buffer. This is buffer stores the depth for every pixel that has been draw so far so that as subsequent triangles are drawn they can be clipped if some or all of them is behind whatever was previously drawn at that location. This buffer is automatically cleared in between calls to draw() in p5.js but you can also call it yourself mid-frame:
let bg;
let zSlider;
let glContext;
function setup() {
let c = createCanvas(200, 200, WEBGL);
glContext = c.GL;
bg = createGraphics(width, height);
bg.background('red');
for (let y = 0; y < height; y += 20) {
for (let x = 0; x < width; x += 20) {
if ((x / 20 + y / 20) % 2 === 0) {
bg.fill('black');
} else {
bg.fill(
map(x + y, 0, width + height, 0, 360),
map(y, 0, height, 50, 100),
map(x, 0, width, 50, 100)
);
}
bg.square(x, y, 20)
}
}
zSlider = createSlider(0, width * 2, width);
zSlider.position(10, 10);
}
function draw() {
image(bg, -width / 2, -height / 2, width, height);
// Clear the z-buffer, subsequent drawing commands will not clip, even if they
// intersect with or are behind previously drawn elements (like our background
// image)
glContext.clear(glContext.DEPTH_BUFFER_BIT);
push();
translate(0, 0, (zSlider.value() - width) * 2);
rotateX(millis() / 1000 * PI / 4);
rotateY(millis() / 1000 * PI / 8);
box(100);
pop();
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.3.1/lib/p5.js"></script>
There is also kludgy solution that doesn't rely on calling WebGL internals, but I have only been able to make it work for square canvases:
Switch to an orthographic camera mode before drawing your background image.
Translate in the negative Z direction as far as possible without going beyond the "far" clipping plane.
Draw your background image.
Pop the state back to the normal perspective camera.
This uses orthographic projection to allow you to draw the background image behind the rest of the scene without diminishing size due to perspective. However I haven't come up with a fool proof way to determine what the perfect translation value is, nor how to reliably setup the orthographic project to control where the "far" clipping plane is.
let bg;
let zSlider;
function setup() {
createCanvas(200, 200, WEBGL);
bg = createGraphics(width, height);
bg.background('red');
for (let y = 0; y < height; y += 20) {
for (let x = 0; x < width; x += 20) {
if ((x / 20 + y / 20) % 2 === 0) {
bg.fill('black');
} else {
bg.fill(
map(x + y, 0, width + height, 0, 360),
map(y, 0, height, 50, 100),
map(x, 0, width, 50, 100)
);
}
bg.square(x, y, 20)
}
}
zSlider = createSlider(0, width * 2, width);
zSlider.position(10, 10);
}
function draw() {
push();
ortho();
translate(0, 0, min(width, height) * -0.13);
image(bg, -width / 2, -height / 2, width, height);
pop();
push();
translate(0, 0, (zSlider.value() - width) * 2);
rotateX(millis() / 1000 * PI / 4);
rotateY(millis() / 1000 * PI / 8);
box(100);
pop();
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.3.1/lib/p5.js"></script>

Related

Drawing a sine wave from top right to bottom left

I'm trying to design a small project using canvas and I want to design a sine wave in such a way that the wave is generated from the top right corner to the left bottom corner infinitely if possible.
Something like an inverse sine graph.
All I can do is make the sine wave go from left to right but making this work from top right to bottom left is very difficult.
This is my code at the moment
It's looking very sad...
const canvas = document.querySelector(".canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth - 5;
canvas.height = window.innerHeight - 5;
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
const wave = {
y: canvas.height / 2,
length: 0.02,
amplitude: 100,
frequency: 0.01,
yOffSet: canvas.height,
};
let increment = wave.frequency;
function animate() {
requestAnimationFrame(animate);
ctx.fillStyle = "rgb(0,0,0)";
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(0, canvas.height);
for (let i = 0; i < canvas.width; i++) {
ctx.lineTo(Math.sin(i / wave.length + increment) * wave.amplitude + wave.yOffSet, 0);
}
ctx.stroke();
increment += wave.frequency;
}
animate();
body {
margin: 0;
padding: 0;
}
<div>
<canvas class="canvas"></canvas>
</div>
Desired Output
The problem is in this line:
ctx.lineTo(Math.sin(i / wave.length + increment) * wave.amplitude + wave.yOffSet, 0);
You are only moving in the x co-ordinate.
I have added the motion in the y co-ordinate and rewrote on three lines just for clarity.
x=i+wave.amplitude*Math.sin(i/wave.length);
y=canvas.height-(i-(wave. amplitude * Math.sin(i/wave.length)));
ctx.lineTo(x,y);
The result it produces is like what you describe, if I understood correctly. There are many more waves than you show in the drawing, but that can be cahnged by the wave.length parameter.

Rotate tiled images individually in HTML canvas

I have some code in my game that rotates an icon within its canvas as follows:
if (rotate > 0) {
context.translate(canvasWidth / 2, canvasHeight / 2);
context.rotate(rotate);
context.translate(-canvasWidth / 2, -canvasHeight / 2);
}
Nothing you haven't seen before. I've also added a function that tiles the icons within a larger canvas like so:
var x = 0;
var y = 0;
for (var i = 0; i < totalUnits; i++) {
context.drawImage(img, x, y);
if (i != 0 && (i + 1) % level == 0) {
x = 0;
y += 72;
}
else {
x += 72;
}
}
Note that the variable level can be any integer, and totalUnits is its square if it's greater than 1, so for example if I specify level as 2, then 4 images are drawn on my canvas, 2 across and 2 down. Note also that my images are always 72x72 pixels, hence the 72 above. Again, nothing particularly exciting.
My difficulty is trying to rotate the images within the canvas such that the individual image is rotated by the value passed in rotate, but not the whole canvas itself. I have tried adding the following code with many permutations replacing the above call to context.drawImage in the for loop, with no luck so far:
context.translate(72 / 2, 72 / 2);
context.rotate(rotate);
context.drawImage(img, x, y);
context.rotate(0);
context.translate(-72 / 2, -72 / 2);
To help visualise the effect I am trying to achieve, here is what is drawn when rotate is set to 0:
And here is what I'd like my tiled images on the canvas to look like when rotated by 45 degrees (for example):
I'd like to point out that I am not trying to rotate the entire canvas - I know how to do that, but it is not the effect I'm trying to achieve as I need the icons to stay in their individual x, y positions. Also, rotating the entire canvas presents cutoff corner challenges.
Any help would be much appreciated!
Easiest way is to overwrite the current transform
ctx.setTransform(scale, 0, 0, scale, x, y);
ctx.rotate(rotate);
ctx.drawImage(img, -img.width / 2, -img.height/ 2);
Draw image center at x, y, rotated by rotate around img center, and scales by scale. Scale of 1 is no scale.
To reset to default transform for example before clearing the canvas
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
Because under the hood the ctx.rotate requires at least 1 sin and 1 cos, 12 temp numbers, then 12 multiplications and 8 additions. If the scaling is uniform (same scale for x and y), it is quicker to use a simplified method to creating the matrix. Note if the scale is always 1 you don't need the scale`
Also when the image loads you can set the point of rotation as properties of the image.
When the image loads set the offset
img.offset_x = - img.naturalWidth / 2; // NOTE I use snake_case for the property names
img.offset_y = - img.naturalHeight / 2; // so that the names will never clash with
// future changes to the standard
To render that image
const ax = Math.cos(rotate) * scale;
const ay = Math.sin(rotate) * scale;
ctx.setTranform(ax, ay, -ay, ax, x, y);
ctx.drawImage(img, img.offset_x, img.offset_y);

Image Manipulation - add image with corners in exact positions

I have an image which is a background containing a boxed area like this:
I know the exact positions of the corners of that shape, and I'd like to place another image within it. (So it appears to be inside the box).
I'm aware of the drawImage method for HTML5 canvas, but it seems to only support x, y, width, height parameters rather than exact coordinates. How might I draw an image onto a canvas at a specific set of coordinates, and ideally have the browser itself handle stretching the image.
Quadrilateral transform
One way to go about this is to use Quadrilateral transforms. They are different than 3D transforms and would allow you to draw to a canvas in case you want to export the result.
The example shown here is simplified and uses basic sub-divison and "cheats" on the rendering itself - that is, it draws in a small square instead of the shape of the sub-divided cell but because of the small size and the overlap we can get away with it in many non-extreme cases.
The proper way would be to split the shape into two triangles, then scan pixel wise in the destination bitmap, map the point from destination triangle to source triangle. If the position value was fractional you would use that to determine pixel interpolation (f.ex. bi-linear 2x2 or bi-cubic 4x4).
I do not intend to cover all this in this answer as it would quickly become out of scope for the SO format, but the method would probably be suitable in this case unless you need to animate it (it is not performant enough for that if you want high resolution).
Method
Lets start with an initial quadrilateral shape:
The first step is to interpolate the Y-positions on each bar C1-C4 and C2-C3. We're gonna need current position as well as next position. We'll use linear interpolation ("lerp") for this using a normalized value for t:
y1current = lerp( C1, C4, y / height)
y2current = lerp( C2, C3, y / height)
y1next = lerp(C1, C4, (y + step) / height)
y2next = lerp(C2, C3, (y + step) / height)
This gives us a new line between and along the outer vertical bars.
Next we need the X positions on that line, both current and next. This will give us four positions we will fill with current pixel, either as-is or interpolate it (not shown here):
p1 = lerp(y1current, y2current, x / width)
p2 = lerp(y1current, y2current, (x + step) / width)
p3 = lerp(y1next, y2next, (x + step) / width)
p4 = lerp(y1next, y2next, x / width)
x and y will be the position in the source image using integer values.
We can use this setup inside a loop that will iterate over each pixel in the source bitmap.
Demo
The demo can be found at the bottom of the answer. Move the circular handles around to transform and play with the step value to see its impact on performance and result.
The demo will have moire and other artifacts, but as mentioned earlier that would be a topic for another day.
Snapshot from demo:
Alternative methods
You can also use WebGL or Three.js to setup a 3D environment and render to canvas. Here is a link to the latter solution:
Three.js
and an example of how to use texture mapped surface:
Three.js texturing (instead of defining a cube, just define one place/face).
Using this approach will enable you to export the result to a canvas or an image as well, but for performance a GPU is required on the client.
If you don't need to export or manipulate the result I would suggest to use simple CSS 3D transform as shown in the other answers.
/* Quadrilateral Transform - (c) Ken Nilsen, CC3.0-Attr */
var img = new Image(); img.onload = go;
img.src = "https://i.imgur.com/EWoZkZm.jpg";
function go() {
var me = this,
stepEl = document.querySelector("input"),
stepTxt = document.querySelector("span"),
c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
corners = [
{x: 100, y: 20}, // ul
{x: 520, y: 20}, // ur
{x: 520, y: 380}, // br
{x: 100, y: 380} // bl
],
radius = 10, cPoint, timer, // for mouse handling
step = 4; // resolution
update();
// render image to quad using current settings
function render() {
var p1, p2, p3, p4, y1c, y2c, y1n, y2n,
w = img.width - 1, // -1 to give room for the "next" points
h = img.height - 1;
ctx.clearRect(0, 0, c.width, c.height);
for(y = 0; y < h; y += step) {
for(x = 0; x < w; x += step) {
y1c = lerp(corners[0], corners[3], y / h);
y2c = lerp(corners[1], corners[2], y / h);
y1n = lerp(corners[0], corners[3], (y + step) / h);
y2n = lerp(corners[1], corners[2], (y + step) / h);
// corners of the new sub-divided cell p1 (ul) -> p2 (ur) -> p3 (br) -> p4 (bl)
p1 = lerp(y1c, y2c, x / w);
p2 = lerp(y1c, y2c, (x + step) / w);
p3 = lerp(y1n, y2n, (x + step) / w);
p4 = lerp(y1n, y2n, x / w);
ctx.drawImage(img, x, y, step, step, p1.x, p1.y, // get most coverage for w/h:
Math.ceil(Math.max(step, Math.abs(p2.x - p1.x), Math.abs(p4.x - p3.x))) + 1,
Math.ceil(Math.max(step, Math.abs(p1.y - p4.y), Math.abs(p2.y - p3.y))) + 1)
}
}
}
function lerp(p1, p2, t) {
return {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t}
}
/* Stuff for demo: -----------------*/
function drawCorners() {
ctx.strokeStyle = "#09f";
ctx.lineWidth = 2;
ctx.beginPath();
// border
for(var i = 0, p; p = corners[i++];) ctx[i ? "lineTo" : "moveTo"](p.x, p.y);
ctx.closePath();
// circular handles
for(i = 0; p = corners[i++];) {
ctx.moveTo(p.x + radius, p.y);
ctx.arc(p.x, p.y, radius, 0, 6.28);
}
ctx.stroke()
}
function getXY(e) {
var r = c.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top}
}
function inCircle(p, pos) {
var dx = pos.x - p.x,
dy = pos.y - p.y;
return dx*dx + dy*dy <= radius * radius
}
// handle mouse
c.onmousedown = function(e) {
var pos = getXY(e);
for(var i = 0, p; p = corners[i++];) {if (inCircle(p, pos)) {cPoint = p; break}}
}
window.onmousemove = function(e) {
if (cPoint) {
var pos = getXY(e);
cPoint.x = pos.x; cPoint.y = pos.y;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(update.bind(me))
}
}
window.onmouseup = function() {cPoint = null}
stepEl.oninput = function() {
stepTxt.innerHTML = (step = Math.pow(2, +this.value));
update();
}
function update() {render(); drawCorners()}
}
body {margin:20px;font:16px sans-serif}
canvas {border:1px solid #000;margin-top:10px}
<label>Step: <input type=range min=0 max=5 value=2></label><span>4</span><br>
<canvas width=620 height=400></canvas>
You can use CSS Transforms to make your image look like that box. For example:
img {
margin: 50px;
transform: perspective(500px) rotateY(20deg) rotateX(20deg);
}
<img src="https://via.placeholder.com/400x200">
Read more about CSS Transforms on MDN.
This solution relies on the browser performing the compositing. You put the image that you want warped in a separate element, overlaying the background using position: absolute.
Then use CSS transform property to apply any perspective transform to the overlay element.
To find the transform matrix you can use the answer from: How to match 3D perspective of real photo and object in CSS3 3D transforms

How to make object orbit from behind to front?

Is it possible to make an object orbit around another object that goes from behind and then to the front?
I've seen it being done with rotation animations that do a full 360 around the perimeter, but was wondering if it was possible to do it at an angle.
I couldn't find any resources that could do this, so I've included an image example of what I want to accomplish. The red line would be an object orbiting the blue circle.
Thanks so much - I really appreciate the help!
I figured I'd just write up a solution using the <canvas>
var x, y, scale, state, // Variables we'll use later.
canvas = document.getElementById("canvas"), // Get the canvas,
ctx = canvas.getContext("2d"), // And it's context.
counter = 0, // Counter to increment for the sin / cos functions.
width = 350, // Canvas width.
height = 200, // Canvas height.
centerX = width / 2, // X-axis center position.
centerY = height / 2, // Y-axis center position.
orbit = { // Settings for the orbiting planet:
width: 150, // Orbit width,
height: 50, // Orbit height,
size: 10 // Orbiting planet's size.
};
canvas.width = width; // Set the width and height of the canvas.
canvas.height = height;
function update(){
state = counter / 75; // Decrease the speed of the planet for a nice smooth animation.
x = centerX + Math.sin(state) * orbit.width; // Orbiting planet x position.
y = centerY + Math.cos(state) * orbit.height; // Orbiting planet y position.
scale = (Math.cos(state) + 2) * orbit.size; // Orbiting planet size.
ctx.clearRect(0, 0, width, height); // Clear the canvas
// If the orbiting planet is before the center one, draw the center one first.
(y > centerY) && drawPlanet();
drawPlanet("#f00", x, y, scale); // Draw the orbiting planet.
(y <= centerY) && drawPlanet();
counter++;
}
// Draw a planet. Without parameters, this will draw a black planet at the center.
function drawPlanet(color, x, y, size){
ctx.fillStyle = color || "#000";
ctx.beginPath();
ctx.arc(x || centerX,
y || centerY,
size || 50,
0,
Math.PI * 2);
ctx.fill();
}
// Execute `update` every 10 ms.
setInterval(update, 10);
<canvas id="canvas"></canvas>
If you want to change the roation direction of the orbiting planet, just replace:
x = centerX + Math.sin(state) * orbit.width;
y = centerY + Math.cos(state) * orbit.height;
With:
x = centerX + Math.cos(state) * orbit.width;
y = centerY + Math.sin(state) * orbit.height;
// ^ Those got switched.
The speed of the orbit can be changed by modifying the 75 in:
state = counter / 75;

Understanding HTML5 Canvas

I am trying to get to grips and understand how to use and create colliding balls with HTML5 canvas,examples I have looked at have a lot of JavaScript, but I need to break it down into much smaller chunks to get a better understanding of what's going on.
In my example what I understand so far is that I am redrawing the circles every 40 milliseconds onto the canvas, and calling the animate function each time. Every time this is called the position of the circle changes as I am changing it with
circles[0].x+=1;
circles[0].y+=-1.5;
So my circle objects are in an array, and there are 2 things I would like to achieve:
1) not to let the balls escape the canvas area
2) if the balls collide then bounce off each other and reverse in direction.
What I want to tackle first though is not letting the balls escape the canvas and how I would go about working that out.
I have access to the window.width and window.height, so it's a case of understanding how to get the position of each ball in the array, and ensure that it does not cross those boundaries.
I don't want to just have it work, would much prefer to understand what is happening.
This will check collisions on the bounds of the canvas. I updated your objects to store vx and vy (velocity) and the draw() function to move based on these properties. I added checkBounds() which reverses the velocity when the circle goes outside the bounds.
EDIT: modified so that it takes into account the radius of the circles too.
Doing a collision detect between the circles could follow a similar pattern
http://jsfiddle.net/3tfUN/5/
var canvas = document.getElementById('ball-canvas');
var context = canvas.getContext('2d')
var radius = 50;
var strokewidth = 2;
var strokestyle = '#666';
var frameCount = 0;
var w = canvas.width;
var h = canvas.height;
// Circle Objects
var yellowCircle = {
x: 50,
y: h / 2,
radius: radius,
color: 'yellow',
vx: 1,
vy: 1.5
}
var redCircle = {
x: 450,
y: h / 2,
radius: radius,
color: 'red',
vx: 1,
vy: -1
}
var blueCircle = {
x: 850,
y: h / 2,
radius: radius,
color: 'blue',
vx: -1,
vy: -1.5
}
// Create empty array and then push cirlce objects into array
var circles = [];
circles.push(yellowCircle, blueCircle, redCircle);
function checkBounds() {
for (var i = 0; i < circles.length; i++) {
var c = circles[i];
if (c.x > w - c.radius || c.x < c.radius) {
c.vx = -c.vx;
}
if (c.y > h - c.radius || c.y < c.radius) {
c.vy = -c.vy;
}
}
}
// Clear last circle and draw again
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the circle from the from page
for (var i = 0; i < circles.length; i++) {
var c = circles[i];
context.beginPath();
context.fillStyle = c.color // Set the color of the circle using key:valuecontext.fill();
context.lineWidth = strokewidth;
context.strokeStyle = strokestyle;
context.stroke();
context.arc(c.x, c.y, c.radius, 0, Math.PI * 2); // X-axis Position, y-axis Position, radius, % of fill, ?
context.closePath();
context.fill();
}
}
function animate() {
for (i = 0; i <= 2; i++) {
circles[i].x += circles[i].vx;
circles[i].y += circles[i].vy;
}
checkBounds();
draw();
}
var canvas = document.getElementById('ball-canvas');
var context = canvas.getContext('2d')
var radius = 50;
setInterval(animate, 40);
circles[0].x+=1;
circles[0].y+=-1.5;
That's pretty tough to maintain. Instead, I'd suggest you have properties for X and Y speeds (I used moveX and moveY in the example).
Next, you need to check whether the position of the ball + the radius compensation is touching the canvas edges, and if so, reverse the speed value. So, for example, the X speed of the ball is 4 and now it hits the left or the right canvas egde, the X speed now becomes -4.
This is it, in a nutshell:
var c = circles[i];
// check rebounds
if (c.x - c.radius <= 0 || c.x + c.radius >= canvas.width)
c.moveX = -c.moveX; // flip the horizontal speed component
if (c.y - c.radius <= 0 || c.y + c.radius >= canvas.height)
c.moveY = -c.moveY; // flip the vertical speed component
// Yellow Circle
c.x += c.moveX; // here we don't have to worry
c.y += c.moveY; // about directions anymore
See my example here: http://jsfiddle.net/3tfUN/8/
The same principle applies for collisions between balls. I'm assuming you want to do simple collisions without angle changes.
But if you wish to simulate real ball collisions, that would require some more serious trigonometry to calculate when exactly the pixel-perfect collision happens, and to calculate the new X and Y speed components.
UPDATE
An example featuring slightly improved collision detection and speed transfer between balls: http://jsfiddle.net/3tfUN/12/
The canvas is just a "canvas" where you draw the circles. What you need to accomplish what you want is to model a "world" where the circles are object with width and height dimensions and their current position, and where the bounds are well defined. Once you have the width and height of each circle and their position, you can calculate where they are in respect to the bounds you set and see if you need to change direction or keep going.
Collisions stem from the same principle but are somewhat harder to model if you want them to be "realistic" (in the bounds problem you are only interested in the width and height of the circles because the bounding area is box shaped and the circle will always collide in the furthest point from its center, while when two circles collide you should take into account the radius of each circle instead of the "bounding box" around them.
I don't have time right now to show you this concepts with examples, but hopefully I sent you in the right track :).

Categories