Only draw frames when necessary

This commit is contained in:
Idrees Hassan
2025-10-28 20:42:08 -04:00
parent 72641cc818
commit a456b2269c
7 changed files with 178 additions and 37 deletions

70
dist/birb.js vendored
View File

@@ -454,6 +454,9 @@
* @param {number} canvasPixelSize * @param {number} canvasPixelSize
*/ */
draw(ctx, direction, canvasPixelSize, species) { draw(ctx, direction, canvasPixelSize, species) {
// Clear the canvas before drawing the new frame
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const pixels = this.getPixels(species?.tags[0]); const pixels = this.getPixels(species?.tags[0]);
for (let y = 0; y < pixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
const row = pixels[y]; const row = pixels[y];
@@ -474,12 +477,49 @@
this.frames = frames; this.frames = frames;
this.durations = durations; this.durations = durations;
this.loop = loop; this.loop = loop;
this.lastFrameIndex = -1;
this.lastDirection = null;
this.lastTimeStart = null;
} }
getAnimationDuration() { getAnimationDuration() {
return this.durations.reduce((a, b) => a + b, 0); return this.durations.reduce((a, b) => a + b, 0);
} }
/**
* Get the current frame index based on elapsed time
* @param {number} time The elapsed time since animation start
* @returns {number} The index of the current frame
*/
getCurrentFrameIndex(time) {
let totalDuration = 0;
for (let i = 0; i < this.durations.length; i++) {
totalDuration += this.durations[i];
if (time < totalDuration) {
return i;
}
}
return this.frames.length - 1;
}
/**
* Clear the cached frame state
*/
#clearCache() {
this.lastFrameIndex = -1;
this.lastDirection = null;
}
/**
* Check if the frame needs to be redrawn
* @param {number} frameIndex The current frame index
* @param {number} direction The current direction
* @returns {boolean} Whether the frame needs to be redrawn
*/
#shouldRedraw(frameIndex, direction) {
return frameIndex !== this.lastFrameIndex || direction !== this.lastDirection;
}
/** /**
* @param {CanvasRenderingContext2D} ctx * @param {CanvasRenderingContext2D} ctx
* @param {number} direction * @param {number} direction
@@ -489,22 +529,29 @@
* @returns {boolean} Whether the animation is complete * @returns {boolean} Whether the animation is complete
*/ */
draw(ctx, direction, timeStart, canvasPixelSize, species) { draw(ctx, direction, timeStart, canvasPixelSize, species) {
// Reset cache if animation was restarted
if (this.lastTimeStart !== timeStart) {
this.#clearCache();
this.lastTimeStart = timeStart;
}
let time = Date.now() - timeStart; let time = Date.now() - timeStart;
const duration = this.getAnimationDuration(); const duration = this.getAnimationDuration();
if (this.loop) { if (this.loop) {
time %= duration; time %= duration;
} }
let totalDuration = 0;
for (let i = 0; i < this.durations.length; i++) { const currentFrameIndex = this.getCurrentFrameIndex(time);
totalDuration += this.durations[i];
if (time < totalDuration) { if (this.#shouldRedraw(currentFrameIndex, direction)) {
this.frames[i].draw(ctx, direction, canvasPixelSize, species); this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, species);
return false; this.lastFrameIndex = currentFrameIndex;
this.lastDirection = direction;
} }
}
// Draw the last frame if the animation is complete // Return whether animation is complete (for non-looping animations)
this.frames[this.frames.length - 1].draw(ctx, direction, canvasPixelSize, species); return !this.loop && time >= duration;
return true;
} }
} }
@@ -629,7 +676,6 @@
* @returns {boolean} Whether the animation has completed (for non-looping animations) * @returns {boolean} Whether the animation has completed (for non-looping animations)
*/ */
draw(species) { draw(species) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const anim = this.animations[this.currentAnimation]; const anim = this.animations[this.currentAnimation];
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species);
} }
@@ -1606,7 +1652,7 @@
insertModal(`${birdBirb()} Mode`, message); insertModal(`${birdBirb()} Mode`, message);
}), }),
new Separator(), new Separator(),
new MenuItem("2025.10.28.47", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.10.28.47"); }, false), new MenuItem("2025.10.28.80", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.10.28.80"); }, false),
]; ];
const styleElement = document.createElement("style"); const styleElement = document.createElement("style");

72
dist/birb.user.js vendored
View File

@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Pocket Bird // @name Pocket Bird
// @namespace https://idreesinc.com // @namespace https://idreesinc.com
// @version 2025.10.28.47 // @version 2025.10.28.80
// @description birb // @description birb
// @author Idrees // @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
@@ -468,6 +468,9 @@
* @param {number} canvasPixelSize * @param {number} canvasPixelSize
*/ */
draw(ctx, direction, canvasPixelSize, species) { draw(ctx, direction, canvasPixelSize, species) {
// Clear the canvas before drawing the new frame
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const pixels = this.getPixels(species?.tags[0]); const pixels = this.getPixels(species?.tags[0]);
for (let y = 0; y < pixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
const row = pixels[y]; const row = pixels[y];
@@ -488,12 +491,49 @@
this.frames = frames; this.frames = frames;
this.durations = durations; this.durations = durations;
this.loop = loop; this.loop = loop;
this.lastFrameIndex = -1;
this.lastDirection = null;
this.lastTimeStart = null;
} }
getAnimationDuration() { getAnimationDuration() {
return this.durations.reduce((a, b) => a + b, 0); return this.durations.reduce((a, b) => a + b, 0);
} }
/**
* Get the current frame index based on elapsed time
* @param {number} time The elapsed time since animation start
* @returns {number} The index of the current frame
*/
getCurrentFrameIndex(time) {
let totalDuration = 0;
for (let i = 0; i < this.durations.length; i++) {
totalDuration += this.durations[i];
if (time < totalDuration) {
return i;
}
}
return this.frames.length - 1;
}
/**
* Clear the cached frame state
*/
#clearCache() {
this.lastFrameIndex = -1;
this.lastDirection = null;
}
/**
* Check if the frame needs to be redrawn
* @param {number} frameIndex The current frame index
* @param {number} direction The current direction
* @returns {boolean} Whether the frame needs to be redrawn
*/
#shouldRedraw(frameIndex, direction) {
return frameIndex !== this.lastFrameIndex || direction !== this.lastDirection;
}
/** /**
* @param {CanvasRenderingContext2D} ctx * @param {CanvasRenderingContext2D} ctx
* @param {number} direction * @param {number} direction
@@ -503,22 +543,29 @@
* @returns {boolean} Whether the animation is complete * @returns {boolean} Whether the animation is complete
*/ */
draw(ctx, direction, timeStart, canvasPixelSize, species) { draw(ctx, direction, timeStart, canvasPixelSize, species) {
// Reset cache if animation was restarted
if (this.lastTimeStart !== timeStart) {
this.#clearCache();
this.lastTimeStart = timeStart;
}
let time = Date.now() - timeStart; let time = Date.now() - timeStart;
const duration = this.getAnimationDuration(); const duration = this.getAnimationDuration();
if (this.loop) { if (this.loop) {
time %= duration; time %= duration;
} }
let totalDuration = 0;
for (let i = 0; i < this.durations.length; i++) { const currentFrameIndex = this.getCurrentFrameIndex(time);
totalDuration += this.durations[i];
if (time < totalDuration) { if (this.#shouldRedraw(currentFrameIndex, direction)) {
this.frames[i].draw(ctx, direction, canvasPixelSize, species); this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, species);
return false; this.lastFrameIndex = currentFrameIndex;
this.lastDirection = direction;
} }
}
// Draw the last frame if the animation is complete // Return whether animation is complete (for non-looping animations)
this.frames[this.frames.length - 1].draw(ctx, direction, canvasPixelSize, species); return !this.loop && time >= duration;
return true;
} }
} }
@@ -643,7 +690,6 @@
* @returns {boolean} Whether the animation has completed (for non-looping animations) * @returns {boolean} Whether the animation has completed (for non-looping animations)
*/ */
draw(species) { draw(species) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const anim = this.animations[this.currentAnimation]; const anim = this.animations[this.currentAnimation];
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species);
} }
@@ -1620,7 +1666,7 @@
insertModal(`${birdBirb()} Mode`, message); insertModal(`${birdBirb()} Mode`, message);
}), }),
new Separator(), new Separator(),
new MenuItem("2025.10.28.47", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.10.28.47"); }, false), new MenuItem("2025.10.28.80", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.10.28.80"); }, false),
]; ];
const styleElement = document.createElement("style"); const styleElement = document.createElement("style");

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Pocket Bird", "name": "Pocket Bird",
"description": "It's a bird, in your browser. What more could you want?", "description": "It's a bird, in your browser. What more could you want?",
"version": "2025.10.28.47", "version": "2025.10.28.80",
"homepage_url": "https://idreesinc.com", "homepage_url": "https://idreesinc.com",
"icons": { "icons": {
"48": "images/icons/transparent/48x48x1.png", "48": "images/icons/transparent/48x48x1.png",

View File

@@ -58,6 +58,9 @@ class Frame {
* @param {number} canvasPixelSize * @param {number} canvasPixelSize
*/ */
draw(ctx, direction, canvasPixelSize, species) { draw(ctx, direction, canvasPixelSize, species) {
// Clear the canvas before drawing the new frame
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const pixels = this.getPixels(species?.tags[0]); const pixels = this.getPixels(species?.tags[0]);
for (let y = 0; y < pixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
const row = pixels[y]; const row = pixels[y];

View File

@@ -11,12 +11,49 @@ class Anim {
this.frames = frames; this.frames = frames;
this.durations = durations; this.durations = durations;
this.loop = loop; this.loop = loop;
this.lastFrameIndex = -1;
this.lastDirection = null;
this.lastTimeStart = null;
} }
getAnimationDuration() { getAnimationDuration() {
return this.durations.reduce((a, b) => a + b, 0); return this.durations.reduce((a, b) => a + b, 0);
} }
/**
* Get the current frame index based on elapsed time
* @param {number} time The elapsed time since animation start
* @returns {number} The index of the current frame
*/
getCurrentFrameIndex(time) {
let totalDuration = 0;
for (let i = 0; i < this.durations.length; i++) {
totalDuration += this.durations[i];
if (time < totalDuration) {
return i;
}
}
return this.frames.length - 1;
}
/**
* Clear the cached frame state
*/
#clearCache() {
this.lastFrameIndex = -1;
this.lastDirection = null;
}
/**
* Check if the frame needs to be redrawn
* @param {number} frameIndex The current frame index
* @param {number} direction The current direction
* @returns {boolean} Whether the frame needs to be redrawn
*/
#shouldRedraw(frameIndex, direction) {
return frameIndex !== this.lastFrameIndex || direction !== this.lastDirection;
}
/** /**
* @param {CanvasRenderingContext2D} ctx * @param {CanvasRenderingContext2D} ctx
* @param {number} direction * @param {number} direction
@@ -26,22 +63,29 @@ class Anim {
* @returns {boolean} Whether the animation is complete * @returns {boolean} Whether the animation is complete
*/ */
draw(ctx, direction, timeStart, canvasPixelSize, species) { draw(ctx, direction, timeStart, canvasPixelSize, species) {
// Reset cache if animation was restarted
if (this.lastTimeStart !== timeStart) {
this.#clearCache();
this.lastTimeStart = timeStart;
}
let time = Date.now() - timeStart; let time = Date.now() - timeStart;
const duration = this.getAnimationDuration(); const duration = this.getAnimationDuration();
if (this.loop) { if (this.loop) {
time %= duration; time %= duration;
} }
let totalDuration = 0;
for (let i = 0; i < this.durations.length; i++) { const currentFrameIndex = this.getCurrentFrameIndex(time);
totalDuration += this.durations[i];
if (time < totalDuration) { if (this.#shouldRedraw(currentFrameIndex, direction)) {
this.frames[i].draw(ctx, direction, canvasPixelSize, species); this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, species);
return false; this.lastFrameIndex = currentFrameIndex;
this.lastDirection = direction;
} }
}
// Draw the last frame if the animation is complete // Return whether animation is complete (for non-looping animations)
this.frames[this.frames.length - 1].draw(ctx, direction, canvasPixelSize, species); return !this.loop && time >= duration;
return true;
} }
} }

View File

@@ -125,7 +125,6 @@ export class Birb {
* @returns {boolean} Whether the animation has completed (for non-looping animations) * @returns {boolean} Whether the animation has completed (for non-looping animations)
*/ */
draw(species) { draw(species) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const anim = this.animations[this.currentAnimation]; const anim = this.animations[this.currentAnimation];
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species);
} }

View File

@@ -58,6 +58,9 @@ class Frame {
* @param {number} canvasPixelSize * @param {number} canvasPixelSize
*/ */
draw(ctx, direction, canvasPixelSize, species) { draw(ctx, direction, canvasPixelSize, species) {
// Clear the canvas before drawing the new frame
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const pixels = this.getPixels(species?.tags[0]); const pixels = this.getPixels(species?.tags[0]);
for (let y = 0; y < pixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
const row = pixels[y]; const row = pixels[y];