Which part of this movement code should I apply delta time? - javascript

this.up = function () {
this.velocity += this.lift;
};
this.update = function () {
this.velocity += this.gravity;
this.velocity *= 0.9;
this.y += this.velocity;
}
Hi, so I have this code in my canvas draw loop to make a shape move up and down, the thing is so its frame rate dependent so im trying to apply delta time to these functions. Anyone know how I'd go about doing that?
Help would be appreciated

requestAnimationFrame calls its callback with a timestamp. This allows you to track the time passed between two frames:
let t = null;
const frame = dt => {
// Update and render your canvas
};
const loop = ts => {
const dt = t === null ? 0 : ts - t;
t = ts;
frame(dt / 1000);
requestAnimationFrame(loop);
};
This example calls frame with the elapsed time in seconds. This means you can express your velocity and acceleration in pixels/second.
You'll get:
position in px
velocity in px/s
acceleration in px/s/s
Updating the three becomes something like:
px += vx * dt;
vx += ax * dt;
In the example below I've implemented a quick random animation. Try increasing the entity count using the number input.
For small number of entities, we're able to render at 60fps and using the real elapsed time doesn't matter much.
But as you increase the number of entities, you'll see stuff starting to move really slowly if we don't take the elapsed time in to account!
const G = 300; // px / s / s
const cvs = document.createElement("canvas");
cvs.width = 300;
cvs.height = 300;
cvs.style.border = "1px solid red";
const ctx = cvs.getContext("2d");
const Ball = (x, y, vx = 0, vy = 0, ax = 0, ay = G, color = "black") => {
const render = () => {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
};
const update = (dt) => {
return Ball(
x + dt * vx,
y + dt * vy,
vx + dt * ax,
vy + dt * ay,
ax,
ay,
color
);
}
return { render, update };
}
const randomBalls = n => Array.from(
{ length: n },
() => Ball(
Math.random() * 300,
Math.random() * 300,
-100 + Math.random() * 200,
Math.random() * -200,
0,
G,
`#${[
(Math.floor(Math.random() * 255)).toString(16),
(Math.floor(Math.random() * 255)).toString(16),
(Math.floor(Math.random() * 255)).toString(16)
].join("")}`
)
);
// Game
let balls = randomBalls(100);
let t = null;
let useFrameTime = true;
const frame = dt => {
if (!useFrameTime) dt = 1 / 60;
// Update entities
balls = balls.map(b => b.update(dt));
// Clear window
ctx.clearRect(0, 0, cvs.width, cvs.height);
// Draw new rects
balls.forEach(b => b.render());
}
const loop = ts => {
const dt = t === null ? 0 : ts - t;
t = ts;
frame(dt / 1000);
requestAnimationFrame(loop);
}
document.body.append(cvs);
requestAnimationFrame(loop);
document.querySelector("button").addEventListener("click", () => {
const n = +document.querySelector("input[type=number]").value;
balls = randomBalls(n);
});
document.querySelector("input[type=checkbox]").addEventListener("change", (e) => {
useFrameTime = e.target.checked;
});
body { display: flex; }
<div style="width: 300px">
<p>Increase the count here. Time for all balls to exit the frame should stay roughly the same, but you should see the frame rate drop as you increase the number of entities to render.
</p>
<input type="number" value="100">
<button>Go</button>
<br>
<label><input type="checkbox" checked> Use frame time in render loop</label>
</div>

Related

Infinite Loop Confetti Loop

I have created a fancy confetti animation but it seems to run infinitely. I only want it to run for a few seconds and stop gracefully. This is my first time working with requestAnimationFrame so I would not know where to stop the animation. I have tried to use a simple counter variable around the update but I have noticed that I have to multiply potential endless loops that will still run in the background eating resources. This snippet of code does not seem to fully stop all the functions from running in a never-ending loop.
let timer = 0;
const drawConfetti = () => {
ctx.clearRect(0, 0, wW, wH);
timer++;
confetties.forEach(confetti => {
if (timer < 1500){
confetti.update();
}else{
return
}
});
requestAnimationFrame(drawConfetti);
I have updated the code below to the first answer suggested but now the animation is stops mid rendering now.
const AMOUNT = 150;
const INTERVAL = 300;
const COLORS = ['#4579FF', '#29EAFC', '#FAB1C0', '#50E3C2', '#FFFC9D', '#1A04B3', '#F81C4D'];
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const wW = window.innerWidth;
const wH = window.innerHeight;
const random = (min, max) => {
return Math.random() * (max - min) + min;
};
const randomInt = (min, max) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
};
const confetties = [];
class Confetti {
constructor(width, height, color, speed, x, y, rotation) {
this.width = width;
this.height = height;
this.color = color;
this.speed = speed;
this.x = x;
this.y = y;
this.rotation = rotation;
}
update() {
const y = this.y < wH ? this.y += this.speed : -20;
const x = this.x + Math.sin(this.y * (this.speed / 100));
this.x = x > wW ? 0 : x;
this.y = y;
ctx.fillStyle = this.color;
ctx.save();
ctx.translate(x + (this.width / 2), y + (this.height / 2));
ctx.rotate((y * this.speed) * Math.PI / 180);
ctx.scale(Math.cos(y / 10), 1);
ctx.fillRect(-this.width / 2, -this.height / 2,
this.width,
this.height
);
ctx.restore();
}
}
canvas.width = wW;
canvas.height = wH;
let animationId;
const drawConfetti = () => {
ctx.clearRect(0, 0, wW, wH);
confetties.forEach(confetti => {
confetti.update();
});
animationId = requestAnimationFrame(drawConfetti);
}
const renderConfetti = () => {
let count = 0;
let stagger = setInterval(() => {
if (count < AMOUNT) {
const x = random(0, wW);
const speed = random(1.0, 2.0);
const width = 24 / speed;
const height = 48 / speed;
const color = COLORS[randomInt(0, COLORS.length)];
const confetti = new Confetti(width, height, color, speed, x, -20, 0);
confetties.push(confetti);
} else {
clearInterval(stagger);
}
count++;
}, INTERVAL);
drawConfetti();
}
renderConfetti();
function stop() {
cancelAnimationFrame(animationId)
}
setTimeout(stop, 5000);
body {
margin: 0;
background-color: #000;
}
<canvas id="canvas"></canvas>
The requestAnimationFrame funciton is being called recursively. If you want to stop it, you should use cancelAnimationFrame. To do this, save the ID of the animation outside of the drawLoop function.
e.g
let animationId;
const drawConfetti = () => {
ctx.clearRect(0, 0, wW, wH);
confetties.forEach(confetti => {
confetti.update();
});
animationId = requestAnimationFrame(drawConfetti);
}
// now cancel whenever you want
cancelAnimationFrame(animationId)

JavaScript Smoother Multiplication Circle

The Idea
I came across this idea of multiplication circles from a YouTube video that I stumbled upon and I thought that would be a fun thing to try and recreate using JavasSript and the canvas element.
The Original Video
The Problem
I smoothed out the animation the best I could but it still doesn't look as proper as I'd like. I suspect coming up with a solution would require a decent amount of math. To grasp the problem in detail I think it's easier to look at the code
window.onload = () => {
const app = document.querySelector('#app')
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const { cos, sin, PI } = Math
const Tao = PI * 2
const width = window.innerWidth
const height = window.innerHeight
const cx = width / 2
const cy = height / 2
const baseNumberingSystem = 200
const stop = 34
let color = 'teal'
let multiplier = 0
let drawQue = []
// setup canvas
canvas.width = width
canvas.height = height
class Circle {
constructor(x, y, r, strokeColor, fillColor) {
this.x = x
this.y = y
this.r = r
this.strokeColor = strokeColor || '#fff'
this.fillColor = fillColor || '#fff'
}
draw(stroke, fill) {
ctx.moveTo(this.x, this.y)
ctx.beginPath()
ctx.arc(this.x, this.y, this.r, 0, Tao)
ctx.closePath()
if (fill) {
ctx.fillStyle = this.fillColor
ctx.fill()
}
if (stroke) {
ctx.strokeStyle = this.strokeColor
ctx.stroke()
}
}
createChildCircleAt(degree, radius, strokeColor, fillColor) {
const radian = degreeToRadian(degree)
const x = this.x + (this.r * cos(radian))
const y = this.y + (this.r * sin(radian))
return new Circle(x, y, radius, strokeColor, fillColor)
}
divideCircle(nTimes, radius) {
const degree = 360 / nTimes
let division = 1;
while (division <= nTimes) {
drawQue.push(this.createChildCircleAt(division * degree, radius))
division++
}
}
}
function degreeToRadian(degree) {
return degree * (PI / 180)
}
function draw() {
const mainCircle = new Circle(cx, cy, cy * 0.9)
// empty the que
drawQue = []
// clear canvas
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = "black"
ctx.fillRect(0, 0, width, height)
// redraw everything
mainCircle.draw()
mainCircle.divideCircle(baseNumberingSystem, 4)
drawQue.forEach(item => item.draw())
// draw modular times table
for (let i = 1; i <= drawQue.length; i++) {
const product = i * multiplier;
const firstPoint = drawQue[i]
const secondPoint = drawQue[product % drawQue.length]
if (firstPoint && secondPoint) {
ctx.beginPath()
ctx.moveTo(firstPoint.x, firstPoint.y)
ctx.strokeStyle = color
ctx.lineTo(secondPoint.x, secondPoint.y)
ctx.closePath()
ctx.stroke()
}
}
}
function animate() {
multiplier+= 0.1
multiplier = parseFloat(multiplier.toFixed(2))
draw()
console.log(multiplier, stop)
if (multiplier === stop) {
clearInterval(animation)
}
}
app.appendChild(canvas)
let animation = setInterval(animate, 120)
}
So the main issue comes from when we increment the multiplier by values less than 1 in an attempt to make the animation more fluid feeling. Example: multiplier+= 0.1. When we do this it increase the amount of times our if block in our draw function will fail because the secondPoint will return null.
const product = i * multiplier; // this is sometimes a decimal
const firstPoint = drawQue[i]
const secondPoint = drawQue[product % drawQue.length] // therefore this will often not be found
// Then this if block wont execute. Which is good because if it did we the code would crash
// But I think what we need is an if clause to still draw a line to a value in between the two
// closest indices of our drawQue
if (firstPoint && secondPoint) {
//...
}
Possible Solution
I think what I'd need to do is when we fail to find the secondPoint get the remainder of product % drawQue.length and use that to create a new circle in between the two closest circles in the drawQue array and use that new circle as the second point of our line.
If you use requestAnimationFrame it looks smooth
function animate() {
if (multiplier != stop) {
multiplier+= 0.1
multiplier = parseFloat(multiplier.toFixed(2))
draw()
requestAnimationFrame(animate);
}
}
app.appendChild(canvas)
animate()
My possible solution ended up working. I'll leave the added else if block here for anyone whos interested. I also had to store the degree value in my circle objects when they were made as well as calculate the distance between each subdivision of the circle.
Added If Else Statement
for (let i = 1; i <= drawQue.length; i++) {
const product = i * multiplier;
const newIndex = product % drawQue.length
const firstPoint = drawQue[i]
const secondPoint = drawQue[newIndex]
if (firstPoint && secondPoint) {
ctx.beginPath()
ctx.moveTo(firstPoint.x, firstPoint.y)
ctx.strokeStyle = color
ctx.lineTo(secondPoint.x, secondPoint.y)
ctx.closePath()
ctx.stroke()
} else if (!secondPoint) {
const percent = newIndex % 1
const closest = drawQue[Math.floor(newIndex)]
const newDegree = closest.degree + (degreeIncriments * percent)
const target = mainCircle.createChildCircleAt(newDegree, 4)
if (firstPoint && target) {
ctx.beginPath()
ctx.moveTo(firstPoint.x, firstPoint.y)
ctx.strokeStyle = color
ctx.lineTo(target.x, target.y)
ctx.closePath()
ctx.stroke()
}
}
Other changes
// ...
const degreeIncriments = 360 / baseNumberingSystem
// ...
class Circle {
constructor(/* ... */, degree )
// ...
this.degree = degree || 0
}
Hope someone finds this useful...

gpu.js "Error: not enough arguments for kernel"

I have some heavily calculating canvas effects in a react app that I'm making. I have been trying to figure out how to do it for about 2 days, and It's almost working but I have this one error that I can't seem to figure out.
Here is the code running the calculation for one of the effects:
drawBubbles = () => {
const ctx = this.Canvas.getContext("2d");
ctx.strokeStyle = "white";
ctx.globalAlpha = 0.6;
if (this.bubbles.length) {
const gpu = new GPU();
const kernel = gpu
.createKernel(function ([
bubbles,
canvasWidth,
canvasHeight,
beginPath,
arc,
stroke,
]) {
const bubble = bubbles[this.thread.x];
bubble[1] -= bubble[2];
if (bubble[1] + bubble[2] * 2 < 0) {
bubble[1] = canvasHeight + bubble[2] * 2;
bubble[0] = Math.random() * canvasWidth;
}
if (Math.floor(Math.random() * 2) === 0) {
bubble[0] += Math.random() * 6 - 2.5;
}
ctx.lineWidth = bubble[2] / 2.5;
beginPath();
arc(bubble[0], bubble[1], bubble[2] * 2, 0, 2 * Math.PI);
stroke();
return bubble;
})
.setOutput([this.bubbles.length]);
this.bubbles = kernel([
this.bubbles,
this.Canvas.width,
this.Canvas.height,
ctx.beginPath,
ctx.arc,
ctx.stroke,
]);
} else {
for (let i = 0; i < 15; i++) {
this.bubbles.push([
// 0 = X
Math.random() * this.Canvas.width,
// 1 = Y
Math.random() * this.Canvas.height,
// 2 = Size/Speed
Math.random() * 3 + 1,
]);
}
}
};
I got nothing about it when I looked it up, and if I don't implement this then my app will be going at 10-20 fps (or lower on slower computers)

How to draw only visible part of the tilemap on js canvas?

I created simple tilemap using Tiled (3200 x 3200 pixels). I loaded it on my canvas using this library
I draw entire tilemap 3200 x 3200 60 times per seocnd.
I tried to move around and it works fine. Btw, I move around canvas using ctx.translate. I included this in my own function
But when I created bigger map in Tiled ( 32000 x 32000 pixels ) - I got a very freezing page. I couldn't move around fast, I think there was about 10 fps
So how to fix it? I have to call drawTiles() function 60 times per second. But is there any way to draw only visible part of the tile? Like draw only what I see on my screen (0, 0, monitorWidth, monitorHeight I guess)
Thank you
##Drawing a large tileset
If you have a large tile set and only see part of it in the canvas you just need to calculate the tile at the top left of the canvas and the number of tiles across and down that will fit the canvas.
Then draw the square array of tiles that fit the canvas.
In the example the tile set is 1024 by 1024 tiles (worldTileCount = 1024), each tile is 64 by 64 pixels tileSize = 64, making the total playfield 65536 pixels square
The position of the top left tile is set by the variables worldX, worldY
###Function to draw tiles
// val | 0 is the same as Math.floor(val)
var worldX = 512 * tileSize; // pixel position of playfield
var worldY = 512 * tileSize;
function drawWorld(){
const c = worldTileCount; // get the width of the tile array
const s = tileSize; // get the tile size in pixels
// get the tile position
const tx = worldX / s | 0; // get the top left tile
const ty = worldY / s | 0;
// get the number of tiles that will fit the canvas
const tW = (canvas.width / s | 0) + 2;
const tH = (canvas.height / s | 0) + 2;
// set the location. Must floor to pixel boundary or you get holes
ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);
// Draw the tiles across and down
for(var y = 0; y < tH; y += 1){
for(var x = 0; x < tW; x += 1){
// get the index into the tile array for the tile at x,y plus the topleft tile
const i = tx + x + (ty + y) * c;
// get the tile id from the tileMap. If outside map default to tile 6
const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
// draw the tile at its location. last 2 args are x,y pixel location
imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
}
}
}
###setTransform and absolute coordinates.
Use absolute coordinates makes everything simple.
Use the canvas context setTransform to set the world position and then each tile can be drawn at its own coordinate.
// set the world location. The | 0 floors the values and ensures no holes
ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);
That way if you have a character at position 51023, 34256 you can just draw it at that location.
playerX = 51023;
playerY = 34256;
ctx.drawImage(myPlayerImage,playerX,playerY);
If you want the tile map relative to the player then just set the world position to be half the canvas size up and to the left plus one tile to ensure overlap
playerX = 51023;
playerY = 34256;
worldX = playerX - canvas.width / 2 - tileWidth;
worldY = playerY - canvas.height / 2 - tileHeight;
###Demo of large 65536 by 65536 pixel tile map.
At 60fps if you have the horses and can handle much much bigger without any frame rate loss. (map size limit using this method is approx 4,000,000,000 by 4,000,000,000pixels (32 bit integers coordinates))
#UPDATE 15/5/2019 re Jitter
The comments have pointed out that there is some jitter as the map scrolls.
I have made changes to smooth out the random path with a strong ease in out turn every 240 frame (4 seconds at 60fps) Also added a frame rate reducer, if you click and hold the mouse button on the canvas the frame rate will be slowed to 1/8th normal so that the jitter is easier to see.
There are two reasons for the jitter.
###Time error
The first and least is the time passed to the update function by requestAnimationFrame, the interval is not perfect and rounding errors due to the time is compounding the alignment problems.
To reduce the time error I have set the move speed to a constant interval to minimize the rounding error drift between frames.
###Aligning tiles to pixels
The main reason for the jitter is that the tiles must be rendered on pixel boundaries. If not then aliasing errors will create visible seams between tiles.
To see the difference click the button top left to toggle pixel alignment on and off.
To get smooth scrolling (sub pixel positioning) draw the map to an offscreen canvas aligning to the pixels, then render that canvas to the display canvas adding the sub pixel offset. That will give the best possible result using the canvas. For better you will need to use webGL
###End of update
var refereshSkip = false; // when true drops frame rate by 4
var dontAlignToPixel = false;
var ctx = canvas.getContext("2d");
function mouseEvent(e) {
if(e.type === "click") {
dontAlignToPixel = !dontAlignToPixel;
pixAlignInfo.textContent = dontAlignToPixel ? "Pixel Align is OFF" : "Pixel Align is ON";
} else {
refereshSkip = e.type === "mousedown";
}
}
pixAlignInfo.addEventListener("click",mouseEvent);
canvas.addEventListener("mousedown",mouseEvent);
canvas.addEventListener("mouseup",mouseEvent);
// wait for code under this to setup
setTimeout(() => {
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// create tile map
const worldTileCount = 1024;
const tileMap = new Uint8Array(worldTileCount * worldTileCount);
// add random tiles
doFor(worldTileCount * worldTileCount, i => {
tileMap[i] = randI(1, tileCount);
});
// this is the movement direction of the map
var worldDir = Math.PI / 4;
/* =======================================================================
Drawing the tileMap
========================================================================*/
var worldX = 512 * tileSize;
var worldY = 512 * tileSize;
function drawWorld() {
const c = worldTileCount; // get the width of the tile array
const s = tileSize; // get the tile size in pixels
const tx = worldX / s | 0; // get the top left tile
const ty = worldY / s | 0;
const tW = (canvas.width / s | 0) + 2; // get the number of tiles to fit canvas
const tH = (canvas.height / s | 0) + 2;
// set the location
if(dontAlignToPixel) {
ctx.setTransform(1, 0, 0, 1, -worldX,-worldY);
} else {
ctx.setTransform(1, 0, 0, 1, Math.floor(-worldX),Math.floor(-worldY));
}
// Draw the tiles
for (var y = 0; y < tH; y += 1) {
for (var x = 0; x < tW; x += 1) {
const i = tx + x + (ty + y) * c;
const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
}
}
}
var timer = 0;
var refreshFrames = 0;
const dirChangeMax = 3.5;
const framesBetweenDirChange = 240;
var dirChangeDelay = 1;
var dirChange = 0;
var prevDir = worldDir;
const eCurve = (v, p = 2) => v < 0 ? 0 : v > 1 ? 1 : v ** p / (v ** p + (1 - v) ** p);
//==============================================================
// main render function
function update() {
refreshFrames ++;
if(!refereshSkip || (refereshSkip && refreshFrames % 8 === 0)){
timer += 1000 / 60;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
// Move the map
var speed = Math.sin(timer / 10000) * 8;
worldX += Math.cos(worldDir) * speed;
worldY += Math.sin(worldDir) * speed;
if(dirChangeDelay-- <= 0) {
dirChangeDelay = framesBetweenDirChange;
prevDir = worldDir = prevDir + dirChange;
dirChange = rand(-dirChangeMax , dirChangeMax);
}
worldDir = prevDir + (1-eCurve(dirChangeDelay / framesBetweenDirChange,3)) * dirChange;
// Draw the map
drawWorld();
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
}, 0);
/*===========================================================================
CODE FROM HERE DOWN UNRELATED TO THE ANSWER
===========================================================================*/
const imageTools = (function() {
// This interface is as is. No warenties no garenties, and NOT to be used comercialy
var workImg, workImg1, keep; // for internal use
keep = false;
var tools = {
canvas(width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage: function(width, height) {
var i = this.canvas(width, height);
i.ctx = i.getContext("2d");
return i;
},
drawSpriteQuick: function(image, spriteIndex, x, y) {
var w, h, spr;
spr = image.sprites[spriteIndex];
w = spr.w;
h = spr.h;
ctx.drawImage(image, spr.x, spr.y, w, h, x, y, w, h);
},
line(x1, y1, x2, y2) {
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
},
circle(x, y, r) {
ctx.moveTo(x + r, y);
ctx.arc(x, y, r, 0, Math.PI * 2);
},
};
return tools;
})();
const doFor = (count, cb) => {
var i = 0;
while (i < count && cb(i++) !== true);
}; // the ; after while loop is important don't remove
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const seededRandom = (() => {
var seed = 1;
return {
max: 2576436549074795,
reseed(s) {
seed = s
},
random() {
return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max
}
}
})();
const randSeed = (seed) => seededRandom.reseed(seed | 0);
const randSI = (min, max = min + (min = 0)) => (seededRandom.random() % (max - min)) + min;
const randS = (min = 1, max = min + (min = 0)) => (seededRandom.random() / seededRandom.max) * (max - min) + min;
const tileSize = 64;
const tileCount = 7;
function drawGrass(ctx, c1, c2, c3) {
const s = tileSize;
const gs = s / (8 * c3);
ctx.fillStyle = c1;
ctx.fillRect(0, 0, s, s);
ctx.strokeStyle = c2;
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.beginPath();
doFor(s, i => {
const x = rand(-gs, s + gs);
const y = rand(-gs, s + gs);
const x1 = rand(x - gs, x + gs);
const y1 = rand(y - gs, y + gs);
imageTools.line(x, y, x1, y1);
imageTools.line(x + s, y, x1 + s, y1);
imageTools.line(x - s, y, x1 - s, y1);
imageTools.line(x, y + s, x1, y1 + s);
imageTools.line(x, y - s, x1, y1 - s);
})
ctx.stroke();
}
function drawTree(ctx, c1, c2, c3) {
const seed = Date.now();
const s = tileSize;
const gs = s / 2;
const gh = gs / 2;
ctx.fillStyle = c1;
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
ctx.save();
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 8;
ctx.shadowOffsetY = 8;
randSeed(seed);
ctx.beginPath();
doFor(18, i => {
const ss = 1 - i / 18;
imageTools.circle(randS(gs - gh * ss, gs + gh * ss), randS(gs - gh * ss, gs + gh * ss), randS(gh / 4, gh / 2));
})
ctx.stroke();
ctx.fill();
ctx.restore();
ctx.fillStyle = c2;
ctx.strokeStyle = c3;
ctx.lineWidth = 2;
ctx.save();
randSeed(seed);
ctx.beginPath();
doFor(18, i => {
const ss = 1 - i / 18;
imageTools.circle(randS(gs - gh * ss, gs + gh * ss) - 2, randS(gs - gh * ss, gs + gh * ss) - 2, randS(gh / 4, gh / 2) / 1.6);
})
ctx.stroke();
ctx.fill();
ctx.restore();
}
const tileRenders = [
(ctx) => {
drawGrass(ctx, "#4C4", "#4F4", 1)
},
(ctx) => {
drawGrass(ctx, "#644", "#844", 2)
},
(ctx) => {
tileRenders[0](ctx);
drawTree(ctx, "#480", "#8E0", "#7C0")
},
(ctx) => {
tileRenders[1](ctx);
drawTree(ctx, "#680", "#AE0", "#8C0")
},
(ctx) => {
drawGrass(ctx, "#008", "#00A", 4)
},
(ctx) => {
drawGrass(ctx, "#009", "#00C", 4)
},
(ctx) => {
drawGrass(ctx, "#00B", "#00D", 4)
},
]
const tileSet = imageTools.createImage(tileSize * tileCount, tileSize);
const ctxMain = ctx;
ctx = tileSet.ctx;
tileSet.sprites = [];
doFor(tileCount, i => {
x = i * tileSize;
ctx.save();
ctx.setTransform(1, 0, 0, 1, x, 0);
ctx.beginPath();
ctx.rect(0, 0, tileSize, tileSize);
ctx.clip()
if (tileRenders[i]) {
tileRenders[i](ctx)
}
tileSet.sprites.push({
x,
y: 0,
w: tileSize,
h: tileSize
});
ctx.restore();
});
ctx = ctxMain;
canvas {
position: absolute;
top: 0px;
left: 0px;
}
div {
position: absolute;
top: 8px;
left: 8px;
color: white;
}
#pixAlignInfo {
color: yellow;
cursor: pointer;
border: 2px solid green;
margin: 4px;
}
#pixAlignInfo:hover {
color: white;
background: #0008;
cursor: pointer;
}
body {
background: #49c;
}
<canvas id="canvas"></canvas>
<div>Hold left button to slow to 1/8th<br>
<span id="pixAlignInfo">Click this button to toggle pixel alignment. Alignment is ON</span></div>

How do I allow a 2D shape to wrap around a canvas using WebGL?

I have created a simple animation in WebGL (html & javascript) where a 2D shape is animated and manipulated on a canvas. The default animation is shape moving to the right at a set speed and then using "WASD" changes its direction. The shape moves in the given direction indefinitely, even after it is off of the canvas and out of the clip-space. I would like to have the shape wrap around the canvas instead of just continuing even after it is unseen. For example, if the shape is moving to the right and moves completely off of the canvas, I would like it to appear on left side still moving to the right and continue cycling. Same goes for if it is moving left or up or down.
Any suggestions on how to implement this?
You have to draw it 2 to 4 times depending on if you want to wrap both left/right and top/bottom
Assume we only want to wrap around horizontally. If the player is near the left edge we need to also draw the player 1 screen width to the right. If the player is near the right edge we need to draw the player again one screen to the left. Same with up and down
Here's an example using canvas 2D but the only difference for WebGL is you'd draw using WebGL. The concept is the same.
Example:
var x = 150;
var y = 100;
var vx = 0;
var vy = 0;
const maxSpeed = 250;
const acceleration = 1000;
const ctx = document.querySelector("canvas").getContext("2d");
const keys = {};
const LEFT = 65;
const RIGHT = 68;
const DOWN = 83;
const UP = 87;
const width = ctx.canvas.width;
const height = ctx.canvas.height;
var then = 0;
function render(now) {
now *= 0.001; // seconds
const deltaTime = now - then;
then = now;
ctx.clearRect(0, 0, width, height);
if (keys[UP]) { vy -= acceleration * deltaTime; }
if (keys[DOWN]) { vy += acceleration * deltaTime; }
if (keys[LEFT]) { vx -= acceleration * deltaTime; }
if (keys[RIGHT]) { vx += acceleration * deltaTime; }
// keep speed under max
vx = absmin(vx, maxSpeed);
vy = absmin(vy, maxSpeed);
// move based on velociy
x += vx * deltaTime;
y += vy * deltaTime;
// wrap
x = euclideanModulo(x, width);
y = euclideanModulo(y, height);
// draw player 4 times
const xoff = x < width / 2 ? width : -width;
const yoff = y < height / 2 ? height : -height;
drawPlayer(x, y);
drawPlayer(x + xoff, y);
drawPlayer(x, y + yoff);
drawPlayer(x + xoff, y + yoff);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function drawPlayer(x, y) {
ctx.fillStyle = "blue";
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(x, y, 20, 0, Math.PI * 2, false);
ctx.fill();
ctx.stroke();
}
function absmin(v, max) {
return Math.min(Math.abs(v), max) * Math.sign(v);
}
function euclideanModulo(n, m) {
return ((n % m) + m) % m;
}
window.addEventListener('keydown', e => {
keys[e.keyCode] = true;
});
window.addEventListener('keyup', e => {
keys[e.keyCode] = false;
});
canvas {
display: block;
border: 1px solid black;
}
<canvas></canvas>
<p><span style="color:red;">click here</span> then use ASWD to move</p>
A WebGL version changes no code related to wrapping.
var x = 150;
var y = 100;
var vx = 0;
var vy = 0;
const maxSpeed = 250;
const acceleration = 1000;
const gl = document.querySelector("canvas").getContext("webgl");
const keys = {};
const LEFT = 65;
const RIGHT = 68;
const DOWN = 83;
const UP = 87;
const width = gl.canvas.width;
const height = gl.canvas.height;
var program = setupWebGL();
var positionLoc = gl.getAttribLocation(program, "position");
var then = 0;
function render(now) {
now *= 0.001; // seconds
const deltaTime = now - then;
then = now;
if (keys[UP]) { vy -= acceleration * deltaTime; }
if (keys[DOWN]) { vy += acceleration * deltaTime; }
if (keys[LEFT]) { vx -= acceleration * deltaTime; }
if (keys[RIGHT]) { vx += acceleration * deltaTime; }
// keep speed under max
vx = absmin(vx, maxSpeed);
vy = absmin(vy, maxSpeed);
// move based on velociy
x += vx * deltaTime;
y += vy * deltaTime;
// wrap
x = euclideanModulo(x, width);
y = euclideanModulo(y, height);
// draw player 4 times
const xoff = x < width / 2 ? width : -width;
const yoff = y < height / 2 ? height : -height;
drawPlayer(x, y);
drawPlayer(x + xoff, y);
drawPlayer(x, y + yoff);
drawPlayer(x + xoff, y + yoff);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function drawPlayer(x, y) {
gl.useProgram(program);
// only drawing a single point so no need to use a buffer
gl.vertexAttrib2f(
positionLoc,
x / width * 2 - 1,
y / height * -2 + 1);
gl.drawArrays(gl.POINTS, 0, 1);
}
function absmin(v, max) {
return Math.min(Math.abs(v), max) * Math.sign(v);
}
function euclideanModulo(n, m) {
return ((n % m) + m) % m;
}
window.addEventListener('keydown', e => {
keys[e.keyCode] = true;
});
window.addEventListener('keyup', e => {
keys[e.keyCode] = false;
});
function setupWebGL() {
const vs = `
attribute vec4 position;
void main() {
gl_Position = position;
gl_PointSize = 40.;
}
`;
const fs = `
void main() {
gl_FragColor = vec4(1,0,1,1);
}
`;
// compiles and links shaders and assigns position to location 0
return twgl.createProgramFromSources(gl, [vs, fs]);
}
canvas {
display: block;
border: 1px solid black;
}
<canvas></canvas>
<p><span style="color:red;">click here</span> then use ASWD to move</p>
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
If you don't want the player appear on both sides then your question has nothing to do with graphics, you just wait until the player's x position is at least screenWidth + haflPlayerWidth which means they're completely off the right side and then you set their x position to -halfPlayerWidth which will put them just off the left and visa versa
var x = 150;
var y = 100;
var vx = 0;
var vy = 0;
const maxSpeed = 250;
const acceleration = 1000;
const ctx = document.querySelector("canvas").getContext("2d");
const keys = {};
const LEFT = 65;
const RIGHT = 68;
const DOWN = 83;
const UP = 87;
const width = ctx.canvas.width;
const height = ctx.canvas.height;
const playerSize = 40;
const halfPlayerSize = playerSize / 2;
var then = 0;
function render(now) {
now *= 0.001; // seconds
const deltaTime = now - then;
then = now;
ctx.clearRect(0, 0, width, height);
if (keys[UP]) { vy -= acceleration * deltaTime; }
if (keys[DOWN]) { vy += acceleration * deltaTime; }
if (keys[LEFT]) { vx -= acceleration * deltaTime; }
if (keys[RIGHT]) { vx += acceleration * deltaTime; }
// keep speed under max
vx = absmin(vx, maxSpeed);
vy = absmin(vy, maxSpeed);
// move based on velociy
x += vx * deltaTime;
y += vy * deltaTime;
// wrap
x = euclideanModulo(x + halfPlayerSize, width + playerSize) - halfPlayerSize;
y = euclideanModulo(y + halfPlayerSize, height + playerSize) - halfPlayerSize;
// draw player
drawPlayer(x, y);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function drawPlayer(x, y) {
ctx.fillStyle = "blue";
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(x, y, halfPlayerSize, 0, Math.PI * 2, false);
ctx.fill();
ctx.stroke();
}
function absmin(v, max) {
return Math.min(Math.abs(v), max) * Math.sign(v);
}
function euclideanModulo(n, m) {
return ((n % m) + m) % m;
}
window.addEventListener('keydown', e => {
keys[e.keyCode] = true;
});
window.addEventListener('keyup', e => {
keys[e.keyCode] = false;
});
canvas {
display: block;
border: 1px solid black;
}
<canvas></canvas>
<p><span style="color:red;">click here</span> then use ASWD to move</p>
this code probably needs an explanation
x = euclideanModulo(x + haflPlayerSize, width + playerSize) - haflPlayerSize;
y = euclideanModulo(y + haflPlayerSize, height + playerSize) - haflPlayerSize;
First off euclideanModulo is just like normal % modulo operator, it returns the remainder after division, except euclidean modulo keeps the same remainder even for negative numbers. In other words
3 % 5 = 3
8 % 5 = 3
13 % 5 = 3
-2 % 5 = -2
-7 % 5 = -2
-12 % 5 = -2
but
3 euclideanMod 5 = 3
8 euclideanMod 5 = 3
13 euclideanMod 5 = 3
-2 euclideanMod 5 = 3
-7 euclideanMod 5 = 3
-12 euclideanMod 5 = 3
So it's a super easy way to wrap things.
x = euclideanModulo(x, screenWidth)
Is similar to
if (x < 0) x += screenWidth;
if (x >= screenWidth) x -= screenWidth;
Except those would fail if x > screenWidth * 2 for example where as the one using euclideanModulo would not.
So, back to
x = euclideanModulo(x + haflPlayerSize, width + playerSize) - haflPlayerSize;
y = euclideanModulo(y + haflPlayerSize, height + playerSize) - haflPlayerSize;
We know the player (in this case a circle) has its position at its center. So, we know when its center is half the playerSize off the left or right of the screen it's completely off the screen and we therefore want to move it to the other side. That means we can imagine the screen is really width + halfPlayerSize + halfPlayerSize wide. The first halfPlayerSize is for the stepping off the left side, the second halfPlayerSize is for stepping off the right side. In other words it's just width + playerSize wide. We then want the player to wrap from left to right when x < -halfPlayerSize. So we add halfPlayerSize to the player's position, then do the euclideanModulo which will wrap the position, then subtract that halfPlayerSize back out.

Categories