Move birb rendering to separate file

This commit is contained in:
Idrees Hassan
2025-10-26 17:56:49 -04:00
parent e1c88120d3
commit fe497a4020
5 changed files with 930 additions and 384 deletions

437
dist/birb.js vendored
View File

@@ -492,6 +492,284 @@
}
}
/**
* @typedef {keyof typeof Animations} AnimationType
*/
const Animations = /** @type {const} */ ({
STILL: "STILL",
BOB: "BOB",
FLYING: "FLYING",
HEART: "HEART"
});
class Birb {
animStart = Date.now();
x = 0;
y = 0;
direction = Directions.RIGHT;
isAbsolutePositioned = false;
visible = true;
/** @type {AnimationType} */
currentAnimation = Animations.STILL;
/**
* @param {number} birbCssScale
* @param {number} canvasPixelSize
* @param {string[][]} spriteSheet The loaded sprite sheet pixel data
* @param {number} spriteWidth
* @param {number} spriteHeight
*/
constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) {
this.birbCssScale = birbCssScale;
this.canvasPixelSize = canvasPixelSize;
this.windowPixelSize = canvasPixelSize * birbCssScale;
this.spriteWidth = spriteWidth;
this.spriteHeight = spriteHeight;
// Build layers from sprite sheet
this.layers = {
base: new Layer(this.getLayer(spriteSheet, 0)),
down: new Layer(this.getLayer(spriteSheet, 1)),
heartOne: new Layer(this.getLayer(spriteSheet, 2)),
heartTwo: new Layer(this.getLayer(spriteSheet, 3)),
heartThree: new Layer(this.getLayer(spriteSheet, 4)),
tuftBase: new Layer(this.getLayer(spriteSheet, 5), "tuft"),
tuftDown: new Layer(this.getLayer(spriteSheet, 6), "tuft"),
wingsUp: new Layer(this.getLayer(spriteSheet, 7)),
wingsDown: new Layer(this.getLayer(spriteSheet, 8)),
happyEye: new Layer(this.getLayer(spriteSheet, 9)),
};
// Build frames from layers
this.frames = {
base: new Frame([this.layers.base, this.layers.tuftBase]),
headDown: new Frame([this.layers.down, this.layers.tuftDown]),
wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]),
wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]),
heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]),
heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]),
heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
};
// Build animations from frames
this.animations = {
[Animations.STILL]: new Anim([this.frames.base], [1000]),
[Animations.BOB]: new Anim([
this.frames.base,
this.frames.headDown
], [
420,
420
]),
[Animations.FLYING]: new Anim([
this.frames.base,
this.frames.wingsUp,
this.frames.headDown,
this.frames.wingsDown,
], [
30,
80,
30,
60,
]),
[Animations.HEART]: new Anim([
this.frames.heartOne,
this.frames.heartTwo,
this.frames.heartThree,
this.frames.heartFour,
this.frames.heartThree,
this.frames.heartFour,
this.frames.heartThree,
this.frames.heartFour,
], [
60,
80,
250,
250,
250,
250,
250,
250,
], false),
};
// Create canvas element
this.canvas = document.createElement("canvas");
this.canvas.id = "birb";
this.canvas.width = this.frames.base.getPixels()[0].length * canvasPixelSize;
this.canvas.height = spriteHeight * canvasPixelSize;
this.ctx = this.canvas.getContext("2d");
// Append to document
document.body.appendChild(this.canvas);
}
/**
* Draw the current animation frame
* @param {BirdType} species The species color data
* @returns {boolean} Whether the animation has completed (for non-looping animations)
*/
draw(species) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const anim = this.animations[this.currentAnimation];
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species);
}
/**
* Get a layer from the sprite sheet array
* @param {string[][]} array
* @param {number} sprite
* @returns {string[][]}
*/
getLayer(array, sprite) {
// From an array of a horizontal sprite sheet, get the layer for a specific sprite
const layer = [];
for (let y = 0; y < this.spriteWidth; y++) {
layer.push(array[y].slice(sprite * this.spriteWidth, (sprite + 1) * this.spriteWidth));
}
return layer;
}
/**
* @returns {AnimationType} The current animation key
*/
getCurrentAnimation() {
return this.currentAnimation;
}
/**
* Set the current animation by name and reset the animation timer
* @param {AnimationType} animationName
*/
setAnimation(animationName) {
this.currentAnimation = animationName;
this.animStart = Date.now();
}
/**
* Get the frames object
* @returns {Record<string, Frame>}
*/
getFrames() {
return this.frames;
}
/**
* Get the canvas element
* @returns {HTMLCanvasElement}
*/
getElement() {
return this.canvas;
}
/**
* Get the canvas width in CSS pixels
* @returns {number}
*/
getElementWidth() {
return this.canvas.width * this.birbCssScale;
}
/**
* Get the canvas height in CSS pixels
* @returns {number}
*/
getElementHeight() {
return this.canvas.height * this.birbCssScale;
}
getElementTop() {
const rect = this.canvas.getBoundingClientRect();
return rect.top;
}
/**
* Set the X position
* @param {number} x
*/
setX(x) {
this.x = x;
let mod = this.getElementWidth() / -2 - (this.windowPixelSize * (this.direction === Directions.RIGHT ? 2 : -2));
this.canvas.style.left = `${x + mod}px`;
}
/**
* Set the Y position
* @param {number} y
*/
setY(y) {
this.y = y;
let bottom;
if (this.isAbsolutePositioned) {
// Position is absolute, convert from fixed
bottom = y - window.scrollY;
} else {
// Position is fixed
bottom = y;
}
this.canvas.style.bottom = `${bottom}px`;
}
/**
* Get the current X position
* @returns {number}
*/
getX() {
return this.x;
}
/**
* Get the current Y position
* @returns {number}
*/
getY() {
return this.y;
}
/**
* Set the direction the bird is facing
* @param {number} direction
*/
setDirection(direction) {
this.direction = direction;
}
/**
* Set whether the element should be absolutely positioned
* @param {boolean} absolute
*/
setAbsolutePositioned(absolute) {
this.isAbsolutePositioned = absolute;
if (absolute) {
this.canvas.classList.add("birb-absolute");
} else {
this.canvas.classList.remove("birb-absolute");
}
// Update Y position to apply the new positioning mode
this.setY(this.y);
}
/**
* Set visibility of the bird
* @param {boolean} visible
*/
setVisible(visible) {
this.visible = visible;
this.canvas.style.display = visible ? "" : "none";
}
/**
* Get visibility of the bird
* @returns {boolean}
*/
isVisible() {
return this.visible;
}
}
/**
* @typedef {Object} SavedStickyNote
* @property {string} id
@@ -1264,79 +1542,14 @@
const SPRITE_SHEET = birbPixels;
const FEATHER_SPRITE_SHEET = featherPixels;
const layers = {
base: new Layer(getLayer(SPRITE_SHEET, 0)),
down: new Layer(getLayer(SPRITE_SHEET, 1)),
heartOne: new Layer(getLayer(SPRITE_SHEET, 2)),
heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)),
heartThree: new Layer(getLayer(SPRITE_SHEET, 4)),
tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"),
tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"),
wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)),
wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)),
happyEye: new Layer(getLayer(SPRITE_SHEET, 9)),
};
const featherLayers = {
feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)),
};
const birbFrames = {
base: new Frame([layers.base, layers.tuftBase]),
headDown: new Frame([layers.down, layers.tuftDown]),
wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]),
wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]),
heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]),
heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]),
heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]),
heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]),
};
const featherFrames = {
feather: new Frame([featherLayers.feather]),
};
const Animations = {
STILL: new Anim([birbFrames.base], [1000]),
BOB: new Anim([
birbFrames.base,
birbFrames.headDown
], [
420,
420
]),
FLYING: new Anim([
birbFrames.base,
birbFrames.wingsUp,
birbFrames.headDown,
birbFrames.wingsDown,
], [
30,
80,
30,
60,
]),
HEART: new Anim([
birbFrames.heartOne,
birbFrames.heartTwo,
birbFrames.heartThree,
birbFrames.heartFour,
birbFrames.heartThree,
birbFrames.heartFour,
birbFrames.heartThree,
birbFrames.heartFour,
], [
60,
80,
250,
250,
250,
250,
250,
250,
], false),
};
const FEATHER_ANIMATIONS = {
feather: new Anim([
featherFrames.feather,
@@ -1377,10 +1590,9 @@
];
const styleElement = document.createElement("style");
const canvas = document.createElement("canvas");
/** @type {CanvasRenderingContext2D} */
const ctx = canvas.getContext("2d");
/** @type {Birb} */
let birb;
const States = {
IDLE: "idle",
@@ -1391,9 +1603,6 @@
let frozen = false;
let stateStart = Date.now();
let currentState = States.IDLE;
let animStart = Date.now();
let currentAnimation = Animations.BOB;
let direction = Directions.RIGHT;
let ticks = 0;
// Bird's current position
let birdY = 0;
@@ -1561,10 +1770,8 @@
styleElement.innerHTML = STYLESHEET;
document.head.appendChild(styleElement);
canvas.id = "birb";
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas);
birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT);
birb.setAnimation(Animations.BOB);
window.addEventListener("scroll", () => {
lastActionTimestamp = Date.now();
@@ -1577,13 +1784,17 @@
}
});
onClick(canvas, () => {
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
const birbElement = birb.getElement();
onClick(birbElement, () => {
if (birb.getCurrentAnimation() === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
// Currently being pet, don't open menu
return;
}
insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation);
}); canvas.addEventListener("mouseover", () => {
});
birbElement.addEventListener("mouseover", () => {
lastActionTimestamp = Date.now();
if (currentState === States.IDLE) {
petStack.push(Date.now());
@@ -1599,7 +1810,7 @@
}
});
canvas.addEventListener("touchmove", (e) => {
birbElement.addEventListener("touchmove", (e) => {
pet();
});
@@ -1628,7 +1839,7 @@
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
if (Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
@@ -1660,7 +1871,7 @@
function draw() {
requestAnimationFrame(draw);
if (!visible) {
if (!birb.isVisible()) {
return;
}
@@ -1688,14 +1899,13 @@
focusOnGround();
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) {
setAnimation(Animations.STILL);
if (birb.draw(SPECIES[currentSpecies])) {
birb.setAnimation(Animations.STILL);
}
// Update HTML element position
setX(birdX);
setY(birdY);
birb.setX(birdX);
birb.setY(birdY);
}
/**
@@ -1843,7 +2053,7 @@
*/
function updateMenuLocation(menu) {
let x = birdX;
let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
let y = birb.getElementTop() + birb.getElementHeight() / 2 + WINDOW_PIXEL_SIZE * 10;
const offset = 20;
if (x < window.innerWidth / 2) {
// Left side
@@ -1917,7 +2127,7 @@
if (!speciesCtx) {
return;
}
birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type);
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type);
speciesElement.appendChild(speciesCanvas);
content.appendChild(speciesElement);
if (unlocked) {
@@ -1997,7 +2207,7 @@
birdX = targetX;
birdY = targetY;
} else {
direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT;
birb.setDirection(targetX > birdX ? Directions.RIGHT : Directions.LEFT);
}
return complete;
}
@@ -2069,17 +2279,13 @@
focusedBounds = { left, right, top };
}
function getCanvasWidth() {
return canvas.width * BIRB_CSS_SCALE
}
function hop() {
if (frozen) {
return;
}
if (currentState === States.IDLE) {
setState(States.HOP);
setAnimation(Animations.FLYING);
birb.setAnimation(Animations.FLYING);
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
targetX = birdX - HOP_DISTANCE;
} else {
@@ -2090,14 +2296,14 @@
}
function pet() {
if (currentState === States.IDLE && currentAnimation !== Animations.HEART) {
setAnimation(Animations.HEART);
if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) {
birb.setAnimation(Animations.HEART);
lastPetTimestamp = Date.now();
}
}
function hideBirb() {
canvas.style.display = "none";
birb.setVisible(false);
visible = false;
}
@@ -2109,7 +2315,7 @@
targetX = x;
targetY = y;
setState(States.FLYING);
setAnimation(Animations.FLYING);
birb.setAnimation(Animations.FLYING);
}
/**
@@ -2119,15 +2325,6 @@
return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP);
}
/**
* Set the current animation and reset the animation timer
* @param {Anim} animation
*/
function setAnimation(animation) {
currentAnimation = animation;
animStart = Date.now();
}
/**
* Set the current state and reset the state timer
* @param {string} state
@@ -2138,37 +2335,17 @@
startY = birdY;
currentState = state;
if (state === States.IDLE) {
setAnimation(Animations.BOB);
}
if (isAbsolute()) {
canvas.classList.add("birb-absolute");
} else {
canvas.classList.remove("birb-absolute");
birb.setAnimation(Animations.BOB);
}
birb.setAbsolutePositioned(isAbsolute());
setY(birdY);
}
/**
* @param {number} x
*/
function setX(x) {
let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2));
canvas.style.left = `${x + mod}px`;
}
/**
* @param {number} y
*/
function setY(y) {
let bottom;
if (isAbsolute()) {
// Position is absolute, convert from fixed
bottom = y - window.scrollY;
} else {
// Position is fixed
bottom = y;
}
canvas.style.bottom = `${bottom}px`;
birb.setY(y);
}
// Helper functions

439
dist/birb.user.js vendored
View File

@@ -1,7 +1,7 @@
// ==UserScript==
// @name Pocket Bird
// @namespace https://idreesinc.com
// @version 2025.10.26.240
// @version 2025.10.26.388
// @description birb
// @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
@@ -506,6 +506,284 @@
}
}
/**
* @typedef {keyof typeof Animations} AnimationType
*/
const Animations = /** @type {const} */ ({
STILL: "STILL",
BOB: "BOB",
FLYING: "FLYING",
HEART: "HEART"
});
class Birb {
animStart = Date.now();
x = 0;
y = 0;
direction = Directions.RIGHT;
isAbsolutePositioned = false;
visible = true;
/** @type {AnimationType} */
currentAnimation = Animations.STILL;
/**
* @param {number} birbCssScale
* @param {number} canvasPixelSize
* @param {string[][]} spriteSheet The loaded sprite sheet pixel data
* @param {number} spriteWidth
* @param {number} spriteHeight
*/
constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) {
this.birbCssScale = birbCssScale;
this.canvasPixelSize = canvasPixelSize;
this.windowPixelSize = canvasPixelSize * birbCssScale;
this.spriteWidth = spriteWidth;
this.spriteHeight = spriteHeight;
// Build layers from sprite sheet
this.layers = {
base: new Layer(this.getLayer(spriteSheet, 0)),
down: new Layer(this.getLayer(spriteSheet, 1)),
heartOne: new Layer(this.getLayer(spriteSheet, 2)),
heartTwo: new Layer(this.getLayer(spriteSheet, 3)),
heartThree: new Layer(this.getLayer(spriteSheet, 4)),
tuftBase: new Layer(this.getLayer(spriteSheet, 5), "tuft"),
tuftDown: new Layer(this.getLayer(spriteSheet, 6), "tuft"),
wingsUp: new Layer(this.getLayer(spriteSheet, 7)),
wingsDown: new Layer(this.getLayer(spriteSheet, 8)),
happyEye: new Layer(this.getLayer(spriteSheet, 9)),
};
// Build frames from layers
this.frames = {
base: new Frame([this.layers.base, this.layers.tuftBase]),
headDown: new Frame([this.layers.down, this.layers.tuftDown]),
wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]),
wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]),
heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]),
heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]),
heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
};
// Build animations from frames
this.animations = {
[Animations.STILL]: new Anim([this.frames.base], [1000]),
[Animations.BOB]: new Anim([
this.frames.base,
this.frames.headDown
], [
420,
420
]),
[Animations.FLYING]: new Anim([
this.frames.base,
this.frames.wingsUp,
this.frames.headDown,
this.frames.wingsDown,
], [
30,
80,
30,
60,
]),
[Animations.HEART]: new Anim([
this.frames.heartOne,
this.frames.heartTwo,
this.frames.heartThree,
this.frames.heartFour,
this.frames.heartThree,
this.frames.heartFour,
this.frames.heartThree,
this.frames.heartFour,
], [
60,
80,
250,
250,
250,
250,
250,
250,
], false),
};
// Create canvas element
this.canvas = document.createElement("canvas");
this.canvas.id = "birb";
this.canvas.width = this.frames.base.getPixels()[0].length * canvasPixelSize;
this.canvas.height = spriteHeight * canvasPixelSize;
this.ctx = this.canvas.getContext("2d");
// Append to document
document.body.appendChild(this.canvas);
}
/**
* Draw the current animation frame
* @param {BirdType} species The species color data
* @returns {boolean} Whether the animation has completed (for non-looping animations)
*/
draw(species) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const anim = this.animations[this.currentAnimation];
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species);
}
/**
* Get a layer from the sprite sheet array
* @param {string[][]} array
* @param {number} sprite
* @returns {string[][]}
*/
getLayer(array, sprite) {
// From an array of a horizontal sprite sheet, get the layer for a specific sprite
const layer = [];
for (let y = 0; y < this.spriteWidth; y++) {
layer.push(array[y].slice(sprite * this.spriteWidth, (sprite + 1) * this.spriteWidth));
}
return layer;
}
/**
* @returns {AnimationType} The current animation key
*/
getCurrentAnimation() {
return this.currentAnimation;
}
/**
* Set the current animation by name and reset the animation timer
* @param {AnimationType} animationName
*/
setAnimation(animationName) {
this.currentAnimation = animationName;
this.animStart = Date.now();
}
/**
* Get the frames object
* @returns {Record<string, Frame>}
*/
getFrames() {
return this.frames;
}
/**
* Get the canvas element
* @returns {HTMLCanvasElement}
*/
getElement() {
return this.canvas;
}
/**
* Get the canvas width in CSS pixels
* @returns {number}
*/
getElementWidth() {
return this.canvas.width * this.birbCssScale;
}
/**
* Get the canvas height in CSS pixels
* @returns {number}
*/
getElementHeight() {
return this.canvas.height * this.birbCssScale;
}
getElementTop() {
const rect = this.canvas.getBoundingClientRect();
return rect.top;
}
/**
* Set the X position
* @param {number} x
*/
setX(x) {
this.x = x;
let mod = this.getElementWidth() / -2 - (this.windowPixelSize * (this.direction === Directions.RIGHT ? 2 : -2));
this.canvas.style.left = `${x + mod}px`;
}
/**
* Set the Y position
* @param {number} y
*/
setY(y) {
this.y = y;
let bottom;
if (this.isAbsolutePositioned) {
// Position is absolute, convert from fixed
bottom = y - window.scrollY;
} else {
// Position is fixed
bottom = y;
}
this.canvas.style.bottom = `${bottom}px`;
}
/**
* Get the current X position
* @returns {number}
*/
getX() {
return this.x;
}
/**
* Get the current Y position
* @returns {number}
*/
getY() {
return this.y;
}
/**
* Set the direction the bird is facing
* @param {number} direction
*/
setDirection(direction) {
this.direction = direction;
}
/**
* Set whether the element should be absolutely positioned
* @param {boolean} absolute
*/
setAbsolutePositioned(absolute) {
this.isAbsolutePositioned = absolute;
if (absolute) {
this.canvas.classList.add("birb-absolute");
} else {
this.canvas.classList.remove("birb-absolute");
}
// Update Y position to apply the new positioning mode
this.setY(this.y);
}
/**
* Set visibility of the bird
* @param {boolean} visible
*/
setVisible(visible) {
this.visible = visible;
this.canvas.style.display = visible ? "" : "none";
}
/**
* Get visibility of the bird
* @returns {boolean}
*/
isVisible() {
return this.visible;
}
}
/**
* @typedef {Object} SavedStickyNote
* @property {string} id
@@ -1278,79 +1556,14 @@
const SPRITE_SHEET = birbPixels;
const FEATHER_SPRITE_SHEET = featherPixels;
const layers = {
base: new Layer(getLayer(SPRITE_SHEET, 0)),
down: new Layer(getLayer(SPRITE_SHEET, 1)),
heartOne: new Layer(getLayer(SPRITE_SHEET, 2)),
heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)),
heartThree: new Layer(getLayer(SPRITE_SHEET, 4)),
tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"),
tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"),
wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)),
wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)),
happyEye: new Layer(getLayer(SPRITE_SHEET, 9)),
};
const featherLayers = {
feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)),
};
const birbFrames = {
base: new Frame([layers.base, layers.tuftBase]),
headDown: new Frame([layers.down, layers.tuftDown]),
wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]),
wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]),
heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]),
heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]),
heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]),
heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]),
};
const featherFrames = {
feather: new Frame([featherLayers.feather]),
};
const Animations = {
STILL: new Anim([birbFrames.base], [1000]),
BOB: new Anim([
birbFrames.base,
birbFrames.headDown
], [
420,
420
]),
FLYING: new Anim([
birbFrames.base,
birbFrames.wingsUp,
birbFrames.headDown,
birbFrames.wingsDown,
], [
30,
80,
30,
60,
]),
HEART: new Anim([
birbFrames.heartOne,
birbFrames.heartTwo,
birbFrames.heartThree,
birbFrames.heartFour,
birbFrames.heartThree,
birbFrames.heartFour,
birbFrames.heartThree,
birbFrames.heartFour,
], [
60,
80,
250,
250,
250,
250,
250,
250,
], false),
};
const FEATHER_ANIMATIONS = {
feather: new Anim([
featherFrames.feather,
@@ -1391,10 +1604,9 @@
];
const styleElement = document.createElement("style");
const canvas = document.createElement("canvas");
/** @type {CanvasRenderingContext2D} */
const ctx = canvas.getContext("2d");
/** @type {Birb} */
let birb;
const States = {
IDLE: "idle",
@@ -1405,9 +1617,6 @@
let frozen = false;
let stateStart = Date.now();
let currentState = States.IDLE;
let animStart = Date.now();
let currentAnimation = Animations.BOB;
let direction = Directions.RIGHT;
let ticks = 0;
// Bird's current position
let birdY = 0;
@@ -1575,10 +1784,8 @@
styleElement.innerHTML = STYLESHEET;
document.head.appendChild(styleElement);
canvas.id = "birb";
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas);
birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT);
birb.setAnimation(Animations.BOB);
window.addEventListener("scroll", () => {
lastActionTimestamp = Date.now();
@@ -1591,13 +1798,17 @@
}
});
onClick(canvas, () => {
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
const birbElement = birb.getElement();
onClick(birbElement, () => {
if (birb.getCurrentAnimation() === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
// Currently being pet, don't open menu
return;
}
insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation);
}); canvas.addEventListener("mouseover", () => {
});
birbElement.addEventListener("mouseover", () => {
lastActionTimestamp = Date.now();
if (currentState === States.IDLE) {
petStack.push(Date.now());
@@ -1613,7 +1824,7 @@
}
});
canvas.addEventListener("touchmove", (e) => {
birbElement.addEventListener("touchmove", (e) => {
pet();
});
@@ -1642,7 +1853,7 @@
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
if (Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
@@ -1674,7 +1885,7 @@
function draw() {
requestAnimationFrame(draw);
if (!visible) {
if (!birb.isVisible()) {
return;
}
@@ -1702,14 +1913,13 @@
focusOnGround();
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) {
setAnimation(Animations.STILL);
if (birb.draw(SPECIES[currentSpecies])) {
birb.setAnimation(Animations.STILL);
}
// Update HTML element position
setX(birdX);
setY(birdY);
birb.setX(birdX);
birb.setY(birdY);
}
/**
@@ -1857,7 +2067,7 @@
*/
function updateMenuLocation(menu) {
let x = birdX;
let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
let y = birb.getElementTop() + birb.getElementHeight() / 2 + WINDOW_PIXEL_SIZE * 10;
const offset = 20;
if (x < window.innerWidth / 2) {
// Left side
@@ -1931,7 +2141,7 @@
if (!speciesCtx) {
return;
}
birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type);
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type);
speciesElement.appendChild(speciesCanvas);
content.appendChild(speciesElement);
if (unlocked) {
@@ -2011,7 +2221,7 @@
birdX = targetX;
birdY = targetY;
} else {
direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT;
birb.setDirection(targetX > birdX ? Directions.RIGHT : Directions.LEFT);
}
return complete;
}
@@ -2083,17 +2293,13 @@
focusedBounds = { left, right, top };
}
function getCanvasWidth() {
return canvas.width * BIRB_CSS_SCALE
}
function hop() {
if (frozen) {
return;
}
if (currentState === States.IDLE) {
setState(States.HOP);
setAnimation(Animations.FLYING);
birb.setAnimation(Animations.FLYING);
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
targetX = birdX - HOP_DISTANCE;
} else {
@@ -2104,14 +2310,14 @@
}
function pet() {
if (currentState === States.IDLE && currentAnimation !== Animations.HEART) {
setAnimation(Animations.HEART);
if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) {
birb.setAnimation(Animations.HEART);
lastPetTimestamp = Date.now();
}
}
function hideBirb() {
canvas.style.display = "none";
birb.setVisible(false);
visible = false;
}
@@ -2123,7 +2329,7 @@
targetX = x;
targetY = y;
setState(States.FLYING);
setAnimation(Animations.FLYING);
birb.setAnimation(Animations.FLYING);
}
/**
@@ -2133,15 +2339,6 @@
return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP);
}
/**
* Set the current animation and reset the animation timer
* @param {Anim} animation
*/
function setAnimation(animation) {
currentAnimation = animation;
animStart = Date.now();
}
/**
* Set the current state and reset the state timer
* @param {string} state
@@ -2152,37 +2349,17 @@
startY = birdY;
currentState = state;
if (state === States.IDLE) {
setAnimation(Animations.BOB);
}
if (isAbsolute()) {
canvas.classList.add("birb-absolute");
} else {
canvas.classList.remove("birb-absolute");
birb.setAnimation(Animations.BOB);
}
birb.setAbsolutePositioned(isAbsolute());
setY(birdY);
}
/**
* @param {number} x
*/
function setX(x) {
let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2));
canvas.style.left = `${x + mod}px`;
}
/**
* @param {number} y
*/
function setY(y) {
let bottom;
if (isAbsolute()) {
// Position is absolute, convert from fixed
bottom = y - window.scrollY;
} else {
// Position is fixed
bottom = y;
}
canvas.style.bottom = `${bottom}px`;
birb.setY(y);
}
// Helper functions

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Pocket Bird",
"description": "It's a bird, in your browser. What more could you want?",
"version": "2025.10.26.240",
"version": "2025.10.26.388",
"homepage_url": "https://idreesinc.com",
"content_scripts": [
{

View File

@@ -1,6 +1,8 @@
import Frame from './frame.js';
import Layer from './layer.js';
import Anim from './anim.js';
import { Birb, Animations } from './birb.js';
import {
Directions,
isDebug,
@@ -169,79 +171,14 @@ Promise.all([
const SPRITE_SHEET = birbPixels;
const FEATHER_SPRITE_SHEET = featherPixels;
const layers = {
base: new Layer(getLayer(SPRITE_SHEET, 0)),
down: new Layer(getLayer(SPRITE_SHEET, 1)),
heartOne: new Layer(getLayer(SPRITE_SHEET, 2)),
heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)),
heartThree: new Layer(getLayer(SPRITE_SHEET, 4)),
tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"),
tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"),
wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)),
wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)),
happyEye: new Layer(getLayer(SPRITE_SHEET, 9)),
};
const featherLayers = {
feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)),
};
const birbFrames = {
base: new Frame([layers.base, layers.tuftBase]),
headDown: new Frame([layers.down, layers.tuftDown]),
wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]),
wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]),
heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]),
heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]),
heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]),
heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]),
};
const featherFrames = {
feather: new Frame([featherLayers.feather]),
};
const Animations = {
STILL: new Anim([birbFrames.base], [1000]),
BOB: new Anim([
birbFrames.base,
birbFrames.headDown
], [
420,
420
]),
FLYING: new Anim([
birbFrames.base,
birbFrames.wingsUp,
birbFrames.headDown,
birbFrames.wingsDown,
], [
30,
80,
30,
60,
]),
HEART: new Anim([
birbFrames.heartOne,
birbFrames.heartTwo,
birbFrames.heartThree,
birbFrames.heartFour,
birbFrames.heartThree,
birbFrames.heartFour,
birbFrames.heartThree,
birbFrames.heartFour,
], [
60,
80,
250,
250,
250,
250,
250,
250,
], false),
};
const FEATHER_ANIMATIONS = {
feather: new Anim([
featherFrames.feather,
@@ -282,10 +219,9 @@ Promise.all([
];
const styleElement = document.createElement("style");
const canvas = document.createElement("canvas");
/** @type {CanvasRenderingContext2D} */
const ctx = canvas.getContext("2d");
/** @type {Birb} */
let birb;
const States = {
IDLE: "idle",
@@ -296,9 +232,6 @@ Promise.all([
let frozen = false;
let stateStart = Date.now();
let currentState = States.IDLE;
let animStart = Date.now();
let currentAnimation = Animations.BOB;
let direction = Directions.RIGHT;
let ticks = 0;
// Bird's current position
let birdY = 0;
@@ -466,10 +399,8 @@ Promise.all([
styleElement.innerHTML = STYLESHEET;
document.head.appendChild(styleElement);
canvas.id = "birb";
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas);
birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT);
birb.setAnimation(Animations.BOB);
window.addEventListener("scroll", () => {
lastActionTimestamp = Date.now();
@@ -482,13 +413,17 @@ Promise.all([
}
});
onClick(canvas, () => {
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
const birbElement = birb.getElement();
onClick(birbElement, () => {
if (birb.getCurrentAnimation() === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
// Currently being pet, don't open menu
return;
}
insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation);
}); canvas.addEventListener("mouseover", () => {
});
birbElement.addEventListener("mouseover", () => {
lastActionTimestamp = Date.now();
if (currentState === States.IDLE) {
petStack.push(Date.now());
@@ -504,7 +439,7 @@ Promise.all([
}
});
canvas.addEventListener("touchmove", (e) => {
birbElement.addEventListener("touchmove", (e) => {
pet();
});
@@ -533,7 +468,7 @@ Promise.all([
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
if (Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
@@ -565,7 +500,7 @@ Promise.all([
function draw() {
requestAnimationFrame(draw);
if (!visible) {
if (!birb.isVisible()) {
return;
}
@@ -593,14 +528,13 @@ Promise.all([
focusOnGround();
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) {
setAnimation(Animations.STILL);
if (birb.draw(SPECIES[currentSpecies])) {
birb.setAnimation(Animations.STILL);
}
// Update HTML element position
setX(birdX);
setY(birdY);
birb.setX(birdX);
birb.setY(birdY);
}
/**
@@ -751,7 +685,7 @@ Promise.all([
*/
function updateMenuLocation(menu) {
let x = birdX;
let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
let y = birb.getElementTop() + birb.getElementHeight() / 2 + WINDOW_PIXEL_SIZE * 10;
const offset = 20;
if (x < window.innerWidth / 2) {
// Left side
@@ -826,7 +760,7 @@ Promise.all([
if (!speciesCtx) {
return;
}
birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type);
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type);
speciesElement.appendChild(speciesCanvas);
content.appendChild(speciesElement);
if (unlocked) {
@@ -906,7 +840,7 @@ Promise.all([
birdX = targetX;
birdY = targetY;
} else {
direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT;
birb.setDirection(targetX > birdX ? Directions.RIGHT : Directions.LEFT);
}
return complete;
}
@@ -979,7 +913,7 @@ Promise.all([
}
function getCanvasWidth() {
return canvas.width * BIRB_CSS_SCALE
return birb.getElementWidth();
}
function hop() {
@@ -988,7 +922,7 @@ Promise.all([
}
if (currentState === States.IDLE) {
setState(States.HOP);
setAnimation(Animations.FLYING);
birb.setAnimation(Animations.FLYING);
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
targetX = birdX - HOP_DISTANCE;
} else {
@@ -999,14 +933,14 @@ Promise.all([
}
function pet() {
if (currentState === States.IDLE && currentAnimation !== Animations.HEART) {
setAnimation(Animations.HEART);
if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) {
birb.setAnimation(Animations.HEART);
lastPetTimestamp = Date.now();
}
}
function hideBirb() {
canvas.style.display = "none";
birb.setVisible(false);
visible = false;
}
@@ -1018,7 +952,7 @@ Promise.all([
targetX = x;
targetY = y;
setState(States.FLYING);
setAnimation(Animations.FLYING);
birb.setAnimation(Animations.FLYING);
}
/**
@@ -1028,15 +962,6 @@ Promise.all([
return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP);
}
/**
* Set the current animation and reset the animation timer
* @param {Anim} animation
*/
function setAnimation(animation) {
currentAnimation = animation;
animStart = Date.now();
}
/**
* Set the current state and reset the state timer
* @param {string} state
@@ -1047,13 +972,9 @@ Promise.all([
startY = birdY;
currentState = state;
if (state === States.IDLE) {
setAnimation(Animations.BOB);
}
if (isAbsolute()) {
canvas.classList.add("birb-absolute");
} else {
canvas.classList.remove("birb-absolute");
birb.setAnimation(Animations.BOB);
}
birb.setAbsolutePositioned(isAbsolute());
setY(birdY);
}
@@ -1061,23 +982,14 @@ Promise.all([
* @param {number} x
*/
function setX(x) {
let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2));
canvas.style.left = `${x + mod}px`;
birb.setX(x);
}
/**
* @param {number} y
*/
function setY(y) {
let bottom;
if (isAbsolute()) {
// Position is absolute, convert from fixed
bottom = y - window.scrollY;
} else {
// Position is fixed
bottom = y;
}
canvas.style.bottom = `${bottom}px`;
birb.setY(y);
}
// Helper functions

View File

@@ -1,3 +1,283 @@
export class Birb {
import { Directions } from './shared.js';
import Layer from './layer.js';
import Frame from './frame.js';
import Anim from './anim.js';
import { BirdType } from './sprites.js';
/**
* @typedef {keyof typeof Animations} AnimationType
*/
export const Animations = /** @type {const} */ ({
STILL: "STILL",
BOB: "BOB",
FLYING: "FLYING",
HEART: "HEART"
});
export class Birb {
animStart = Date.now();
x = 0;
y = 0;
direction = Directions.RIGHT;
isAbsolutePositioned = false;
visible = true;
/** @type {AnimationType} */
currentAnimation = Animations.STILL;
/**
* @param {number} birbCssScale
* @param {number} canvasPixelSize
* @param {string[][]} spriteSheet The loaded sprite sheet pixel data
* @param {number} spriteWidth
* @param {number} spriteHeight
*/
constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) {
this.birbCssScale = birbCssScale;
this.canvasPixelSize = canvasPixelSize;
this.windowPixelSize = canvasPixelSize * birbCssScale;
this.spriteWidth = spriteWidth;
this.spriteHeight = spriteHeight;
// Build layers from sprite sheet
this.layers = {
base: new Layer(this.getLayer(spriteSheet, 0)),
down: new Layer(this.getLayer(spriteSheet, 1)),
heartOne: new Layer(this.getLayer(spriteSheet, 2)),
heartTwo: new Layer(this.getLayer(spriteSheet, 3)),
heartThree: new Layer(this.getLayer(spriteSheet, 4)),
tuftBase: new Layer(this.getLayer(spriteSheet, 5), "tuft"),
tuftDown: new Layer(this.getLayer(spriteSheet, 6), "tuft"),
wingsUp: new Layer(this.getLayer(spriteSheet, 7)),
wingsDown: new Layer(this.getLayer(spriteSheet, 8)),
happyEye: new Layer(this.getLayer(spriteSheet, 9)),
};
// Build frames from layers
this.frames = {
base: new Frame([this.layers.base, this.layers.tuftBase]),
headDown: new Frame([this.layers.down, this.layers.tuftDown]),
wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]),
wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]),
heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]),
heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]),
heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
};
// Build animations from frames
this.animations = {
[Animations.STILL]: new Anim([this.frames.base], [1000]),
[Animations.BOB]: new Anim([
this.frames.base,
this.frames.headDown
], [
420,
420
]),
[Animations.FLYING]: new Anim([
this.frames.base,
this.frames.wingsUp,
this.frames.headDown,
this.frames.wingsDown,
], [
30,
80,
30,
60,
]),
[Animations.HEART]: new Anim([
this.frames.heartOne,
this.frames.heartTwo,
this.frames.heartThree,
this.frames.heartFour,
this.frames.heartThree,
this.frames.heartFour,
this.frames.heartThree,
this.frames.heartFour,
], [
60,
80,
250,
250,
250,
250,
250,
250,
], false),
};
// Create canvas element
this.canvas = document.createElement("canvas");
this.canvas.id = "birb";
this.canvas.width = this.frames.base.getPixels()[0].length * canvasPixelSize;
this.canvas.height = spriteHeight * canvasPixelSize;
this.ctx = this.canvas.getContext("2d");
// Append to document
document.body.appendChild(this.canvas);
}
/**
* Draw the current animation frame
* @param {BirdType} species The species color data
* @returns {boolean} Whether the animation has completed (for non-looping animations)
*/
draw(species) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const anim = this.animations[this.currentAnimation];
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species);
}
/**
* Get a layer from the sprite sheet array
* @param {string[][]} array
* @param {number} sprite
* @returns {string[][]}
*/
getLayer(array, sprite) {
// From an array of a horizontal sprite sheet, get the layer for a specific sprite
const layer = [];
for (let y = 0; y < this.spriteWidth; y++) {
layer.push(array[y].slice(sprite * this.spriteWidth, (sprite + 1) * this.spriteWidth));
}
return layer;
}
/**
* @returns {AnimationType} The current animation key
*/
getCurrentAnimation() {
return this.currentAnimation;
}
/**
* Set the current animation by name and reset the animation timer
* @param {AnimationType} animationName
*/
setAnimation(animationName) {
this.currentAnimation = animationName;
this.animStart = Date.now();
}
/**
* Get the frames object
* @returns {Record<string, Frame>}
*/
getFrames() {
return this.frames;
}
/**
* Get the canvas element
* @returns {HTMLCanvasElement}
*/
getElement() {
return this.canvas;
}
/**
* Get the canvas width in CSS pixels
* @returns {number}
*/
getElementWidth() {
return this.canvas.width * this.birbCssScale;
}
/**
* Get the canvas height in CSS pixels
* @returns {number}
*/
getElementHeight() {
return this.canvas.height * this.birbCssScale;
}
getElementTop() {
const rect = this.canvas.getBoundingClientRect();
return rect.top;
}
/**
* Set the X position
* @param {number} x
*/
setX(x) {
this.x = x;
let mod = this.getElementWidth() / -2 - (this.windowPixelSize * (this.direction === Directions.RIGHT ? 2 : -2));
this.canvas.style.left = `${x + mod}px`;
}
/**
* Set the Y position
* @param {number} y
*/
setY(y) {
this.y = y;
let bottom;
if (this.isAbsolutePositioned) {
// Position is absolute, convert from fixed
bottom = y - window.scrollY;
} else {
// Position is fixed
bottom = y;
}
this.canvas.style.bottom = `${bottom}px`;
}
/**
* Get the current X position
* @returns {number}
*/
getX() {
return this.x;
}
/**
* Get the current Y position
* @returns {number}
*/
getY() {
return this.y;
}
/**
* Set the direction the bird is facing
* @param {number} direction
*/
setDirection(direction) {
this.direction = direction;
}
/**
* Set whether the element should be absolutely positioned
* @param {boolean} absolute
*/
setAbsolutePositioned(absolute) {
this.isAbsolutePositioned = absolute;
if (absolute) {
this.canvas.classList.add("birb-absolute");
} else {
this.canvas.classList.remove("birb-absolute");
}
// Update Y position to apply the new positioning mode
this.setY(this.y);
}
/**
* Set visibility of the bird
* @param {boolean} visible
*/
setVisible(visible) {
this.visible = visible;
this.canvas.style.display = visible ? "" : "none";
}
/**
* Get visibility of the bird
* @returns {boolean}
*/
isVisible() {
return this.visible;
}
}