mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-25 12:17:22 +00:00
Move sprite related files to separate folder
This commit is contained in:
92
src/animation/anim.js
Normal file
92
src/animation/anim.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import Frame from "./frame.js";
|
||||
import { BirdType } from "./sprites";
|
||||
|
||||
class Anim {
|
||||
/**
|
||||
* @param {Frame[]} frames
|
||||
* @param {number[]} durations
|
||||
* @param {boolean} loop
|
||||
*/
|
||||
constructor(frames, durations, loop = true) {
|
||||
this.frames = frames;
|
||||
this.durations = durations;
|
||||
this.loop = loop;
|
||||
this.lastFrameIndex = -1;
|
||||
this.lastDirection = null;
|
||||
this.lastTimeStart = null;
|
||||
}
|
||||
|
||||
getAnimationDuration() {
|
||||
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 {number} direction
|
||||
* @param {number} timeStart The start time of the animation in milliseconds
|
||||
* @param {number} canvasPixelSize The size of a canvas pixel in pixels
|
||||
* @param {BirdType} [species] The species to use for the animation
|
||||
* @returns {boolean} Whether the animation is complete
|
||||
*/
|
||||
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;
|
||||
const duration = this.getAnimationDuration();
|
||||
|
||||
if (this.loop) {
|
||||
time %= duration;
|
||||
}
|
||||
|
||||
const currentFrameIndex = this.getCurrentFrameIndex(time);
|
||||
|
||||
if (this.#shouldRedraw(currentFrameIndex, direction)) {
|
||||
this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, species);
|
||||
this.lastFrameIndex = currentFrameIndex;
|
||||
this.lastDirection = direction;
|
||||
}
|
||||
|
||||
// Return whether animation is complete (for non-looping animations)
|
||||
return !this.loop && time >= duration;
|
||||
}
|
||||
}
|
||||
|
||||
export default Anim;
|
||||
76
src/animation/frame.js
Normal file
76
src/animation/frame.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Directions } from '../shared.js';
|
||||
import { Sprite, BirdType } from './sprites.js';
|
||||
import Layer from './layer.js';
|
||||
|
||||
class Frame {
|
||||
|
||||
/** @type {{ [tag: string]: string[][] }} */
|
||||
#pixelsByTag = {};
|
||||
|
||||
/**
|
||||
* @param {Layer[]} layers
|
||||
*/
|
||||
constructor(layers) {
|
||||
/** @type {Set<string>} */
|
||||
let tags = new Set();
|
||||
for (let layer of layers) {
|
||||
tags.add(layer.tag);
|
||||
}
|
||||
tags.add("default");
|
||||
for (let tag of tags) {
|
||||
let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0);
|
||||
if (layers[0].tag !== "default") {
|
||||
throw new Error("First layer must have the 'default' tag");
|
||||
}
|
||||
this.pixels = layers[0].pixels.map(row => row.slice());
|
||||
// Pad from top with transparent pixels
|
||||
while (this.pixels.length < maxHeight) {
|
||||
this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT));
|
||||
}
|
||||
// Combine layers
|
||||
for (let i = 1; i < layers.length; i++) {
|
||||
if (layers[i].tag === "default" || layers[i].tag === tag) {
|
||||
let layerPixels = layers[i].pixels;
|
||||
let topMargin = maxHeight - layerPixels.length;
|
||||
for (let y = 0; y < layerPixels.length; y++) {
|
||||
for (let x = 0; x < layerPixels[y].length; x++) {
|
||||
this.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.#pixelsByTag[tag] = this.pixels.map(row => row.slice());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [tag]
|
||||
* @returns {string[][]}
|
||||
*/
|
||||
getPixels(tag = "default") {
|
||||
return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {BirdType} [species]
|
||||
* @param {number} direction
|
||||
* @param {number} canvasPixelSize
|
||||
*/
|
||||
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]);
|
||||
for (let y = 0; y < pixels.length; y++) {
|
||||
const row = pixels[y];
|
||||
for (let x = 0; x < pixels[y].length; x++) {
|
||||
const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1];
|
||||
ctx.fillStyle = species?.colors[cell] ?? cell;
|
||||
ctx.fillRect(x * canvasPixelSize, y * canvasPixelSize, canvasPixelSize, canvasPixelSize);
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Frame;
|
||||
12
src/animation/layer.js
Normal file
12
src/animation/layer.js
Normal file
@@ -0,0 +1,12 @@
|
||||
class Layer {
|
||||
/**
|
||||
* @param {string[][]} pixels
|
||||
* @param {string} [tag]
|
||||
*/
|
||||
constructor(pixels, tag = "default") {
|
||||
this.pixels = pixels;
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
export default Layer;
|
||||
199
src/animation/sprites.js
Normal file
199
src/animation/sprites.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/** Indicators for parts of the base bird sprite sheet */
|
||||
export const Sprite = {
|
||||
THEME_HIGHLIGHT: "theme-highlight",
|
||||
TRANSPARENT: "transparent",
|
||||
OUTLINE: "outline",
|
||||
BORDER: "border",
|
||||
FOOT: "foot",
|
||||
BEAK: "beak",
|
||||
EYE: "eye",
|
||||
FACE: "face",
|
||||
HOOD: "hood",
|
||||
NOSE: "nose",
|
||||
BELLY: "belly",
|
||||
UNDERBELLY: "underbelly",
|
||||
WING: "wing",
|
||||
WING_EDGE: "wing-edge",
|
||||
HEART: "heart",
|
||||
HEART_BORDER: "heart-border",
|
||||
HEART_SHINE: "heart-shine",
|
||||
FEATHER_SPINE: "feather-spine",
|
||||
};
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
export const SPRITE_SHEET_COLOR_MAP = {
|
||||
"transparent": Sprite.TRANSPARENT,
|
||||
"#ffffff": Sprite.BORDER,
|
||||
"#000000": Sprite.OUTLINE,
|
||||
"#010a19": Sprite.BEAK,
|
||||
"#190301": Sprite.EYE,
|
||||
"#af8e75": Sprite.FOOT,
|
||||
"#639bff": Sprite.FACE,
|
||||
"#99e550": Sprite.HOOD,
|
||||
"#d95763": Sprite.NOSE,
|
||||
"#f8b143": Sprite.BELLY,
|
||||
"#ec8637": Sprite.UNDERBELLY,
|
||||
"#578ae6": Sprite.WING,
|
||||
"#326ed9": Sprite.WING_EDGE,
|
||||
"#c82e2e": Sprite.HEART,
|
||||
"#501a1a": Sprite.HEART_BORDER,
|
||||
"#ff6b6b": Sprite.HEART_SHINE,
|
||||
"#373737": Sprite.FEATHER_SPINE,
|
||||
};
|
||||
|
||||
export class BirdType {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} description
|
||||
* @param {Record<string, string>} colors
|
||||
* @param {string[]} [tags]
|
||||
*/
|
||||
constructor(name, description, colors, tags = []) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
const defaultColors = {
|
||||
[Sprite.TRANSPARENT]: "transparent",
|
||||
[Sprite.OUTLINE]: "#000000",
|
||||
[Sprite.BORDER]: "#ffffff",
|
||||
[Sprite.BEAK]: "#000000",
|
||||
[Sprite.EYE]: "#000000",
|
||||
[Sprite.HEART]: "#c82e2e",
|
||||
[Sprite.HEART_BORDER]: "#501a1a",
|
||||
[Sprite.HEART_SHINE]: "#ff6b6b",
|
||||
[Sprite.FEATHER_SPINE]: "#373737",
|
||||
[Sprite.HOOD]: colors.face,
|
||||
[Sprite.NOSE]: colors.face,
|
||||
};
|
||||
/** @type {Record<string, string>} */
|
||||
this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Record<string, BirdType>} */
|
||||
export const SPECIES = {
|
||||
bluebird: new BirdType("Eastern Bluebird",
|
||||
"Native to North American and very social, though can be timid around people.", {
|
||||
[Sprite.FOOT]: "#af8e75",
|
||||
[Sprite.FACE]: "#639bff",
|
||||
[Sprite.BELLY]: "#f8b143",
|
||||
[Sprite.UNDERBELLY]: "#ec8637",
|
||||
[Sprite.WING]: "#578ae6",
|
||||
[Sprite.WING_EDGE]: "#326ed9",
|
||||
}),
|
||||
shimaEnaga: new BirdType("Shima Enaga",
|
||||
"Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", {
|
||||
[Sprite.FOOT]: "#af8e75",
|
||||
[Sprite.FACE]: "#ffffff",
|
||||
[Sprite.BELLY]: "#ebe9e8",
|
||||
[Sprite.UNDERBELLY]: "#ebd9d0",
|
||||
[Sprite.WING]: "#f3d3c1",
|
||||
[Sprite.WING_EDGE]: "#2d2d2dff",
|
||||
[Sprite.THEME_HIGHLIGHT]: "#d7ac93",
|
||||
}),
|
||||
tuftedTitmouse: new BirdType("Tufted Titmouse",
|
||||
"Native to the eastern United States, full of personality, and notably my wife's favorite bird.", {
|
||||
[Sprite.FOOT]: "#af8e75",
|
||||
[Sprite.FACE]: "#c7cad7",
|
||||
[Sprite.BELLY]: "#e4e5eb",
|
||||
[Sprite.UNDERBELLY]: "#d7cfcb",
|
||||
[Sprite.WING]: "#b1b5c5",
|
||||
[Sprite.WING_EDGE]: "#9d9fa9",
|
||||
}, ["tuft"]),
|
||||
europeanRobin: new BirdType("European Robin",
|
||||
"Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", {
|
||||
[Sprite.FOOT]: "#af8e75",
|
||||
[Sprite.FACE]: "#ffaf34",
|
||||
[Sprite.HOOD]: "#aaa094",
|
||||
[Sprite.BELLY]: "#ffaf34",
|
||||
[Sprite.UNDERBELLY]: "#babec2",
|
||||
[Sprite.WING]: "#aaa094",
|
||||
[Sprite.WING_EDGE]: "#888580",
|
||||
[Sprite.THEME_HIGHLIGHT]: "#ffaf34",
|
||||
}),
|
||||
redCardinal: new BirdType("Red Cardinal",
|
||||
"Native to the eastern United States, this strikingly red bird is hard to miss.", {
|
||||
[Sprite.BEAK]: "#d93619",
|
||||
[Sprite.FOOT]: "#af8e75",
|
||||
[Sprite.FACE]: "#31353d",
|
||||
[Sprite.HOOD]: "#e83a1b",
|
||||
[Sprite.BELLY]: "#e83a1b",
|
||||
[Sprite.UNDERBELLY]: "#dc3719",
|
||||
[Sprite.WING]: "#d23215",
|
||||
[Sprite.WING_EDGE]: "#b1321c",
|
||||
}, ["tuft"]),
|
||||
americanGoldfinch: new BirdType("American Goldfinch",
|
||||
"Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", {
|
||||
[Sprite.BEAK]: "#ffaf34",
|
||||
[Sprite.FOOT]: "#af8e75",
|
||||
[Sprite.FACE]: "#fff255",
|
||||
[Sprite.NOSE]: "#383838",
|
||||
[Sprite.HOOD]: "#383838",
|
||||
[Sprite.BELLY]: "#fff255",
|
||||
[Sprite.UNDERBELLY]: "#f5ea63",
|
||||
[Sprite.WING]: "#e8e079",
|
||||
[Sprite.WING_EDGE]: "#191919",
|
||||
[Sprite.THEME_HIGHLIGHT]: "#ffcc00"
|
||||
}),
|
||||
barnSwallow: new BirdType("Barn Swallow",
|
||||
"Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", {
|
||||
[Sprite.FOOT]: "#af8e75",
|
||||
[Sprite.FACE]: "#db7c4d",
|
||||
[Sprite.BELLY]: "#f7e1c9",
|
||||
[Sprite.UNDERBELLY]: "#ebc9a3",
|
||||
[Sprite.WING]: "#2252a9",
|
||||
[Sprite.WING_EDGE]: "#1c448b",
|
||||
[Sprite.HOOD]: "#2252a9",
|
||||
}),
|
||||
mistletoebird: new BirdType("Mistletoebird",
|
||||
"Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", {
|
||||
[Sprite.FOOT]: "#6c6a7c",
|
||||
[Sprite.FACE]: "#352e6d",
|
||||
[Sprite.BELLY]: "#fd6833",
|
||||
[Sprite.UNDERBELLY]: "#e6e1d8",
|
||||
[Sprite.WING]: "#342b7c",
|
||||
[Sprite.WING_EDGE]: "#282065",
|
||||
}),
|
||||
redAvadavat: new BirdType("Red Avadavat",
|
||||
"Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.", {
|
||||
[Sprite.BEAK]: "#f71919",
|
||||
[Sprite.FOOT]: "#af7575",
|
||||
[Sprite.FACE]: "#cb092b",
|
||||
[Sprite.BELLY]: "#ae1724",
|
||||
[Sprite.UNDERBELLY]: "#831b24",
|
||||
[Sprite.WING]: "#7e3030",
|
||||
[Sprite.WING_EDGE]: "#490f0f",
|
||||
}),
|
||||
scarletRobin: new BirdType("Scarlet Robin",
|
||||
"Native to Australia, this striking robin can be found in Eucalyptus forests.", {
|
||||
[Sprite.FOOT]: "#494949",
|
||||
[Sprite.FACE]: "#3d3d3d",
|
||||
[Sprite.BELLY]: "#fc5633",
|
||||
[Sprite.UNDERBELLY]: "#dcdcdc",
|
||||
[Sprite.WING]: "#2b2b2b",
|
||||
[Sprite.WING_EDGE]: "#ebebeb",
|
||||
[Sprite.THEME_HIGHLIGHT]: "#fc5633",
|
||||
}),
|
||||
americanRobin: new BirdType("American Robin",
|
||||
"While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", {
|
||||
[Sprite.BEAK]: "#e89f30",
|
||||
[Sprite.FOOT]: "#9f8075",
|
||||
[Sprite.FACE]: "#2d2d2d",
|
||||
[Sprite.BELLY]: "#eb7a3a",
|
||||
[Sprite.UNDERBELLY]: "#eb7a3a",
|
||||
[Sprite.WING]: "#444444",
|
||||
[Sprite.WING_EDGE]: "#232323",
|
||||
[Sprite.THEME_HIGHLIGHT]: "#eb7a3a",
|
||||
}),
|
||||
carolinaWren: new BirdType("Carolina Wren",
|
||||
"Native to the eastern United States, these little birds are known for their curious and energetic nature.", {
|
||||
[Sprite.FOOT]: "#af8e75",
|
||||
[Sprite.FACE]: "#edc7a9",
|
||||
[Sprite.NOSE]: "#f7eee5",
|
||||
[Sprite.HOOD]: "#c58a5b",
|
||||
[Sprite.BELLY]: "#e1b796",
|
||||
[Sprite.UNDERBELLY]: "#c79e7c",
|
||||
[Sprite.WING]: "#c58a5b",
|
||||
[Sprite.WING_EDGE]: "#866348",
|
||||
}),
|
||||
};
|
||||
Reference in New Issue
Block a user