From 3c68009aa7984ee1cdafae6dade738cd816d0969 Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Tue, 30 Aug 2022 00:17:03 -0700 Subject: [PATCH] Make Framerate independent Not sure I got everyone perfect but, the effects assumes 60fps. At least in Chrome on M1 Macs they run at 120fps so all the effects run too fast (unless that was the intent) One thing I didn't fix, at 120fps, effects that spawn particles spawn twice as many particles. Note: normally I'd compute deltaTime in seconds as in ``` const deltaTime = (time - lastTime) / 1000 ``` But, all the calculations in the code assumed 60 fps so it seemed easier to pass in a 60fps clock, meaning, if the frame rate is 60fps then deltaTime will equal 1.0 and all the calculations will be the same. If fthe frame rate is 120fps then deltaTime will equal 0.5. If the frame rate is 30fps then deltaTime will equal 2.0. As for the limit, if the user changes tabs then requestAnimationFrame stop firing. When you get back deltaTime might be giant which could break calculations so the code limits the framerate to 10fps. Below that things will always run at 10fps. This is also useful for debugging because otherwise, stepping through the code, deltaTime would be giant. --- src/bubbleCursor.js | 26 ++++++++++++++------------ src/clockCursor.js | 19 +++++++++++-------- src/emojiCursor.js | 23 +++++++++++++---------- src/fairyDustCursor.js | 23 +++++++++++++---------- src/followingDotCursor.js | 19 +++++++++++-------- src/ghostCursor.js | 17 ++++++++++------- src/rainbowCursor.js | 15 +++++++++------ src/snowflakeCursor.js | 25 ++++++++++++++----------- src/springyEmojiCursor.js | 19 +++++++++++-------- src/trailingCursor.js | 11 +++++++---- 10 files changed, 113 insertions(+), 84 deletions(-) diff --git a/src/bubbleCursor.js b/src/bubbleCursor.js index 8626ec0..0b5ed41 100644 --- a/src/bubbleCursor.js +++ b/src/bubbleCursor.js @@ -9,6 +9,7 @@ export function bubbleCursor(options) { let canvas, context let canvImages = [] + let lastTime = 0; function init(wrapperEl) { canvas = document.createElement("canvas") @@ -31,7 +32,7 @@ export function bubbleCursor(options) { } bindEvents() - loop() + requestAnimationFrame(loop); } // Bind events that are needed @@ -84,12 +85,12 @@ export function bubbleCursor(options) { particles.push(new Particle(x, y, img)) } - function updateParticles() { + function updateParticles(deltaTime) { context.clearRect(0, 0, width, height) // Update for (let i = 0; i < particles.length; i++) { - particles[i].update(context) + particles[i].update(context, deltaTime); } // Remove dead particles @@ -100,8 +101,10 @@ export function bubbleCursor(options) { } } - function loop() { - updateParticles() + function loop(time) { + const deltaTime = Math.min(100, time - lastTime) / (1000 / 60); + lastTime = time; + updateParticles(deltaTime) requestAnimationFrame(loop) } @@ -118,13 +121,12 @@ export function bubbleCursor(options) { this.baseDimension = 4 - this.update = function(context) { - this.position.x += this.velocity.x - this.position.y += this.velocity.y - this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75 - this.velocity.y -= Math.random() / 600 - this.lifeSpan-- - + this.update = function(context, deltaTime) { + this.position.x += this.velocity.x * deltaTime + this.position.y += this.velocity.y * deltaTime + this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75 * deltaTime + this.velocity.y -= Math.random() / 600 * deltaTime + this.lifeSpan -= deltaTime; const scale = 0.2 + (this.initialLifeSpan - this.lifeSpan) / this.initialLifeSpan diff --git a/src/clockCursor.js b/src/clockCursor.js index 83b5504..87b2f85 100644 --- a/src/clockCursor.js +++ b/src/clockCursor.js @@ -10,6 +10,7 @@ export function clockCursor(options) { let height = window.innerHeight; let cursor = { x: width / 2, y: width / 2 }; let canvas, context; + let lastTime = 0; const dateColor = (options && options.dateColor) || "blue"; const faceColor = (options && options.faceColor) || "black"; @@ -182,7 +183,7 @@ export function clockCursor(options) { } bindEvents(); - loop(); + requestAnimationFrame(loop); } // Bind events that are needed @@ -230,14 +231,14 @@ export function clockCursor(options) { } } - function updatePositions() { + function updatePositions(deltaTime) { let widthBuffer = 80; - zy[0] = Math.round((dy[0] += (cursor.y - dy[0]) * del)); - zx[0] = Math.round((dx[0] += (cursor.x - dx[0]) * del)); + zy[0] = Math.round((dy[0] += (cursor.y - dy[0]) * del * deltaTime)); + zx[0] = Math.round((dx[0] += (cursor.x - dx[0]) * del * deltaTime)); for (let i = 1; i < sum; i++) { - zy[i] = Math.round((dy[i] += (zy[i - 1] - dy[i]) * del)); - zx[i] = Math.round((dx[i] += (zx[i - 1] - dx[i]) * del)); + zy[i] = Math.round((dy[i] += (zy[i - 1] - dy[i]) * del * deltaTime)); + zx[i] = Math.round((dx[i] += (zx[i - 1] - dx[i]) * del * deltaTime)); if (dy[i - 1] >= height - 80) dy[i - 1] = height - 80; if (dx[i - 1] >= width - widthBuffer) dx[i - 1] = width - widthBuffer; } @@ -315,8 +316,10 @@ export function clockCursor(options) { } } - function loop() { - updatePositions(); + function loop(time) { + const deltaTime = Math.min(100, time - lastTime) / (1000 / 60); + lastTime = time; + updatePositions(deltaTime); updateParticles(); requestAnimationFrame(loop); diff --git a/src/emojiCursor.js b/src/emojiCursor.js index 21519ec..ac4a7ad 100644 --- a/src/emojiCursor.js +++ b/src/emojiCursor.js @@ -11,6 +11,7 @@ export function emojiCursor(options) { const particles = [] const canvImages = [] let canvas, context + let lastTime = 0; function init() { canvas = document.createElement("canvas") @@ -57,7 +58,7 @@ export function emojiCursor(options) { }) bindEvents() - loop() + requestAnimationFrame(loop); } // Bind events that are needed @@ -132,12 +133,12 @@ export function emojiCursor(options) { particles.push(new Particle(x, y, img)) } - function updateParticles() { + function updateParticles(deltaTime) { context.clearRect(0, 0, width, height) // Update for (let i = 0; i < particles.length; i++) { - particles[i].update(context) + particles[i].update(context, deltaTime) } // Remove dead particles @@ -148,8 +149,10 @@ export function emojiCursor(options) { } } - function loop() { - updateParticles() + function loop(time) { + const deltaTime = Math.min(100, time - lastTime) / (1000 / 60); + lastTime = time; + updateParticles(deltaTime) requestAnimationFrame(loop) } @@ -168,12 +171,12 @@ export function emojiCursor(options) { this.position = { x: x, y: y } this.canv = canvasItem - this.update = function(context) { - this.position.x += this.velocity.x - this.position.y += this.velocity.y - this.lifeSpan-- + this.update = function(context, deltaTime) { + this.position.x += this.velocity.x * deltaTime + this.position.y += this.velocity.y * deltaTime + this.lifeSpan -= deltaTime; - this.velocity.y += 0.05 + this.velocity.y += 0.05 * deltaTime; const scale = Math.max(this.lifeSpan / this.initialLifeSpan, 0) diff --git a/src/fairyDustCursor.js b/src/fairyDustCursor.js index b3d2e5b..d6717ed 100644 --- a/src/fairyDustCursor.js +++ b/src/fairyDustCursor.js @@ -14,6 +14,7 @@ export function fairyDustCursor(options) { const particles = []; const canvImages = []; let canvas, context; + let lastTime = 0; const char = "*"; @@ -64,7 +65,7 @@ export function fairyDustCursor(options) { }); bindEvents(); - loop(); + requestAnimationFrame(loop); } // Bind events that are needed @@ -133,12 +134,12 @@ export function fairyDustCursor(options) { particles.push(new Particle(x, y, color)); } - function updateParticles() { + function updateParticles(deltaTime) { context.clearRect(0, 0, width, height); // Update for (let i = 0; i < particles.length; i++) { - particles[i].update(context); + particles[i].update(context, deltaTime); } // Remove dead particles @@ -149,8 +150,10 @@ export function fairyDustCursor(options) { } } - function loop() { - updateParticles(); + function loop(time) { + const deltaTime = Math.min(100, time - lastTime) / (1000 / 60); + lastTime = time; + updateParticles(deltaTime); requestAnimationFrame(loop); } @@ -165,12 +168,12 @@ export function fairyDustCursor(options) { this.position = { x: x, y: y }; this.canv = canvasItem; - this.update = function (context) { - this.position.x += this.velocity.x; - this.position.y += this.velocity.y; - this.lifeSpan--; + this.update = function (context, deltaTime) { + this.position.x += this.velocity.x * deltaTime; + this.position.y += this.velocity.y * deltaTime; + this.lifeSpan -= deltaTime; - this.velocity.y += 0.02; + this.velocity.y += 0.02 * deltaTime; const scale = Math.max(this.lifeSpan / this.initialLifeSpan, 0); diff --git a/src/followingDotCursor.js b/src/followingDotCursor.js index 3e566b7..d1ba6d2 100644 --- a/src/followingDotCursor.js +++ b/src/followingDotCursor.js @@ -7,6 +7,7 @@ export function followingDotCursor(options) { let cursor = { x: width / 2, y: width / 2 }; let dot = new Dot(width / 2, height / 2, 10, 10); let canvas, context; + let lastTime = 0; function init() { canvas = document.createElement("canvas"); @@ -28,7 +29,7 @@ export function followingDotCursor(options) { } bindEvents(); - loop(); + requestAnimationFrame(loop); } // Bind events that are needed @@ -61,14 +62,16 @@ export function followingDotCursor(options) { } } - function updateDot() { + function updateDot(deltaTime) { context.clearRect(0, 0, width, height); - dot.moveTowards(cursor.x, cursor.y, context); + dot.moveTowards(cursor.x, cursor.y, context, deltaTime); } - function loop() { - updateDot(); + function loop(time) { + const deltaTime = Math.min(100, time - lastTime) / (1000 / 60); + lastTime = time; + updateDot(deltaTime); requestAnimationFrame(loop); } @@ -77,9 +80,9 @@ export function followingDotCursor(options) { this.width = width; this.lag = lag; - this.moveTowards = function (x, y, context) { - this.position.x += (x - this.position.x) / this.lag; - this.position.y += (y - this.position.y) / this.lag; + this.moveTowards = function (x, y, context, deltaTime) { + this.position.x += (x - this.position.x) / (this.lag / deltaTime); + this.position.y += (y - this.position.y) / (this.lag / deltaTime); context.fillStyle = "rgba(50, 50, 50, 0.65)"; context.beginPath(); diff --git a/src/ghostCursor.js b/src/ghostCursor.js index 894cdf0..d0a5589 100644 --- a/src/ghostCursor.js +++ b/src/ghostCursor.js @@ -7,6 +7,7 @@ export function ghostCursor(options) { let cursor = { x: width / 2, y: width / 2 }; let particles = []; let canvas, context; + let lastTime = 0; let baseImage = new Image(); baseImage.src = @@ -32,7 +33,7 @@ export function ghostCursor(options) { } bindEvents(); - loop(); + requestAnimationFrame(loop); } // Bind events that are needed @@ -81,12 +82,12 @@ export function ghostCursor(options) { particles.push(new Particle(x, y, image)); } - function updateParticles() { + function updateParticles(deltaTime) { context.clearRect(0, 0, width, height); // Update for (let i = 0; i < particles.length; i++) { - particles[i].update(context); + particles[i].update(context, deltaTime); } // Remove dead particles @@ -97,8 +98,10 @@ export function ghostCursor(options) { } } - function loop() { - updateParticles(); + function loop(time) { + const deltaTime = Math.min(100, time - lastTime) / (1000 / 60); + lastTime = time; + updateParticles(deltaTime); requestAnimationFrame(loop); } @@ -114,8 +117,8 @@ export function ghostCursor(options) { this.image = image; - this.update = function (context) { - this.lifeSpan--; + this.update = function (context, deltaTime) { + this.lifeSpan -= deltaTime; const opacity = Math.max(this.lifeSpan / this.initialLifeSpan, 0); context.globalAlpha = opacity; diff --git a/src/rainbowCursor.js b/src/rainbowCursor.js index 6fe4a08..b2912b5 100644 --- a/src/rainbowCursor.js +++ b/src/rainbowCursor.js @@ -7,6 +7,7 @@ export function rainbowCursor(options) { let cursor = { x: width / 2, y: width / 2 }; let particles = []; let canvas, context; + let lastTime = 0; const totalParticles = options?.length || 20; const colors = options?.colors || [ @@ -41,7 +42,7 @@ export function rainbowCursor(options) { } bindEvents(); - loop(); + requestAnimationFrame(loop); } // Bind events that are needed @@ -85,7 +86,7 @@ export function rainbowCursor(options) { particles.push(new Particle(x, y, image)); } - function updateParticles() { + function updateParticles(deltaTime) { context.clearRect(0, 0, width, height); context.lineJoin = "round"; @@ -102,8 +103,8 @@ export function rainbowCursor(options) { particleSets.push({ x: x, y: y }); - x += (nextParticle.position.x - particle.position.x) * 0.4; - y += (nextParticle.position.y - particle.position.y) * 0.4; + x += (nextParticle.position.x - particle.position.x) * 0.4 / deltaTime; + y += (nextParticle.position.y - particle.position.y) * 0.4 / deltaTime; }); colors.forEach((color, index) => { @@ -129,8 +130,10 @@ export function rainbowCursor(options) { }); } - function loop() { - updateParticles(); + function loop(time) { + const deltaTime = Math.min(100, time - lastTime) / (1000 / 60); + lastTime = time; + updateParticles(deltaTime); requestAnimationFrame(loop); } diff --git a/src/snowflakeCursor.js b/src/snowflakeCursor.js index 6824276..2a4448a 100644 --- a/src/snowflakeCursor.js +++ b/src/snowflakeCursor.js @@ -8,6 +8,7 @@ export function snowflakeCursor(options) { let cursor = { x: width / 2, y: width / 2 } let particles = [] let canvas, context + let lastTime = 0 let canvImages = [] @@ -56,7 +57,7 @@ export function snowflakeCursor(options) { }) bindEvents() - loop() + requestAnimationFrame(loop) } // Bind events that are needed @@ -113,12 +114,12 @@ export function snowflakeCursor(options) { particles.push(new Particle(x, y, img)) } - function updateParticles() { + function updateParticles(deltaTime) { context.clearRect(0, 0, width, height) // Update for (let i = 0; i < particles.length; i++) { - particles[i].update(context) + particles[i].update(context, deltaTime) } // Remove dead particles @@ -129,8 +130,10 @@ export function snowflakeCursor(options) { } } - function loop() { - updateParticles() + function loop(time) { + const deltaTime = Math.min(100, time - lastTime) / (1000 / 60); + lastTime = time; + updateParticles(deltaTime) requestAnimationFrame(loop) } @@ -149,13 +152,13 @@ export function snowflakeCursor(options) { this.position = { x: x, y: y } this.canv = canvasItem - this.update = function(context) { - this.position.x += this.velocity.x - this.position.y += this.velocity.y - this.lifeSpan-- + this.update = function(context, deltaTime) { + this.position.x += this.velocity.x * deltaTime + this.position.y += this.velocity.y * deltaTime + this.lifeSpan -= deltaTime; - this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75 - this.velocity.y -= Math.random() / 300 + this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75 * deltaTime + this.velocity.y -= Math.random() / (300 * deltaTime) const scale = Math.max(this.lifeSpan / this.initialLifeSpan, 0) diff --git a/src/springyEmojiCursor.js b/src/springyEmojiCursor.js index bc64914..d7e69b0 100644 --- a/src/springyEmojiCursor.js +++ b/src/springyEmojiCursor.js @@ -23,6 +23,7 @@ export function springyEmojiCursor(options) { let cursor = { x: width / 2, y: width / 2 } let particles = [] let canvas, context + let lastTime = 0; let emojiAsImage @@ -74,7 +75,7 @@ export function springyEmojiCursor(options) { } bindEvents() - loop() + requestAnimationFrame(loop); } // Bind events that are needed @@ -122,7 +123,7 @@ export function springyEmojiCursor(options) { } } - function updateParticles() { + function updateParticles(deltaTime) { canvas.width = canvas.width // follow mouse @@ -151,8 +152,8 @@ export function springyEmojiCursor(options) { (spring.Y + resist.Y) / MASS + GRAVITY ) - particles[i].velocity.x += DELTAT * accel.X - particles[i].velocity.y += DELTAT * accel.Y + particles[i].velocity.x += DELTAT * accel.X * deltaTime + particles[i].velocity.y += DELTAT * accel.Y * deltaTime if ( Math.abs(particles[i].velocity.x) < STOPVEL && @@ -164,8 +165,8 @@ export function springyEmojiCursor(options) { particles[i].velocity.y = 0 } - particles[i].position.x += particles[i].velocity.x - particles[i].position.y += particles[i].velocity.y + particles[i].position.x += particles[i].velocity.x * deltaTime; + particles[i].position.y += particles[i].velocity.y * deltaTime; let height, width height = canvas.clientHeight @@ -196,8 +197,10 @@ export function springyEmojiCursor(options) { } } - function loop() { - updateParticles() + function loop(time) { + const deltaTime = Math.min(100, time - lastTime) / (1000 / 60); + lastTime = time; + updateParticles(deltaTime) requestAnimationFrame(loop) } diff --git a/src/trailingCursor.js b/src/trailingCursor.js index 0f69b51..c9dca2e 100644 --- a/src/trailingCursor.js +++ b/src/trailingCursor.js @@ -10,6 +10,7 @@ export function trailingCursor(options) { let cursor = { x: width / 2, y: width / 2 }; let particles = []; let canvas, context; + let lastTime = 0; const totalParticles = options.particles || 15; let cursorsInitted = false; @@ -38,7 +39,7 @@ export function trailingCursor(options) { } bindEvents(); - loop(); + requestAnimationFrame(loop); } // Bind events that are needed @@ -82,7 +83,7 @@ export function trailingCursor(options) { particles.push(new Particle(x, y, image)); } - function updateParticles() { + function updateParticles(deltaTime) { context.clearRect(0, 0, width, height); let x = cursor.x; @@ -99,8 +100,10 @@ export function trailingCursor(options) { }); } - function loop() { - updateParticles(); + function loop(time) { + const deltaTime = Math.min(100, time - lastTime) / (1000 / 60); + lastTime = time; + updateParticles(deltaTime); requestAnimationFrame(loop); }