mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-24 19:59:36 +00:00
Use rollup and split up files
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/node_modules
|
||||
.DS_Store
|
||||
/dist/birb.bundled.js
|
||||
|
||||
20
README.md
20
README.md
@@ -6,3 +6,23 @@ This project is still being worked on, but if you wish to help me beta test it,
|
||||
2. Enable the Tampermonkey extension and give it the permissions requested
|
||||
3. Install my Pocket Bird script by going to this link and clicking install: [https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js](https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js)
|
||||
4. Now any websites you visit will have a little bird hopping around!
|
||||
|
||||
## Development
|
||||
|
||||
This project uses Rollup to bundle the source files.
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Development Mode
|
||||
|
||||
Watch for changes and rebuild automatically:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The source files are in the `src/` directory. The main entry point is `src/birb.js`, which bundles all the other modules together.
|
||||
20
build.js
20
build.js
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { rollup } from 'rollup';
|
||||
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
||||
|
||||
const spriteSheets = [
|
||||
{
|
||||
@@ -52,7 +53,6 @@ try {
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
const userScriptHeader =
|
||||
`// ==UserScript==
|
||||
// @name Pocket Bird
|
||||
@@ -70,8 +70,19 @@ const userScriptHeader =
|
||||
|
||||
`;
|
||||
|
||||
// Bundle with rollup
|
||||
const bundle = await rollup({
|
||||
input: 'src/birb.js',
|
||||
});
|
||||
|
||||
let birbJs = readFileSync('birb.js', 'utf8');
|
||||
await bundle.write({
|
||||
file: 'dist/birb.bundled.js',
|
||||
format: 'iife',
|
||||
});
|
||||
|
||||
await bundle.close();
|
||||
|
||||
let birbJs = readFileSync('dist/birb.bundled.js', 'utf8');
|
||||
|
||||
// Compile and insert sprite sheets
|
||||
for (const spriteSheet of spriteSheets) {
|
||||
@@ -86,6 +97,9 @@ birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent);
|
||||
// Build standard javascript file
|
||||
writeFileSync('./dist/birb.js', birbJs);
|
||||
|
||||
// Delete bundled file
|
||||
unlinkSync('./dist/birb.bundled.js');
|
||||
|
||||
// Build user script
|
||||
const userScript = userScriptHeader + birbJs;
|
||||
writeFileSync('./dist/birb.user.js', userScript);
|
||||
525
dist/birb.js
vendored
525
dist/birb.js
vendored
@@ -1,41 +1,207 @@
|
||||
// @ts-check
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const SHARED_CONFIG = {
|
||||
// @ts-check
|
||||
|
||||
// Theme color indicators
|
||||
const THEME_HIGHLIGHT = "theme-highlight";
|
||||
const TRANSPARENT = "transparent";
|
||||
const OUTLINE = "outline";
|
||||
const BORDER = "border";
|
||||
const FOOT = "foot";
|
||||
const BEAK = "beak";
|
||||
const EYE = "eye";
|
||||
const FACE = "face";
|
||||
const HOOD = "hood";
|
||||
const NOSE = "nose";
|
||||
const BELLY = "belly";
|
||||
const UNDERBELLY = "underbelly";
|
||||
const WING = "wing";
|
||||
const WING_EDGE = "wing-edge";
|
||||
const HEART = "heart";
|
||||
const HEART_BORDER = "heart-border";
|
||||
const HEART_SHINE = "heart-shine";
|
||||
const FEATHER_SPINE = "feather-spine";
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const SPRITE_SHEET_COLOR_MAP = {
|
||||
"transparent": TRANSPARENT,
|
||||
"#ffffff": BORDER,
|
||||
"#000000": OUTLINE,
|
||||
"#010a19": BEAK,
|
||||
"#190301": EYE,
|
||||
"#af8e75": FOOT,
|
||||
"#639bff": FACE,
|
||||
"#99e550": HOOD,
|
||||
"#d95763": NOSE,
|
||||
"#f8b143": BELLY,
|
||||
"#ec8637": UNDERBELLY,
|
||||
"#578ae6": WING,
|
||||
"#326ed9": WING_EDGE,
|
||||
"#c82e2e": HEART,
|
||||
"#501a1a": HEART_BORDER,
|
||||
"#ff6b6b": HEART_SHINE,
|
||||
"#373737": FEATHER_SPINE,
|
||||
};
|
||||
|
||||
const Directions = {
|
||||
LEFT: -1,
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
// @ts-check
|
||||
|
||||
class Layer {
|
||||
/**
|
||||
* @param {string[][]} pixels
|
||||
* @param {string} [tag]
|
||||
*/
|
||||
constructor(pixels, tag = "default") {
|
||||
this.pixels = pixels;
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
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 = {
|
||||
[TRANSPARENT]: "transparent",
|
||||
[OUTLINE]: "#000000",
|
||||
[BORDER]: "#ffffff",
|
||||
[BEAK]: "#000000",
|
||||
[EYE]: "#000000",
|
||||
[HEART]: "#c82e2e",
|
||||
[HEART_BORDER]: "#501a1a",
|
||||
[HEART_SHINE]: "#ff6b6b",
|
||||
[FEATHER_SPINE]: "#373737",
|
||||
[HOOD]: colors.face,
|
||||
[NOSE]: colors.face,
|
||||
};
|
||||
/** @type {Record<string, string>} */
|
||||
this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
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(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] !== 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) {
|
||||
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);
|
||||
} } }
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
const SHARED_CONFIG = {
|
||||
birbCssScale: 1,
|
||||
uiCssScale: 1,
|
||||
canvasPixelSize: 1,
|
||||
hopSpeed: 0.07,
|
||||
hopDistance: 45,
|
||||
};
|
||||
};
|
||||
|
||||
const DESKTOP_CONFIG = {
|
||||
const DESKTOP_CONFIG = {
|
||||
flySpeed: 0.25
|
||||
};
|
||||
};
|
||||
|
||||
const MOBILE_CONFIG = {
|
||||
const MOBILE_CONFIG = {
|
||||
uiCssScale: 0.9,
|
||||
flySpeed: 0.125,
|
||||
};
|
||||
};
|
||||
|
||||
const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG };
|
||||
const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG };
|
||||
|
||||
let debugMode = location.hostname === "127.0.0.1";
|
||||
let frozen = false;
|
||||
let debugMode = location.hostname === "127.0.0.1";
|
||||
let frozen = false;
|
||||
|
||||
const BIRB_CSS_SCALE = CONFIG.birbCssScale;
|
||||
const UI_CSS_SCALE = CONFIG.uiCssScale;
|
||||
const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize;
|
||||
const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE;
|
||||
const BIRB_CSS_SCALE = CONFIG.birbCssScale;
|
||||
const UI_CSS_SCALE = CONFIG.uiCssScale;
|
||||
const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize;
|
||||
const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE;
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
const DEFAULT_SETTINGS = {
|
||||
birbMode: false
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
/**
|
||||
* @typedef {typeof DEFAULT_SETTINGS} Settings
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* @typedef {Object} SavedStickyNote
|
||||
* @property {string} id
|
||||
* @property {string} site
|
||||
@@ -44,7 +210,7 @@ const DEFAULT_SETTINGS = {
|
||||
* @property {number} left
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* @typedef {Object} BirbSaveData
|
||||
* @property {string[]} unlockedSpecies
|
||||
* @property {string} currentSpecies
|
||||
@@ -52,10 +218,10 @@ const DEFAULT_SETTINGS = {
|
||||
* @property {SavedStickyNote[]} [stickyNotes]
|
||||
*/
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
|
||||
const STYLESHEET = `:root {
|
||||
const STYLESHEET = `:root {
|
||||
--birb-border-size: 2px;
|
||||
--birb-neg-border-size: calc(var(--birb-border-size) * -1);
|
||||
--birb-double-border-size: calc(var(--birb-border-size) * 2);
|
||||
@@ -400,85 +566,7 @@ const STYLESHEET = `:root {
|
||||
outline: none;
|
||||
}`;
|
||||
|
||||
class Layer {
|
||||
/**
|
||||
* @param {string[][]} pixels
|
||||
* @param {string} [tag]
|
||||
*/
|
||||
constructor(pixels, tag = "default") {
|
||||
this.pixels = pixels;
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
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(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] !== 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 {number} direction
|
||||
* @param {BirdType} [species]
|
||||
*/
|
||||
draw(ctx, direction, species) {
|
||||
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 * CANVAS_PIXEL_SIZE, y * CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE);
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Anim {
|
||||
class Anim {
|
||||
/**
|
||||
* @param {Frame[]} frames
|
||||
* @param {number[]} durations
|
||||
@@ -511,87 +599,18 @@ class Anim {
|
||||
for (let i = 0; i < this.durations.length; i++) {
|
||||
totalDuration += this.durations[i];
|
||||
if (time < totalDuration) {
|
||||
this.frames[i].draw(ctx, direction, species);
|
||||
this.frames[i].draw(ctx, direction, CANVAS_PIXEL_SIZE, species);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Draw the last frame if the animation is complete
|
||||
this.frames[this.frames.length - 1].draw(ctx, direction, species);
|
||||
this.frames[this.frames.length - 1].draw(ctx, direction, CANVAS_PIXEL_SIZE, species);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const THEME_HIGHLIGHT = "theme-highlight";
|
||||
const TRANSPARENT = "transparent";
|
||||
const OUTLINE = "outline";
|
||||
const BORDER = "border";
|
||||
const FOOT = "foot";
|
||||
const BEAK = "beak";
|
||||
const EYE = "eye";
|
||||
const FACE = "face";
|
||||
const HOOD = "hood";
|
||||
const NOSE = "nose";
|
||||
const BELLY = "belly";
|
||||
const UNDERBELLY = "underbelly";
|
||||
const WING = "wing";
|
||||
const WING_EDGE = "wing-edge";
|
||||
const HEART = "heart";
|
||||
const HEART_BORDER = "heart-border";
|
||||
const HEART_SHINE = "heart-shine";
|
||||
const FEATHER_SPINE = "feather-spine";
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const SPRITE_SHEET_COLOR_MAP = {
|
||||
"transparent": TRANSPARENT,
|
||||
"#ffffff": BORDER,
|
||||
"#000000": OUTLINE,
|
||||
"#010a19": BEAK,
|
||||
"#190301": EYE,
|
||||
"#af8e75": FOOT,
|
||||
"#639bff": FACE,
|
||||
"#99e550": HOOD,
|
||||
"#d95763": NOSE,
|
||||
"#f8b143": BELLY,
|
||||
"#ec8637": UNDERBELLY,
|
||||
"#578ae6": WING,
|
||||
"#326ed9": WING_EDGE,
|
||||
"#c82e2e": HEART,
|
||||
"#501a1a": HEART_BORDER,
|
||||
"#ff6b6b": HEART_SHINE,
|
||||
"#373737": FEATHER_SPINE,
|
||||
};
|
||||
|
||||
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 = {
|
||||
[TRANSPARENT]: "transparent",
|
||||
[OUTLINE]: "#000000",
|
||||
[BORDER]: "#ffffff",
|
||||
[BEAK]: "#000000",
|
||||
[EYE]: "#000000",
|
||||
[HEART]: "#c82e2e",
|
||||
[HEART_BORDER]: "#501a1a",
|
||||
[HEART_SHINE]: "#ff6b6b",
|
||||
[FEATHER_SPINE]: "#373737",
|
||||
[HOOD]: colors.face,
|
||||
[NOSE]: colors.face,
|
||||
};
|
||||
/** @type {Record<string, string>} */
|
||||
this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Record<string, BirdType>} */
|
||||
const species = {
|
||||
/** @type {Record<string, BirdType>} */
|
||||
const species = {
|
||||
bluebird: new BirdType("Eastern Bluebird",
|
||||
"Native to North American and very social, though can be timid around people.", {
|
||||
[FOOT]: "#af8e75",
|
||||
@@ -716,60 +735,54 @@ const species = {
|
||||
[WING]: "#c58a5b",
|
||||
[WING_EDGE]: "#866348",
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_BIRD = "bluebird";
|
||||
const DEFAULT_BIRD = "bluebird";
|
||||
|
||||
const SPRITE_WIDTH = 32;
|
||||
const SPRITE_HEIGHT = 32;
|
||||
const DECORATIONS_SPRITE_WIDTH = 48;
|
||||
const FEATHER_SPRITE_WIDTH = 32;
|
||||
|
||||
const Directions = {
|
||||
LEFT: -1,
|
||||
RIGHT: 1,
|
||||
};
|
||||
const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg==";
|
||||
const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg==";
|
||||
const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII=";
|
||||
|
||||
const SPRITE_WIDTH = 32;
|
||||
const SPRITE_HEIGHT = 32;
|
||||
const DECORATIONS_SPRITE_WIDTH = 48;
|
||||
const FEATHER_SPRITE_WIDTH = 32;
|
||||
const MENU_ID = "birb-menu";
|
||||
const MENU_EXIT_ID = "birb-menu-exit";
|
||||
const FIELD_GUIDE_ID = "birb-field-guide";
|
||||
const FEATHER_ID = "birb-feather";
|
||||
|
||||
const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg==";
|
||||
const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg==";
|
||||
const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII=";
|
||||
const HOP_SPEED = CONFIG.hopSpeed;
|
||||
const FLY_SPEED = CONFIG.flySpeed;
|
||||
const HOP_DISTANCE = CONFIG.hopDistance;
|
||||
/** Speed at which the feather falls per tick */
|
||||
const FEATHER_FALL_SPEED = 1;
|
||||
/** Time in milliseconds until the user is considered AFK */
|
||||
const AFK_TIME = debugMode ? 0 : 1000 * 30;
|
||||
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
|
||||
// Per-frame chances
|
||||
const HOP_CHANCE = 1 / (60 * 3); // 3 seconds
|
||||
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // 20 seconds
|
||||
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 2 hours
|
||||
/** Multiplier after petting that increases the feather drop chance */
|
||||
const PET_FEATHER_BOOST = 2;
|
||||
/** How long the pet boost lasts in milliseconds */
|
||||
const PET_BOOST_DURATION = 1000 * 60 * 5;
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_TOP = 80;
|
||||
/** Time between checking whether the URL has changed */
|
||||
const URL_CHECK_INTERVAL = 500;
|
||||
/** Time after petting before the menu can be opened */
|
||||
const PET_MENU_COOLDOWN = 1000;
|
||||
|
||||
const MENU_ID = "birb-menu";
|
||||
const MENU_EXIT_ID = "birb-menu-exit";
|
||||
const FIELD_GUIDE_ID = "birb-field-guide";
|
||||
const FEATHER_ID = "birb-feather";
|
||||
|
||||
const HOP_SPEED = CONFIG.hopSpeed;
|
||||
const FLY_SPEED = CONFIG.flySpeed;
|
||||
const HOP_DISTANCE = CONFIG.hopDistance;
|
||||
/** Speed at which the feather falls per tick */
|
||||
const FEATHER_FALL_SPEED = 1;
|
||||
/** Time in milliseconds until the user is considered AFK */
|
||||
const AFK_TIME = debugMode ? 0 : 1000 * 30;
|
||||
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
|
||||
// Per-frame chances
|
||||
const HOP_CHANCE = 1 / (60 * 3); // 3 seconds
|
||||
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // 20 seconds
|
||||
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 2 hours
|
||||
/** Multiplier after petting that increases the feather drop chance */
|
||||
const PET_FEATHER_BOOST = 2;
|
||||
/** How long the pet boost lasts in milliseconds */
|
||||
const PET_BOOST_DURATION = 1000 * 60 * 5;
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_TOP = 80;
|
||||
/** Time between checking whether the URL has changed */
|
||||
const URL_CHECK_INTERVAL = 500;
|
||||
/** Time after petting before the menu can be opened */
|
||||
const PET_MENU_COOLDOWN = 1000;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Load the sprite sheet and return the pixel-map template
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {Promise<string[][]>}
|
||||
*/
|
||||
function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
||||
function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.src = dataUri;
|
||||
@@ -817,15 +830,15 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log("Loading sprite sheets...");
|
||||
log("Loading sprite sheets...");
|
||||
|
||||
Promise.all([
|
||||
Promise.all([
|
||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
||||
loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false),
|
||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
||||
]).then(([birbPixels, decorationPixels, featherPixels]) => {
|
||||
]).then(([birbPixels, decorationPixels, featherPixels]) => {
|
||||
|
||||
const SPRITE_SHEET = birbPixels;
|
||||
const DECORATIONS_SPRITE_SHEET = decorationPixels;
|
||||
@@ -863,9 +876,9 @@ Promise.all([
|
||||
heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]),
|
||||
};
|
||||
|
||||
const decorationFrames = {
|
||||
({
|
||||
mac: new Frame([decorationLayers.mac]),
|
||||
};
|
||||
});
|
||||
|
||||
const featherFrames = {
|
||||
feather: new Frame([featherLayers.feather]),
|
||||
@@ -912,14 +925,6 @@ Promise.all([
|
||||
], false),
|
||||
};
|
||||
|
||||
const DECORATION_ANIMATIONS = {
|
||||
mac: new Anim([
|
||||
decorationFrames.mac,
|
||||
], [
|
||||
1000,
|
||||
]),
|
||||
};
|
||||
|
||||
const FEATHER_ANIMATIONS = {
|
||||
feather: new Anim([
|
||||
featherFrames.feather,
|
||||
@@ -1356,7 +1361,7 @@ Promise.all([
|
||||
</div>
|
||||
<div class="birb-window-content">
|
||||
<textarea class="birb-sticky-note-input" style="width: 150px;" placeholder="Write your notes here and they'll stick to the page!">${stickyNote.content}</textarea>
|
||||
</div>`
|
||||
</div>`;
|
||||
const noteElement = makeElement("birb-window");
|
||||
noteElement.classList.add("birb-sticky-note");
|
||||
noteElement.innerHTML = html;
|
||||
@@ -1501,9 +1506,6 @@ Promise.all([
|
||||
const closeButton = window.querySelector(".birb-window-close");
|
||||
if (closeButton) {
|
||||
makeClosable(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
window.remove();
|
||||
}, closeButton);
|
||||
}
|
||||
@@ -1511,23 +1513,6 @@ Promise.all([
|
||||
return window;
|
||||
}
|
||||
|
||||
function insertDecoration() {
|
||||
// Create a canvas element for the decoration
|
||||
const decorationCanvas = document.createElement("canvas");
|
||||
decorationCanvas.classList.add("birb-decoration");
|
||||
decorationCanvas.width = DECORATIONS_SPRITE_WIDTH * CANVAS_PIXEL_SIZE;
|
||||
decorationCanvas.height = DECORATIONS_SPRITE_WIDTH * CANVAS_PIXEL_SIZE;
|
||||
const decorationCtx = decorationCanvas.getContext("2d");
|
||||
if (!decorationCtx) {
|
||||
return;
|
||||
}
|
||||
// Draw the decoration
|
||||
DECORATION_ANIMATIONS.mac.draw(decorationCtx, Directions.LEFT, Date.now());
|
||||
// Add the decoration to the page
|
||||
document.body.appendChild(decorationCanvas);
|
||||
makeDraggable(decorationCanvas, false);
|
||||
}
|
||||
|
||||
function activateFeather() {
|
||||
if (document.querySelector("#" + FEATHER_ID)) {
|
||||
return;
|
||||
@@ -1639,7 +1624,7 @@ Promise.all([
|
||||
<div class="birb-window-content">
|
||||
<div class="birb-grid-content"></div>
|
||||
<div class="birb-field-guide-description"></div>
|
||||
</div>`
|
||||
</div>`;
|
||||
const fieldGuide = makeElement("birb-window", undefined, FIELD_GUIDE_ID);
|
||||
fieldGuide.innerHTML = html;
|
||||
document.body.appendChild(fieldGuide);
|
||||
@@ -1682,7 +1667,7 @@ Promise.all([
|
||||
if (!speciesCtx) {
|
||||
return;
|
||||
}
|
||||
birbFrames.base.draw(speciesCtx, Directions.RIGHT, type);
|
||||
birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type);
|
||||
speciesElement.appendChild(speciesCanvas);
|
||||
content.appendChild(speciesElement);
|
||||
if (unlocked) {
|
||||
@@ -1732,10 +1717,6 @@ Promise.all([
|
||||
}
|
||||
}
|
||||
|
||||
function isSafari() {
|
||||
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
*/
|
||||
@@ -2069,10 +2050,6 @@ Promise.all([
|
||||
return canvas.width * BIRB_CSS_SCALE
|
||||
}
|
||||
|
||||
function getCanvasHeight() {
|
||||
return canvas.height * BIRB_CSS_SCALE
|
||||
}
|
||||
|
||||
function hop() {
|
||||
if (frozen) {
|
||||
return;
|
||||
@@ -2213,27 +2190,29 @@ Promise.all([
|
||||
// Run the birb
|
||||
init();
|
||||
draw();
|
||||
}).catch((e) => {
|
||||
}).catch((e) => {
|
||||
error("Error while loading sprite sheets: ", e);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
/**
|
||||
* @returns {boolean} Whether the user is on a mobile device
|
||||
*/
|
||||
function isMobile() {
|
||||
function isMobile() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
function log() {
|
||||
function log() {
|
||||
console.log("Birb: ", ...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
function debug() {
|
||||
function debug() {
|
||||
if (debugMode) {
|
||||
console.debug("Birb: ", ...arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function error() {
|
||||
function error() {
|
||||
console.error("Birb: ", ...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
527
dist/birb.user.js
vendored
527
dist/birb.user.js
vendored
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name Pocket Bird
|
||||
// @namespace https://idreesinc.com
|
||||
// @version 2025.10.25.130
|
||||
// @version 2025.10.26.16
|
||||
// @description birb
|
||||
// @author Idrees
|
||||
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
|
||||
@@ -12,44 +12,210 @@
|
||||
// @grant GM_deleteValue
|
||||
// ==/UserScript==
|
||||
|
||||
// @ts-check
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const SHARED_CONFIG = {
|
||||
// @ts-check
|
||||
|
||||
// Theme color indicators
|
||||
const THEME_HIGHLIGHT = "theme-highlight";
|
||||
const TRANSPARENT = "transparent";
|
||||
const OUTLINE = "outline";
|
||||
const BORDER = "border";
|
||||
const FOOT = "foot";
|
||||
const BEAK = "beak";
|
||||
const EYE = "eye";
|
||||
const FACE = "face";
|
||||
const HOOD = "hood";
|
||||
const NOSE = "nose";
|
||||
const BELLY = "belly";
|
||||
const UNDERBELLY = "underbelly";
|
||||
const WING = "wing";
|
||||
const WING_EDGE = "wing-edge";
|
||||
const HEART = "heart";
|
||||
const HEART_BORDER = "heart-border";
|
||||
const HEART_SHINE = "heart-shine";
|
||||
const FEATHER_SPINE = "feather-spine";
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const SPRITE_SHEET_COLOR_MAP = {
|
||||
"transparent": TRANSPARENT,
|
||||
"#ffffff": BORDER,
|
||||
"#000000": OUTLINE,
|
||||
"#010a19": BEAK,
|
||||
"#190301": EYE,
|
||||
"#af8e75": FOOT,
|
||||
"#639bff": FACE,
|
||||
"#99e550": HOOD,
|
||||
"#d95763": NOSE,
|
||||
"#f8b143": BELLY,
|
||||
"#ec8637": UNDERBELLY,
|
||||
"#578ae6": WING,
|
||||
"#326ed9": WING_EDGE,
|
||||
"#c82e2e": HEART,
|
||||
"#501a1a": HEART_BORDER,
|
||||
"#ff6b6b": HEART_SHINE,
|
||||
"#373737": FEATHER_SPINE,
|
||||
};
|
||||
|
||||
const Directions = {
|
||||
LEFT: -1,
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
// @ts-check
|
||||
|
||||
class Layer {
|
||||
/**
|
||||
* @param {string[][]} pixels
|
||||
* @param {string} [tag]
|
||||
*/
|
||||
constructor(pixels, tag = "default") {
|
||||
this.pixels = pixels;
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
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 = {
|
||||
[TRANSPARENT]: "transparent",
|
||||
[OUTLINE]: "#000000",
|
||||
[BORDER]: "#ffffff",
|
||||
[BEAK]: "#000000",
|
||||
[EYE]: "#000000",
|
||||
[HEART]: "#c82e2e",
|
||||
[HEART_BORDER]: "#501a1a",
|
||||
[HEART_SHINE]: "#ff6b6b",
|
||||
[FEATHER_SPINE]: "#373737",
|
||||
[HOOD]: colors.face,
|
||||
[NOSE]: colors.face,
|
||||
};
|
||||
/** @type {Record<string, string>} */
|
||||
this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
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(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] !== 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) {
|
||||
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);
|
||||
} } }
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
const SHARED_CONFIG = {
|
||||
birbCssScale: 1,
|
||||
uiCssScale: 1,
|
||||
canvasPixelSize: 1,
|
||||
hopSpeed: 0.07,
|
||||
hopDistance: 45,
|
||||
};
|
||||
};
|
||||
|
||||
const DESKTOP_CONFIG = {
|
||||
const DESKTOP_CONFIG = {
|
||||
flySpeed: 0.25
|
||||
};
|
||||
};
|
||||
|
||||
const MOBILE_CONFIG = {
|
||||
const MOBILE_CONFIG = {
|
||||
uiCssScale: 0.9,
|
||||
flySpeed: 0.125,
|
||||
};
|
||||
};
|
||||
|
||||
const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG };
|
||||
const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG };
|
||||
|
||||
let debugMode = location.hostname === "127.0.0.1";
|
||||
let frozen = false;
|
||||
let debugMode = location.hostname === "127.0.0.1";
|
||||
let frozen = false;
|
||||
|
||||
const BIRB_CSS_SCALE = CONFIG.birbCssScale;
|
||||
const UI_CSS_SCALE = CONFIG.uiCssScale;
|
||||
const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize;
|
||||
const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE;
|
||||
const BIRB_CSS_SCALE = CONFIG.birbCssScale;
|
||||
const UI_CSS_SCALE = CONFIG.uiCssScale;
|
||||
const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize;
|
||||
const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE;
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
const DEFAULT_SETTINGS = {
|
||||
birbMode: false
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
/**
|
||||
* @typedef {typeof DEFAULT_SETTINGS} Settings
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* @typedef {Object} SavedStickyNote
|
||||
* @property {string} id
|
||||
* @property {string} site
|
||||
@@ -58,7 +224,7 @@ const DEFAULT_SETTINGS = {
|
||||
* @property {number} left
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* @typedef {Object} BirbSaveData
|
||||
* @property {string[]} unlockedSpecies
|
||||
* @property {string} currentSpecies
|
||||
@@ -66,10 +232,10 @@ const DEFAULT_SETTINGS = {
|
||||
* @property {SavedStickyNote[]} [stickyNotes]
|
||||
*/
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
|
||||
const STYLESHEET = `:root {
|
||||
const STYLESHEET = `:root {
|
||||
--birb-border-size: 2px;
|
||||
--birb-neg-border-size: calc(var(--birb-border-size) * -1);
|
||||
--birb-double-border-size: calc(var(--birb-border-size) * 2);
|
||||
@@ -414,85 +580,7 @@ const STYLESHEET = `:root {
|
||||
outline: none;
|
||||
}`;
|
||||
|
||||
class Layer {
|
||||
/**
|
||||
* @param {string[][]} pixels
|
||||
* @param {string} [tag]
|
||||
*/
|
||||
constructor(pixels, tag = "default") {
|
||||
this.pixels = pixels;
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
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(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] !== 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 {number} direction
|
||||
* @param {BirdType} [species]
|
||||
*/
|
||||
draw(ctx, direction, species) {
|
||||
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 * CANVAS_PIXEL_SIZE, y * CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE);
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Anim {
|
||||
class Anim {
|
||||
/**
|
||||
* @param {Frame[]} frames
|
||||
* @param {number[]} durations
|
||||
@@ -525,87 +613,18 @@ class Anim {
|
||||
for (let i = 0; i < this.durations.length; i++) {
|
||||
totalDuration += this.durations[i];
|
||||
if (time < totalDuration) {
|
||||
this.frames[i].draw(ctx, direction, species);
|
||||
this.frames[i].draw(ctx, direction, CANVAS_PIXEL_SIZE, species);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Draw the last frame if the animation is complete
|
||||
this.frames[this.frames.length - 1].draw(ctx, direction, species);
|
||||
this.frames[this.frames.length - 1].draw(ctx, direction, CANVAS_PIXEL_SIZE, species);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const THEME_HIGHLIGHT = "theme-highlight";
|
||||
const TRANSPARENT = "transparent";
|
||||
const OUTLINE = "outline";
|
||||
const BORDER = "border";
|
||||
const FOOT = "foot";
|
||||
const BEAK = "beak";
|
||||
const EYE = "eye";
|
||||
const FACE = "face";
|
||||
const HOOD = "hood";
|
||||
const NOSE = "nose";
|
||||
const BELLY = "belly";
|
||||
const UNDERBELLY = "underbelly";
|
||||
const WING = "wing";
|
||||
const WING_EDGE = "wing-edge";
|
||||
const HEART = "heart";
|
||||
const HEART_BORDER = "heart-border";
|
||||
const HEART_SHINE = "heart-shine";
|
||||
const FEATHER_SPINE = "feather-spine";
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const SPRITE_SHEET_COLOR_MAP = {
|
||||
"transparent": TRANSPARENT,
|
||||
"#ffffff": BORDER,
|
||||
"#000000": OUTLINE,
|
||||
"#010a19": BEAK,
|
||||
"#190301": EYE,
|
||||
"#af8e75": FOOT,
|
||||
"#639bff": FACE,
|
||||
"#99e550": HOOD,
|
||||
"#d95763": NOSE,
|
||||
"#f8b143": BELLY,
|
||||
"#ec8637": UNDERBELLY,
|
||||
"#578ae6": WING,
|
||||
"#326ed9": WING_EDGE,
|
||||
"#c82e2e": HEART,
|
||||
"#501a1a": HEART_BORDER,
|
||||
"#ff6b6b": HEART_SHINE,
|
||||
"#373737": FEATHER_SPINE,
|
||||
};
|
||||
|
||||
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 = {
|
||||
[TRANSPARENT]: "transparent",
|
||||
[OUTLINE]: "#000000",
|
||||
[BORDER]: "#ffffff",
|
||||
[BEAK]: "#000000",
|
||||
[EYE]: "#000000",
|
||||
[HEART]: "#c82e2e",
|
||||
[HEART_BORDER]: "#501a1a",
|
||||
[HEART_SHINE]: "#ff6b6b",
|
||||
[FEATHER_SPINE]: "#373737",
|
||||
[HOOD]: colors.face,
|
||||
[NOSE]: colors.face,
|
||||
};
|
||||
/** @type {Record<string, string>} */
|
||||
this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Record<string, BirdType>} */
|
||||
const species = {
|
||||
/** @type {Record<string, BirdType>} */
|
||||
const species = {
|
||||
bluebird: new BirdType("Eastern Bluebird",
|
||||
"Native to North American and very social, though can be timid around people.", {
|
||||
[FOOT]: "#af8e75",
|
||||
@@ -730,60 +749,54 @@ const species = {
|
||||
[WING]: "#c58a5b",
|
||||
[WING_EDGE]: "#866348",
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_BIRD = "bluebird";
|
||||
const DEFAULT_BIRD = "bluebird";
|
||||
|
||||
const SPRITE_WIDTH = 32;
|
||||
const SPRITE_HEIGHT = 32;
|
||||
const DECORATIONS_SPRITE_WIDTH = 48;
|
||||
const FEATHER_SPRITE_WIDTH = 32;
|
||||
|
||||
const Directions = {
|
||||
LEFT: -1,
|
||||
RIGHT: 1,
|
||||
};
|
||||
const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg==";
|
||||
const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg==";
|
||||
const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII=";
|
||||
|
||||
const SPRITE_WIDTH = 32;
|
||||
const SPRITE_HEIGHT = 32;
|
||||
const DECORATIONS_SPRITE_WIDTH = 48;
|
||||
const FEATHER_SPRITE_WIDTH = 32;
|
||||
const MENU_ID = "birb-menu";
|
||||
const MENU_EXIT_ID = "birb-menu-exit";
|
||||
const FIELD_GUIDE_ID = "birb-field-guide";
|
||||
const FEATHER_ID = "birb-feather";
|
||||
|
||||
const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg==";
|
||||
const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg==";
|
||||
const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII=";
|
||||
const HOP_SPEED = CONFIG.hopSpeed;
|
||||
const FLY_SPEED = CONFIG.flySpeed;
|
||||
const HOP_DISTANCE = CONFIG.hopDistance;
|
||||
/** Speed at which the feather falls per tick */
|
||||
const FEATHER_FALL_SPEED = 1;
|
||||
/** Time in milliseconds until the user is considered AFK */
|
||||
const AFK_TIME = debugMode ? 0 : 1000 * 30;
|
||||
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
|
||||
// Per-frame chances
|
||||
const HOP_CHANCE = 1 / (60 * 3); // 3 seconds
|
||||
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // 20 seconds
|
||||
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 2 hours
|
||||
/** Multiplier after petting that increases the feather drop chance */
|
||||
const PET_FEATHER_BOOST = 2;
|
||||
/** How long the pet boost lasts in milliseconds */
|
||||
const PET_BOOST_DURATION = 1000 * 60 * 5;
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_TOP = 80;
|
||||
/** Time between checking whether the URL has changed */
|
||||
const URL_CHECK_INTERVAL = 500;
|
||||
/** Time after petting before the menu can be opened */
|
||||
const PET_MENU_COOLDOWN = 1000;
|
||||
|
||||
const MENU_ID = "birb-menu";
|
||||
const MENU_EXIT_ID = "birb-menu-exit";
|
||||
const FIELD_GUIDE_ID = "birb-field-guide";
|
||||
const FEATHER_ID = "birb-feather";
|
||||
|
||||
const HOP_SPEED = CONFIG.hopSpeed;
|
||||
const FLY_SPEED = CONFIG.flySpeed;
|
||||
const HOP_DISTANCE = CONFIG.hopDistance;
|
||||
/** Speed at which the feather falls per tick */
|
||||
const FEATHER_FALL_SPEED = 1;
|
||||
/** Time in milliseconds until the user is considered AFK */
|
||||
const AFK_TIME = debugMode ? 0 : 1000 * 30;
|
||||
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
|
||||
// Per-frame chances
|
||||
const HOP_CHANCE = 1 / (60 * 3); // 3 seconds
|
||||
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // 20 seconds
|
||||
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 2 hours
|
||||
/** Multiplier after petting that increases the feather drop chance */
|
||||
const PET_FEATHER_BOOST = 2;
|
||||
/** How long the pet boost lasts in milliseconds */
|
||||
const PET_BOOST_DURATION = 1000 * 60 * 5;
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_TOP = 80;
|
||||
/** Time between checking whether the URL has changed */
|
||||
const URL_CHECK_INTERVAL = 500;
|
||||
/** Time after petting before the menu can be opened */
|
||||
const PET_MENU_COOLDOWN = 1000;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Load the sprite sheet and return the pixel-map template
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {Promise<string[][]>}
|
||||
*/
|
||||
function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
||||
function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.src = dataUri;
|
||||
@@ -831,15 +844,15 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log("Loading sprite sheets...");
|
||||
log("Loading sprite sheets...");
|
||||
|
||||
Promise.all([
|
||||
Promise.all([
|
||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
||||
loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false),
|
||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
||||
]).then(([birbPixels, decorationPixels, featherPixels]) => {
|
||||
]).then(([birbPixels, decorationPixels, featherPixels]) => {
|
||||
|
||||
const SPRITE_SHEET = birbPixels;
|
||||
const DECORATIONS_SPRITE_SHEET = decorationPixels;
|
||||
@@ -877,9 +890,9 @@ Promise.all([
|
||||
heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]),
|
||||
};
|
||||
|
||||
const decorationFrames = {
|
||||
({
|
||||
mac: new Frame([decorationLayers.mac]),
|
||||
};
|
||||
});
|
||||
|
||||
const featherFrames = {
|
||||
feather: new Frame([featherLayers.feather]),
|
||||
@@ -926,14 +939,6 @@ Promise.all([
|
||||
], false),
|
||||
};
|
||||
|
||||
const DECORATION_ANIMATIONS = {
|
||||
mac: new Anim([
|
||||
decorationFrames.mac,
|
||||
], [
|
||||
1000,
|
||||
]),
|
||||
};
|
||||
|
||||
const FEATHER_ANIMATIONS = {
|
||||
feather: new Anim([
|
||||
featherFrames.feather,
|
||||
@@ -1370,7 +1375,7 @@ Promise.all([
|
||||
</div>
|
||||
<div class="birb-window-content">
|
||||
<textarea class="birb-sticky-note-input" style="width: 150px;" placeholder="Write your notes here and they'll stick to the page!">${stickyNote.content}</textarea>
|
||||
</div>`
|
||||
</div>`;
|
||||
const noteElement = makeElement("birb-window");
|
||||
noteElement.classList.add("birb-sticky-note");
|
||||
noteElement.innerHTML = html;
|
||||
@@ -1515,9 +1520,6 @@ Promise.all([
|
||||
const closeButton = window.querySelector(".birb-window-close");
|
||||
if (closeButton) {
|
||||
makeClosable(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
window.remove();
|
||||
}, closeButton);
|
||||
}
|
||||
@@ -1525,23 +1527,6 @@ Promise.all([
|
||||
return window;
|
||||
}
|
||||
|
||||
function insertDecoration() {
|
||||
// Create a canvas element for the decoration
|
||||
const decorationCanvas = document.createElement("canvas");
|
||||
decorationCanvas.classList.add("birb-decoration");
|
||||
decorationCanvas.width = DECORATIONS_SPRITE_WIDTH * CANVAS_PIXEL_SIZE;
|
||||
decorationCanvas.height = DECORATIONS_SPRITE_WIDTH * CANVAS_PIXEL_SIZE;
|
||||
const decorationCtx = decorationCanvas.getContext("2d");
|
||||
if (!decorationCtx) {
|
||||
return;
|
||||
}
|
||||
// Draw the decoration
|
||||
DECORATION_ANIMATIONS.mac.draw(decorationCtx, Directions.LEFT, Date.now());
|
||||
// Add the decoration to the page
|
||||
document.body.appendChild(decorationCanvas);
|
||||
makeDraggable(decorationCanvas, false);
|
||||
}
|
||||
|
||||
function activateFeather() {
|
||||
if (document.querySelector("#" + FEATHER_ID)) {
|
||||
return;
|
||||
@@ -1653,7 +1638,7 @@ Promise.all([
|
||||
<div class="birb-window-content">
|
||||
<div class="birb-grid-content"></div>
|
||||
<div class="birb-field-guide-description"></div>
|
||||
</div>`
|
||||
</div>`;
|
||||
const fieldGuide = makeElement("birb-window", undefined, FIELD_GUIDE_ID);
|
||||
fieldGuide.innerHTML = html;
|
||||
document.body.appendChild(fieldGuide);
|
||||
@@ -1696,7 +1681,7 @@ Promise.all([
|
||||
if (!speciesCtx) {
|
||||
return;
|
||||
}
|
||||
birbFrames.base.draw(speciesCtx, Directions.RIGHT, type);
|
||||
birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type);
|
||||
speciesElement.appendChild(speciesCanvas);
|
||||
content.appendChild(speciesElement);
|
||||
if (unlocked) {
|
||||
@@ -1746,10 +1731,6 @@ Promise.all([
|
||||
}
|
||||
}
|
||||
|
||||
function isSafari() {
|
||||
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
*/
|
||||
@@ -2083,10 +2064,6 @@ Promise.all([
|
||||
return canvas.width * BIRB_CSS_SCALE
|
||||
}
|
||||
|
||||
function getCanvasHeight() {
|
||||
return canvas.height * BIRB_CSS_SCALE
|
||||
}
|
||||
|
||||
function hop() {
|
||||
if (frozen) {
|
||||
return;
|
||||
@@ -2227,27 +2204,29 @@ Promise.all([
|
||||
// Run the birb
|
||||
init();
|
||||
draw();
|
||||
}).catch((e) => {
|
||||
}).catch((e) => {
|
||||
error("Error while loading sprite sheets: ", e);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
/**
|
||||
* @returns {boolean} Whether the user is on a mobile device
|
||||
*/
|
||||
function isMobile() {
|
||||
function isMobile() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
function log() {
|
||||
function log() {
|
||||
console.log("Birb: ", ...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
function debug() {
|
||||
function debug() {
|
||||
if (debugMode) {
|
||||
console.debug("Birb: ", ...arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function error() {
|
||||
function error() {
|
||||
console.error("Birb: ", ...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -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.25.130",
|
||||
"version": "2025.10.26.16",
|
||||
"homepage_url": "https://idreesinc.com",
|
||||
"content_scripts": [
|
||||
{
|
||||
|
||||
360
package-lock.json
generated
360
package-lock.json
generated
@@ -9,9 +9,325 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.10"
|
||||
"nodemon": "^3.1.10",
|
||||
"rollup": "^4.52.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
||||
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
|
||||
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
|
||||
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
|
||||
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
|
||||
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
|
||||
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
|
||||
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
|
||||
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
|
||||
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
|
||||
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
|
||||
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
|
||||
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
|
||||
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
@@ -316,6 +632,48 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.52.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
||||
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.52.5",
|
||||
"@rollup/rollup-android-arm64": "4.52.5",
|
||||
"@rollup/rollup-darwin-arm64": "4.52.5",
|
||||
"@rollup/rollup-darwin-x64": "4.52.5",
|
||||
"@rollup/rollup-freebsd-arm64": "4.52.5",
|
||||
"@rollup/rollup-freebsd-x64": "4.52.5",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.52.5",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.52.5",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.52.5",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.52.5",
|
||||
"@rollup/rollup-linux-x64-musl": "4.52.5",
|
||||
"@rollup/rollup-openharmony-arm64": "4.52.5",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.52.5",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.52.5",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.52.5",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"dev": "nodemon --watch birb.js --watch stylesheet.css --watch build.js --exec \"npm run build\""
|
||||
"dev": "nodemon --watch src --watch stylesheet.css --watch build.js --exec \"npm run build\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.10"
|
||||
"nodemon": "^3.1.10",
|
||||
"rollup": "^4.52.5"
|
||||
}
|
||||
}
|
||||
|
||||
74
src/Frame.js
Normal file
74
src/Frame.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// @ts-check
|
||||
import { TRANSPARENT, Directions } from './constants.js';
|
||||
import Layer from './Layer.js';
|
||||
import BirdType from './birdType.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(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] !== 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) {
|
||||
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;
|
||||
14
src/Layer.js
Normal file
14
src/Layer.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// @ts-check
|
||||
|
||||
class Layer {
|
||||
/**
|
||||
* @param {string[][]} pixels
|
||||
* @param {string} [tag]
|
||||
*/
|
||||
constructor(pixels, tag = "default") {
|
||||
this.pixels = pixels;
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
export default Layer;
|
||||
@@ -1,5 +1,33 @@
|
||||
// @ts-check
|
||||
|
||||
import {
|
||||
THEME_HIGHLIGHT,
|
||||
TRANSPARENT,
|
||||
OUTLINE,
|
||||
BORDER,
|
||||
FOOT,
|
||||
BEAK,
|
||||
EYE,
|
||||
FACE,
|
||||
HOOD,
|
||||
NOSE,
|
||||
BELLY,
|
||||
UNDERBELLY,
|
||||
WING,
|
||||
WING_EDGE,
|
||||
HEART,
|
||||
HEART_BORDER,
|
||||
HEART_SHINE,
|
||||
FEATHER_SPINE,
|
||||
SPRITE_SHEET_COLOR_MAP,
|
||||
Directions
|
||||
} from './constants.js';
|
||||
|
||||
import Frame from './Frame.js';
|
||||
import Layer from './Layer.js';
|
||||
import BirdType from './birdType.js';
|
||||
|
||||
// @ts-ignore
|
||||
const SHARED_CONFIG = {
|
||||
birbCssScale: 1,
|
||||
uiCssScale: 1,
|
||||
@@ -57,84 +85,6 @@ let userSettings = {};
|
||||
|
||||
const STYLESHEET = `___STYLESHEET___`;
|
||||
|
||||
class Layer {
|
||||
/**
|
||||
* @param {string[][]} pixels
|
||||
* @param {string} [tag]
|
||||
*/
|
||||
constructor(pixels, tag = "default") {
|
||||
this.pixels = pixels;
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
|
||||
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(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] !== 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 {number} direction
|
||||
* @param {BirdType} [species]
|
||||
*/
|
||||
draw(ctx, direction, species) {
|
||||
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 * CANVAS_PIXEL_SIZE, y * CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE);
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Anim {
|
||||
/**
|
||||
* @param {Frame[]} frames
|
||||
@@ -168,85 +118,16 @@ class Anim {
|
||||
for (let i = 0; i < this.durations.length; i++) {
|
||||
totalDuration += this.durations[i];
|
||||
if (time < totalDuration) {
|
||||
this.frames[i].draw(ctx, direction, species);
|
||||
this.frames[i].draw(ctx, direction, CANVAS_PIXEL_SIZE, species);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Draw the last frame if the animation is complete
|
||||
this.frames[this.frames.length - 1].draw(ctx, direction, species);
|
||||
this.frames[this.frames.length - 1].draw(ctx, direction, CANVAS_PIXEL_SIZE, species);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const THEME_HIGHLIGHT = "theme-highlight";
|
||||
const TRANSPARENT = "transparent";
|
||||
const OUTLINE = "outline";
|
||||
const BORDER = "border";
|
||||
const FOOT = "foot";
|
||||
const BEAK = "beak";
|
||||
const EYE = "eye";
|
||||
const FACE = "face";
|
||||
const HOOD = "hood";
|
||||
const NOSE = "nose";
|
||||
const BELLY = "belly";
|
||||
const UNDERBELLY = "underbelly";
|
||||
const WING = "wing";
|
||||
const WING_EDGE = "wing-edge";
|
||||
const HEART = "heart";
|
||||
const HEART_BORDER = "heart-border";
|
||||
const HEART_SHINE = "heart-shine";
|
||||
const FEATHER_SPINE = "feather-spine";
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const SPRITE_SHEET_COLOR_MAP = {
|
||||
"transparent": TRANSPARENT,
|
||||
"#ffffff": BORDER,
|
||||
"#000000": OUTLINE,
|
||||
"#010a19": BEAK,
|
||||
"#190301": EYE,
|
||||
"#af8e75": FOOT,
|
||||
"#639bff": FACE,
|
||||
"#99e550": HOOD,
|
||||
"#d95763": NOSE,
|
||||
"#f8b143": BELLY,
|
||||
"#ec8637": UNDERBELLY,
|
||||
"#578ae6": WING,
|
||||
"#326ed9": WING_EDGE,
|
||||
"#c82e2e": HEART,
|
||||
"#501a1a": HEART_BORDER,
|
||||
"#ff6b6b": HEART_SHINE,
|
||||
"#373737": FEATHER_SPINE,
|
||||
};
|
||||
|
||||
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 = {
|
||||
[TRANSPARENT]: "transparent",
|
||||
[OUTLINE]: "#000000",
|
||||
[BORDER]: "#ffffff",
|
||||
[BEAK]: "#000000",
|
||||
[EYE]: "#000000",
|
||||
[HEART]: "#c82e2e",
|
||||
[HEART_BORDER]: "#501a1a",
|
||||
[HEART_SHINE]: "#ff6b6b",
|
||||
[FEATHER_SPINE]: "#373737",
|
||||
[HOOD]: colors.face,
|
||||
[NOSE]: colors.face,
|
||||
};
|
||||
/** @type {Record<string, string>} */
|
||||
this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Record<string, BirdType>} */
|
||||
const species = {
|
||||
bluebird: new BirdType("Eastern Bluebird",
|
||||
@@ -377,12 +258,6 @@ const species = {
|
||||
|
||||
const DEFAULT_BIRD = "bluebird";
|
||||
|
||||
|
||||
const Directions = {
|
||||
LEFT: -1,
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
const SPRITE_WIDTH = 32;
|
||||
const SPRITE_HEIGHT = 32;
|
||||
const DECORATIONS_SPRITE_WIDTH = 48;
|
||||
@@ -1339,7 +1214,7 @@ Promise.all([
|
||||
if (!speciesCtx) {
|
||||
return;
|
||||
}
|
||||
birbFrames.base.draw(speciesCtx, Directions.RIGHT, type);
|
||||
birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type);
|
||||
speciesElement.appendChild(speciesCanvas);
|
||||
content.appendChild(speciesElement);
|
||||
if (unlocked) {
|
||||
47
src/birdType.js
Normal file
47
src/birdType.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// @ts-check
|
||||
|
||||
import {
|
||||
THEME_HIGHLIGHT,
|
||||
OUTLINE,
|
||||
BORDER,
|
||||
BEAK,
|
||||
EYE,
|
||||
HEART,
|
||||
HEART_BORDER,
|
||||
HEART_SHINE,
|
||||
FEATHER_SPINE,
|
||||
TRANSPARENT,
|
||||
NOSE,
|
||||
HOOD
|
||||
} from './constants.js';
|
||||
|
||||
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 = {
|
||||
[TRANSPARENT]: "transparent",
|
||||
[OUTLINE]: "#000000",
|
||||
[BORDER]: "#ffffff",
|
||||
[BEAK]: "#000000",
|
||||
[EYE]: "#000000",
|
||||
[HEART]: "#c82e2e",
|
||||
[HEART_BORDER]: "#501a1a",
|
||||
[HEART_SHINE]: "#ff6b6b",
|
||||
[FEATHER_SPINE]: "#373737",
|
||||
[HOOD]: colors.face,
|
||||
[NOSE]: colors.face,
|
||||
};
|
||||
/** @type {Record<string, string>} */
|
||||
this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
||||
|
||||
export default BirdType;
|
||||
47
src/constants.js
Normal file
47
src/constants.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// @ts-check
|
||||
|
||||
// Theme color indicators
|
||||
export const THEME_HIGHLIGHT = "theme-highlight";
|
||||
export const TRANSPARENT = "transparent";
|
||||
export const OUTLINE = "outline";
|
||||
export const BORDER = "border";
|
||||
export const FOOT = "foot";
|
||||
export const BEAK = "beak";
|
||||
export const EYE = "eye";
|
||||
export const FACE = "face";
|
||||
export const HOOD = "hood";
|
||||
export const NOSE = "nose";
|
||||
export const BELLY = "belly";
|
||||
export const UNDERBELLY = "underbelly";
|
||||
export const WING = "wing";
|
||||
export const WING_EDGE = "wing-edge";
|
||||
export const HEART = "heart";
|
||||
export const HEART_BORDER = "heart-border";
|
||||
export const HEART_SHINE = "heart-shine";
|
||||
export const FEATHER_SPINE = "feather-spine";
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
export const SPRITE_SHEET_COLOR_MAP = {
|
||||
"transparent": TRANSPARENT,
|
||||
"#ffffff": BORDER,
|
||||
"#000000": OUTLINE,
|
||||
"#010a19": BEAK,
|
||||
"#190301": EYE,
|
||||
"#af8e75": FOOT,
|
||||
"#639bff": FACE,
|
||||
"#99e550": HOOD,
|
||||
"#d95763": NOSE,
|
||||
"#f8b143": BELLY,
|
||||
"#ec8637": UNDERBELLY,
|
||||
"#578ae6": WING,
|
||||
"#326ed9": WING_EDGE,
|
||||
"#c82e2e": HEART,
|
||||
"#501a1a": HEART_BORDER,
|
||||
"#ff6b6b": HEART_SHINE,
|
||||
"#373737": FEATHER_SPINE,
|
||||
};
|
||||
|
||||
export const Directions = {
|
||||
LEFT: -1,
|
||||
RIGHT: 1,
|
||||
};
|
||||
Reference in New Issue
Block a user