From 8be8ab2858cb004cb7ed9d0477d51acf86abcbf6 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 13:17:18 -0400 Subject: [PATCH 01/16] Use rollup and split up files --- .gitignore | 1 + README.md | 22 +- build.js | 20 +- dist/birb.js | 3493 ++++++++++++++++++++------------------- dist/birb.user.js | 3495 ++++++++++++++++++++-------------------- manifest.json | 2 +- package-lock.json | 360 ++++- package.json | 5 +- src/Frame.js | 74 + src/Layer.js | 14 + birb.js => src/birb.js | 187 +-- src/birdType.js | 47 + src/constants.js | 47 + 13 files changed, 4088 insertions(+), 3679 deletions(-) create mode 100644 src/Frame.js create mode 100644 src/Layer.js rename birb.js => src/birb.js (92%) create mode 100644 src/birdType.js create mode 100644 src/constants.js diff --git a/.gitignore b/.gitignore index 68b21f0..43d5463 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules .DS_Store +/dist/birb.bundled.js diff --git a/README.md b/README.md index b73fe0d..391b87b 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,24 @@ This project is still being worked on, but if you wish to help me beta test it, 1. Install [Tampermonkey](https://www.tampermonkey.net/) on your web browser 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! \ No newline at end of file +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. \ No newline at end of file diff --git a/build.js b/build.js index 238ae8c..26c4de0 100644 --- a/build.js +++ b/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); \ No newline at end of file diff --git a/dist/birb.js b/dist/birb.js index d9b3a20..3d88a7c 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -1,61 +1,227 @@ -// @ts-check +(function () { + 'use strict'; -const SHARED_CONFIG = { - birbCssScale: 1, - uiCssScale: 1, - canvasPixelSize: 1, - hopSpeed: 0.07, - hopDistance: 45, -}; + // @ts-check -const DESKTOP_CONFIG = { - flySpeed: 0.25 -}; + // 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"; -const MOBILE_CONFIG = { - uiCssScale: 0.9, - flySpeed: 0.125, -}; + /** @type {Record} */ + 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 CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG }; + const Directions = { + LEFT: -1, + RIGHT: 1, + }; -let debugMode = location.hostname === "127.0.0.1"; -let frozen = false; + // @ts-check -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; + class Layer { + /** + * @param {string[][]} pixels + * @param {string} [tag] + */ + constructor(pixels, tag = "default") { + this.pixels = pixels; + this.tag = tag; + } + } -const DEFAULT_SETTINGS = { - birbMode: false -}; + // @ts-check -/** - * @typedef {typeof DEFAULT_SETTINGS} Settings - */ -/** - * @typedef {Object} SavedStickyNote - * @property {string} id - * @property {string} site - * @property {string} content - * @property {number} top - * @property {number} left - */ + class BirdType { + /** + * @param {string} name + * @param {string} description + * @param {Record} 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} */ + this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.tags = tags; + } + } -/** - * @typedef {Object} BirbSaveData - * @property {string[]} unlockedSpecies - * @property {string} currentSpecies - * @property {Partial} settings - * @property {SavedStickyNote[]} [stickyNotes] - */ + // @ts-check -/** @type {Partial} */ -let userSettings = {}; + class Frame { -const STYLESHEET = `:root { + /** @type {{ [tag: string]: string[][] }} */ + #pixelsByTag = {}; + + /** + * @param {Layer[]} layers + */ + constructor(layers) { + /** @type {Set} */ + 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 = { + flySpeed: 0.25 + }; + + const MOBILE_CONFIG = { + uiCssScale: 0.9, + flySpeed: 0.125, + }; + + const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG }; + + 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 DEFAULT_SETTINGS = { + birbMode: false + }; + + /** + * @typedef {typeof DEFAULT_SETTINGS} Settings + */ + + /** + * @typedef {Object} SavedStickyNote + * @property {string} id + * @property {string} site + * @property {string} content + * @property {number} top + * @property {number} left + */ + + /** + * @typedef {Object} BirbSaveData + * @property {string[]} unlockedSpecies + * @property {string} currentSpecies + * @property {Partial} settings + * @property {SavedStickyNote[]} [stickyNotes] + */ + + /** @type {Partial} */ + let userSettings = {}; + + 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,782 +566,621 @@ 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} */ - let tags = new Set(); - for (let layer of layers) { - tags.add(layer.tag); + 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; } - 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"); + + getAnimationDuration() { + return this.durations.reduce((a, b) => a + b, 0); + } + + /** + * @param {CanvasRenderingContext2D} ctx + * @param {number} direction + * @param {number} timeStart The start time of the animation in milliseconds + * @param {BirdType} [species] The species to use for the animation + * @returns {boolean} Whether the animation is complete + */ + draw(ctx, direction, timeStart, species) { + let time = Date.now() - timeStart; + const duration = this.getAnimationDuration(); + if (this.loop) { + time %= duration; } - 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)); + let totalDuration = 0; + for (let i = 0; i < this.durations.length; i++) { + totalDuration += this.durations[i]; + if (time < totalDuration) { + this.frames[i].draw(ctx, direction, CANVAS_PIXEL_SIZE, species); + return false; + } } - // 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]; + // Draw the last frame if the animation is complete + this.frames[this.frames.length - 1].draw(ctx, direction, CANVAS_PIXEL_SIZE, species); + return true; + } + } + + /** @type {Record} */ + const species = { + bluebird: new BirdType("Eastern Bluebird", + "Native to North American and very social, though can be timid around people.", { + [FOOT]: "#af8e75", + [FACE]: "#639bff", + [BELLY]: "#f8b143", + [UNDERBELLY]: "#ec8637", + [WING]: "#578ae6", + [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.", { + [FOOT]: "#af8e75", + [FACE]: "#ffffff", + [BELLY]: "#ebe9e8", + [UNDERBELLY]: "#ebd9d0", + [WING]: "#f3d3c1", + [WING_EDGE]: "#2d2d2dff", + [THEME_HIGHLIGHT]: "#d7ac93", + }), + tuftedTitmouse: new BirdType("Tufted Titmouse", + "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", { + [FOOT]: "#af8e75", + [FACE]: "#c7cad7", + [BELLY]: "#e4e5eb", + [UNDERBELLY]: "#d7cfcb", + [WING]: "#b1b5c5", + [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.", { + [FOOT]: "#af8e75", + [FACE]: "#ffaf34", + [HOOD]: "#aaa094", + [BELLY]: "#ffaf34", + [UNDERBELLY]: "#babec2", + [WING]: "#aaa094", + [WING_EDGE]: "#888580", + [THEME_HIGHLIGHT]: "#ffaf34", + }), + redCardinal: new BirdType("Red Cardinal", + "Native to the eastern United States, this strikingly red bird is hard to miss.", { + [BEAK]: "#d93619", + [FOOT]: "#af8e75", + [FACE]: "#31353d", + [HOOD]: "#e83a1b", + [BELLY]: "#e83a1b", + [UNDERBELLY]: "#dc3719", + [WING]: "#d23215", + [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.", { + [BEAK]: "#ffaf34", + [FOOT]: "#af8e75", + [FACE]: "#fff255", + [NOSE]: "#383838", + [HOOD]: "#383838", + [BELLY]: "#fff255", + [UNDERBELLY]: "#f5ea63", + [WING]: "#e8e079", + [WING_EDGE]: "#191919", + [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.", { + [FOOT]: "#af8e75", + [FACE]: "#db7c4d", + [BELLY]: "#f7e1c9", + [UNDERBELLY]: "#ebc9a3", + [WING]: "#2252a9", + [WING_EDGE]: "#1c448b", + [HOOD]: "#2252a9", + }), + mistletoebird: new BirdType("Mistletoebird", + "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", { + [FOOT]: "#6c6a7c", + [FACE]: "#352e6d", + [BELLY]: "#fd6833", + [UNDERBELLY]: "#e6e1d8", + [WING]: "#342b7c", + [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.", { + [BEAK]: "#f71919", + [FOOT]: "#af7575", + [FACE]: "#cb092b", + [BELLY]: "#ae1724", + [UNDERBELLY]: "#831b24", + [WING]: "#7e3030", + [WING_EDGE]: "#490f0f", + }), + scarletRobin: new BirdType("Scarlet Robin", + "Native to Australia, this striking robin can be found in Eucalyptus forests.", { + [FOOT]: "#494949", + [FACE]: "#3d3d3d", + [BELLY]: "#fc5633", + [UNDERBELLY]: "#dcdcdc", + [WING]: "#2b2b2b", + [WING_EDGE]: "#ebebeb", + [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.", { + [BEAK]: "#e89f30", + [FOOT]: "#9f8075", + [FACE]: "#2d2d2d", + [BELLY]: "#eb7a3a", + [UNDERBELLY]: "#eb7a3a", + [WING]: "#444444", + [WING_EDGE]: "#232323", + [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.", { + [FOOT]: "#af8e75", + [FACE]: "#edc7a9", + [NOSE]: "#f7eee5", + [HOOD]: "#c58a5b", + [BELLY]: "#e1b796", + [UNDERBELLY]: "#c79e7c", + [WING]: "#c58a5b", + [WING_EDGE]: "#866348", + }), + }; + + const DEFAULT_BIRD = "bluebird"; + + const SPRITE_WIDTH = 32; + const SPRITE_HEIGHT = 32; + const DECORATIONS_SPRITE_WIDTH = 48; + const FEATHER_SPRITE_WIDTH = 32; + + 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 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} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(TRANSPARENT); + continue; } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + log("Loading sprite sheets..."); + + Promise.all([ + loadSpriteSheetPixels(SPRITE_SHEET), + loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), + loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) + ]).then(([birbPixels, decorationPixels, featherPixels]) => { + + const SPRITE_SHEET = birbPixels; + const DECORATIONS_SPRITE_SHEET = decorationPixels; + const FEATHER_SPRITE_SHEET = featherPixels; + + const layers = { + base: new Layer(getLayer(SPRITE_SHEET, 0)), + down: new Layer(getLayer(SPRITE_SHEET, 1)), + heartOne: new Layer(getLayer(SPRITE_SHEET, 2)), + heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), + heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), + tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"), + tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"), + wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)), + wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)), + happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), + }; + + const decorationLayers = { + mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)), + }; + + const featherLayers = { + feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), + }; + + const birbFrames = { + base: new Frame([layers.base, layers.tuftBase]), + headDown: new Frame([layers.down, layers.tuftDown]), + wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]), + wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]), + heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]), + heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), + heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]), + heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), + }; + + ({ + mac: new Frame([decorationLayers.mac]), + }); + + const featherFrames = { + feather: new Frame([featherLayers.feather]), + }; + + const Animations = { + STILL: new Anim([birbFrames.base], [1000]), + BOB: new Anim([ + birbFrames.base, + birbFrames.headDown + ], [ + 420, + 420 + ]), + FLYING: new Anim([ + birbFrames.base, + birbFrames.wingsUp, + birbFrames.headDown, + birbFrames.wingsDown, + ], [ + 30, + 80, + 30, + 60, + ]), + HEART: new Anim([ + birbFrames.heartOne, + birbFrames.heartTwo, + birbFrames.heartThree, + birbFrames.heartFour, + birbFrames.heartThree, + birbFrames.heartFour, + birbFrames.heartThree, + birbFrames.heartFour, + ], [ + 60, + 80, + 250, + 250, + 250, + 250, + 250, + 250, + ], false), + }; + + const FEATHER_ANIMATIONS = { + feather: new Anim([ + featherFrames.feather, + ], [ + 1000, + ]), + }; + + class MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {boolean} [removeMenu] + * @param {boolean} [isDebug] + */ + constructor(text, action, removeMenu = true, isDebug = false) { + this.text = text; + this.action = action; + this.removeMenu = removeMenu; + this.isDebug = isDebug; + } + } + + class DebugMenuItem extends MenuItem { + /** + * @param {string} text + * @param {() => void} action + */ + constructor(text, action, removeMenu = true) { + super(text, action, removeMenu, true); + } + } + + class Separator extends MenuItem { + constructor() { + super("", () => { }); + } + } + + class StickyNote { + /** + * @param {string} id + * @param {string} [site] + * @param {string} [content] + * @param {number} [top] + * @param {number} [left] + */ + constructor(id, site = "", content = "", top = 0, left = 0) { + this.id = id; + this.site = site; + this.content = content; + this.top = top; + this.left = left; + } + } + + const menuItems = [ + new MenuItem(`Pet ${birdBirb()}`, pet), + new MenuItem("Field Guide", insertFieldGuide), + new MenuItem("Sticky Note", newStickyNote), + new MenuItem(`Hide ${birdBirb()}`, hideBirb), + new DebugMenuItem("Freeze/Unfreeze", () => { + frozen = !frozen; + }), + new DebugMenuItem("Reset Data", resetSaveData), + new DebugMenuItem("Unlock All", () => { + for (let type in species) { + unlockBird(type); + } + }), + new DebugMenuItem("Disable Debug", () => { + debugMode = false; + }), + new Separator(), + new MenuItem("Settings", () => switchMenuItems(settingsItems), false), + ]; + + const settingsItems = [ + new MenuItem("Go Back", () => switchMenuItems(menuItems), false), + new Separator(), + new MenuItem("Toggle Birb Mode", () => { + userSettings.birbMode = !userSettings.birbMode; + save(); + insertModal(`${birdBirb()} Mode`, `Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"${userSettings.birbMode ? "\n\nWelcome back to 2012" : ""}`); + }) + ]; + + const styleElement = document.createElement("style"); + const canvas = document.createElement("canvas"); + + /** @type {CanvasRenderingContext2D} */ + // @ts-expect-error + const ctx = canvas.getContext("2d"); + + const States = { + IDLE: "idle", + HOP: "hop", + FLYING: "flying", + }; + + let stateStart = Date.now(); + let currentState = States.IDLE; + let animStart = Date.now(); + let currentAnimation = Animations.BOB; + let direction = Directions.RIGHT; + let ticks = 0; + // Bird's current position + let birdY = 0; + let birdX = 40; + // Bird's starting position (when flying) + let startX = 0; + let startY = 0; + // Bird's target position (when flying) + let targetX = 0; + let targetY = 0; + /** @type {HTMLElement|null} */ + let focusedElement = null; + let focusedBounds = { left: 0, right: 0, top: 0 }; + let lastActionTimestamp = Date.now(); + /** @type {number[]} */ + let petStack = []; + let currentSpecies = DEFAULT_BIRD; + let unlockedSpecies = [DEFAULT_BIRD]; + let visible = true; + let lastPetTimestamp = 0; + /** @type {StickyNote[]} */ + let stickyNotes = []; + + /** + * @returns {boolean} Whether the script is running in a userscript extension context + */ + function isUserScript() { + // @ts-expect-error + return typeof GM_getValue === "function"; + } + + function isTestEnvironment() { + return window.location.hostname === "127.0.0.1" + || window.location.hostname === "localhost" + || window.location.hostname.startsWith("192.168."); + } + + function load() { + /** @type {Record} */ + let saveData = {}; + + if (isUserScript()) { + log("Loading save data from UserScript storage"); + // @ts-expect-error + saveData = GM_getValue("birbSaveData", {}) ?? {}; + } else if (isTestEnvironment()) { + log("Test environment detected, loading save data from localStorage"); + saveData = JSON.parse(localStorage.getItem("birbSaveData") ?? "{}"); + } else { + log("Not a UserScript"); + } + + debug("Loaded data: " + JSON.stringify(saveData)); + + if (!saveData.settings) { + log("No user settings found in save data, starting fresh"); + } + + userSettings = saveData.settings ?? {}; + unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; + currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; + stickyNotes = []; + + if (saveData.stickyNotes) { + for (let note of saveData.stickyNotes) { + if (note.id) { + stickyNotes.push(new StickyNote(note.id, note.site, note.content, note.top, note.left)); } } } - this.#pixelsByTag[tag] = this.pixels.map(row => row.slice()); + + log(stickyNotes.length + " sticky notes loaded"); + switchSpecies(currentSpecies); } - } - /** - * @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); + function save() { + /** @type {BirbSaveData} */ + const saveData = { + unlockedSpecies, + currentSpecies, + settings: userSettings }; - }; - } -} -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; - } + if (stickyNotes.length > 0) { + saveData.stickyNotes = stickyNotes.map(note => ({ + id: note.id, + site: note.site, + content: note.content, + top: note.top, + left: note.left + })); + } - getAnimationDuration() { - return this.durations.reduce((a, b) => a + b, 0); - } - - /** - * @param {CanvasRenderingContext2D} ctx - * @param {number} direction - * @param {number} timeStart The start time of the animation in milliseconds - * @param {BirdType} [species] The species to use for the animation - * @returns {boolean} Whether the animation is complete - */ - draw(ctx, direction, timeStart, species) { - let time = Date.now() - timeStart; - const duration = this.getAnimationDuration(); - if (this.loop) { - time %= duration; - } - let totalDuration = 0; - for (let i = 0; i < this.durations.length; i++) { - totalDuration += this.durations[i]; - if (time < totalDuration) { - this.frames[i].draw(ctx, direction, species); - return false; + if (isUserScript()) { + log("Saving data to UserScript storage"); + // @ts-expect-error + GM_setValue("birbSaveData", saveData); + } else if (isTestEnvironment()) { + log("Test environment detected, saving data to localStorage"); + localStorage.setItem("birbSaveData", JSON.stringify(saveData)); + } else { + log("Not a UserScript"); } } - // Draw the last frame if the animation is complete - this.frames[this.frames.length - 1].draw(ctx, direction, 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"; + function resetSaveData() { + if (isUserScript()) { + log("Resetting save data in UserScript storage"); + // @ts-expect-error + GM_deleteValue("birbSaveData"); + } else if (isTestEnvironment()) { + log("Test environment detected, resetting save data in localStorage"); + localStorage.removeItem("birbSaveData"); + } else { + log("Not a UserScript"); + } + load(); + } -/** @type {Record} */ -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, -}; + /** + * Get the user settings merged with default settings + * @returns {Settings} The merged settings + */ + function settings() { + return { ...DEFAULT_SETTINGS, ...userSettings }; + } -class BirdType { - /** - * @param {string} name - * @param {string} description - * @param {Record} 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} */ - this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; - this.tags = tags; - } -} + /** + * Bird or birb, you decide + */ + function birdBirb() { + return settings().birbMode ? "Birb" : "Bird"; + } -/** @type {Record} */ -const species = { - bluebird: new BirdType("Eastern Bluebird", - "Native to North American and very social, though can be timid around people.", { - [FOOT]: "#af8e75", - [FACE]: "#639bff", - [BELLY]: "#f8b143", - [UNDERBELLY]: "#ec8637", - [WING]: "#578ae6", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#ffffff", - [BELLY]: "#ebe9e8", - [UNDERBELLY]: "#ebd9d0", - [WING]: "#f3d3c1", - [WING_EDGE]: "#2d2d2dff", - [THEME_HIGHLIGHT]: "#d7ac93", - }), - tuftedTitmouse: new BirdType("Tufted Titmouse", - "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", { - [FOOT]: "#af8e75", - [FACE]: "#c7cad7", - [BELLY]: "#e4e5eb", - [UNDERBELLY]: "#d7cfcb", - [WING]: "#b1b5c5", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#ffaf34", - [HOOD]: "#aaa094", - [BELLY]: "#ffaf34", - [UNDERBELLY]: "#babec2", - [WING]: "#aaa094", - [WING_EDGE]: "#888580", - [THEME_HIGHLIGHT]: "#ffaf34", - }), - redCardinal: new BirdType("Red Cardinal", - "Native to the eastern United States, this strikingly red bird is hard to miss.", { - [BEAK]: "#d93619", - [FOOT]: "#af8e75", - [FACE]: "#31353d", - [HOOD]: "#e83a1b", - [BELLY]: "#e83a1b", - [UNDERBELLY]: "#dc3719", - [WING]: "#d23215", - [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.", { - [BEAK]: "#ffaf34", - [FOOT]: "#af8e75", - [FACE]: "#fff255", - [NOSE]: "#383838", - [HOOD]: "#383838", - [BELLY]: "#fff255", - [UNDERBELLY]: "#f5ea63", - [WING]: "#e8e079", - [WING_EDGE]: "#191919", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#db7c4d", - [BELLY]: "#f7e1c9", - [UNDERBELLY]: "#ebc9a3", - [WING]: "#2252a9", - [WING_EDGE]: "#1c448b", - [HOOD]: "#2252a9", - }), - mistletoebird: new BirdType("Mistletoebird", - "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", { - [FOOT]: "#6c6a7c", - [FACE]: "#352e6d", - [BELLY]: "#fd6833", - [UNDERBELLY]: "#e6e1d8", - [WING]: "#342b7c", - [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.", { - [BEAK]: "#f71919", - [FOOT]: "#af7575", - [FACE]: "#cb092b", - [BELLY]: "#ae1724", - [UNDERBELLY]: "#831b24", - [WING]: "#7e3030", - [WING_EDGE]: "#490f0f", - }), - scarletRobin: new BirdType("Scarlet Robin", - "Native to Australia, this striking robin can be found in Eucalyptus forests.", { - [FOOT]: "#494949", - [FACE]: "#3d3d3d", - [BELLY]: "#fc5633", - [UNDERBELLY]: "#dcdcdc", - [WING]: "#2b2b2b", - [WING_EDGE]: "#ebebeb", - [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.", { - [BEAK]: "#e89f30", - [FOOT]: "#9f8075", - [FACE]: "#2d2d2d", - [BELLY]: "#eb7a3a", - [UNDERBELLY]: "#eb7a3a", - [WING]: "#444444", - [WING_EDGE]: "#232323", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#edc7a9", - [NOSE]: "#f7eee5", - [HOOD]: "#c58a5b", - [BELLY]: "#e1b796", - [UNDERBELLY]: "#c79e7c", - [WING]: "#c58a5b", - [WING_EDGE]: "#866348", - }), -}; - -const DEFAULT_BIRD = "bluebird"; - - -const Directions = { - LEFT: -1, - RIGHT: 1, -}; - -const SPRITE_WIDTH = 32; -const SPRITE_HEIGHT = 32; -const DECORATIONS_SPRITE_WIDTH = 48; -const FEATHER_SPRITE_WIDTH = 32; - -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 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} - */ -function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); + function init() { + if (window !== window.top) { + // Skip installation if within an iframe + log("In iframe, skipping Birb script initialization"); return; } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); -} + log("Sprite sheets loaded successfully, initializing bird..."); -log("Loading sprite sheets..."); + // Preload font + const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf"; + const fontLink = document.createElement("link"); + fontLink.rel = "stylesheet"; + fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`; + document.head.appendChild(fontLink); -Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) -]).then(([birbPixels, decorationPixels, featherPixels]) => { - - const SPRITE_SHEET = birbPixels; - const DECORATIONS_SPRITE_SHEET = decorationPixels; - const FEATHER_SPRITE_SHEET = featherPixels; - - const layers = { - base: new Layer(getLayer(SPRITE_SHEET, 0)), - down: new Layer(getLayer(SPRITE_SHEET, 1)), - heartOne: new Layer(getLayer(SPRITE_SHEET, 2)), - heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), - heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), - tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"), - tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"), - wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)), - wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)), - happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), - }; - - const decorationLayers = { - mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)), - }; - - const featherLayers = { - feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), - }; - - const birbFrames = { - base: new Frame([layers.base, layers.tuftBase]), - headDown: new Frame([layers.down, layers.tuftDown]), - wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]), - wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]), - heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]), - heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]), - heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - }; - - const decorationFrames = { - mac: new Frame([decorationLayers.mac]), - }; - - const featherFrames = { - feather: new Frame([featherLayers.feather]), - }; - - const Animations = { - STILL: new Anim([birbFrames.base], [1000]), - BOB: new Anim([ - birbFrames.base, - birbFrames.headDown - ], [ - 420, - 420 - ]), - FLYING: new Anim([ - birbFrames.base, - birbFrames.wingsUp, - birbFrames.headDown, - birbFrames.wingsDown, - ], [ - 30, - 80, - 30, - 60, - ]), - HEART: new Anim([ - birbFrames.heartOne, - birbFrames.heartTwo, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - ], [ - 60, - 80, - 250, - 250, - 250, - 250, - 250, - 250, - ], false), - }; - - const DECORATION_ANIMATIONS = { - mac: new Anim([ - decorationFrames.mac, - ], [ - 1000, - ]), - }; - - const FEATHER_ANIMATIONS = { - feather: new Anim([ - featherFrames.feather, - ], [ - 1000, - ]), - }; - - class MenuItem { - /** - * @param {string} text - * @param {() => void} action - * @param {boolean} [removeMenu] - * @param {boolean} [isDebug] - */ - constructor(text, action, removeMenu = true, isDebug = false) { - this.text = text; - this.action = action; - this.removeMenu = removeMenu; - this.isDebug = isDebug; - } - } - - class DebugMenuItem extends MenuItem { - /** - * @param {string} text - * @param {() => void} action - */ - constructor(text, action, removeMenu = true) { - super(text, action, removeMenu, true); - } - } - - class Separator extends MenuItem { - constructor() { - super("", () => { }); - } - } - - class StickyNote { - /** - * @param {string} id - * @param {string} [site] - * @param {string} [content] - * @param {number} [top] - * @param {number} [left] - */ - constructor(id, site = "", content = "", top = 0, left = 0) { - this.id = id; - this.site = site; - this.content = content; - this.top = top; - this.left = left; - } - } - - const menuItems = [ - new MenuItem(`Pet ${birdBirb()}`, pet), - new MenuItem("Field Guide", insertFieldGuide), - new MenuItem("Sticky Note", newStickyNote), - new MenuItem(`Hide ${birdBirb()}`, hideBirb), - new DebugMenuItem("Freeze/Unfreeze", () => { - frozen = !frozen; - }), - new DebugMenuItem("Reset Data", resetSaveData), - new DebugMenuItem("Unlock All", () => { - for (let type in species) { - unlockBird(type); - } - }), - new DebugMenuItem("Disable Debug", () => { - debugMode = false; - }), - new Separator(), - new MenuItem("Settings", () => switchMenuItems(settingsItems), false), - ]; - - const settingsItems = [ - new MenuItem("Go Back", () => switchMenuItems(menuItems), false), - new Separator(), - new MenuItem("Toggle Birb Mode", () => { - userSettings.birbMode = !userSettings.birbMode; - save(); - insertModal(`${birdBirb()} Mode`, `Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"${userSettings.birbMode ? "\n\nWelcome back to 2012" : ""}`); - }) - ]; - - const styleElement = document.createElement("style"); - const canvas = document.createElement("canvas"); - - /** @type {CanvasRenderingContext2D} */ - // @ts-expect-error - const ctx = canvas.getContext("2d"); - - const States = { - IDLE: "idle", - HOP: "hop", - FLYING: "flying", - }; - - let stateStart = Date.now(); - let currentState = States.IDLE; - let animStart = Date.now(); - let currentAnimation = Animations.BOB; - let direction = Directions.RIGHT; - let ticks = 0; - // Bird's current position - let birdY = 0; - let birdX = 40; - // Bird's starting position (when flying) - let startX = 0; - let startY = 0; - // Bird's target position (when flying) - let targetX = 0; - let targetY = 0; - /** @type {HTMLElement|null} */ - let focusedElement = null; - let focusedBounds = { left: 0, right: 0, top: 0 }; - let lastActionTimestamp = Date.now(); - /** @type {number[]} */ - let petStack = []; - let currentSpecies = DEFAULT_BIRD; - let unlockedSpecies = [DEFAULT_BIRD]; - let visible = true; - let lastPetTimestamp = 0; - /** @type {StickyNote[]} */ - let stickyNotes = []; - - /** - * @returns {boolean} Whether the script is running in a userscript extension context - */ - function isUserScript() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - function isTestEnvironment() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - function load() { - /** @type {Record} */ - let saveData = {}; - - if (isUserScript()) { - log("Loading save data from UserScript storage"); - // @ts-expect-error - saveData = GM_getValue("birbSaveData", {}) ?? {}; - } else if (isTestEnvironment()) { - log("Test environment detected, loading save data from localStorage"); - saveData = JSON.parse(localStorage.getItem("birbSaveData") ?? "{}"); - } else { - log("Not a UserScript"); - } - - debug("Loaded data: " + JSON.stringify(saveData)); - - if (!saveData.settings) { - log("No user settings found in save data, starting fresh"); - } - - userSettings = saveData.settings ?? {}; - unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; - currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; - stickyNotes = []; - - if (saveData.stickyNotes) { - for (let note of saveData.stickyNotes) { - if (note.id) { - stickyNotes.push(new StickyNote(note.id, note.site, note.content, note.top, note.left)); - } - } - } - - log(stickyNotes.length + " sticky notes loaded"); - switchSpecies(currentSpecies); - } - - function save() { - /** @type {BirbSaveData} */ - const saveData = { - unlockedSpecies, - currentSpecies, - settings: userSettings - }; - - if (stickyNotes.length > 0) { - saveData.stickyNotes = stickyNotes.map(note => ({ - id: note.id, - site: note.site, - content: note.content, - top: note.top, - left: note.left - })); - } - - if (isUserScript()) { - log("Saving data to UserScript storage"); - // @ts-expect-error - GM_setValue("birbSaveData", saveData); - } else if (isTestEnvironment()) { - log("Test environment detected, saving data to localStorage"); - localStorage.setItem("birbSaveData", JSON.stringify(saveData)); - } else { - log("Not a UserScript"); - } - } - - function resetSaveData() { - if (isUserScript()) { - log("Resetting save data in UserScript storage"); - // @ts-expect-error - GM_deleteValue("birbSaveData"); - } else if (isTestEnvironment()) { - log("Test environment detected, resetting save data in localStorage"); - localStorage.removeItem("birbSaveData"); - } else { - log("Not a UserScript"); - } - load(); - } - - /** - * Get the user settings merged with default settings - * @returns {Settings} The merged settings - */ - function settings() { - return { ...DEFAULT_SETTINGS, ...userSettings }; - } - - /** - * Bird or birb, you decide - */ - function birdBirb() { - return settings().birbMode ? "Birb" : "Bird"; - } - - function init() { - if (window !== window.top) { - // Skip installation if within an iframe - log("In iframe, skipping Birb script initialization"); - return; - } - log("Sprite sheets loaded successfully, initializing bird..."); - - // Preload font - const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf"; - const fontLink = document.createElement("link"); - fontLink.rel = "stylesheet"; - fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`; - document.head.appendChild(fontLink); - - // Add stylesheet font-face - const fontFace = ` + // Add stylesheet font-face + const fontFace = ` @font-face { font-family: 'Monocraft'; src: url(${MONOCRAFT_SRC}) format('opentype'); @@ -1183,309 +1188,309 @@ Promise.all([ font-style: normal; } `; - const fontStyle = document.createElement("style"); - fontStyle.innerHTML = fontFace; - document.head.appendChild(fontStyle); + const fontStyle = document.createElement("style"); + fontStyle.innerHTML = fontFace; + document.head.appendChild(fontStyle); - load(); + load(); - styleElement.innerHTML = STYLESHEET; - document.head.appendChild(styleElement); + styleElement.innerHTML = STYLESHEET; + document.head.appendChild(styleElement); - canvas.id = "birb"; - canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE; - canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; - document.body.appendChild(canvas); + canvas.id = "birb"; + canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE; + canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; + document.body.appendChild(canvas); - window.addEventListener("scroll", () => { - lastActionTimestamp = Date.now(); - }); + window.addEventListener("scroll", () => { + lastActionTimestamp = Date.now(); + }); - onClick(document, (e) => { - lastActionTimestamp = Date.now(); - if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { - removeMenu(); + onClick(document, (e) => { + lastActionTimestamp = Date.now(); + if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { + removeMenu(); + } + }); + + onClick(canvas, () => { + if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { + // Currently being pet, don't open menu + return; + } + insertMenu(); + }); + + canvas.addEventListener("mouseover", () => { + lastActionTimestamp = Date.now(); + if (currentState === States.IDLE) { + petStack.push(Date.now()); + if (petStack.length > 10) { + petStack.shift(); + } + const pets = petStack.filter((time) => Date.now() - time < 1000).length; + if (pets >= 3) { + pet(); + // Clear the stack + petStack = []; + } + } + }); + + canvas.addEventListener("touchmove", (e) => { + pet(); + }); + + drawStickyNotes(); + + let lastUrl = (window.location.href ?? "").split("?")[0]; + setInterval(() => { + const currentUrl = (window.location.href ?? "").split("?")[0]; + if (currentUrl !== lastUrl) { + log("URL changed, updating sticky notes"); + lastUrl = currentUrl; + drawStickyNotes(); + } + }, URL_CHECK_INTERVAL); + + setInterval(update, UPDATE_INTERVAL); + } + + function update() { + ticks++; + + // Hide bird if the browser is fullscreen + if (document.fullscreenElement) { + hideBirb(); + // Won't be restored on fullscreen exit } - }); - onClick(canvas, () => { - if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { - // Currently being pet, don't open menu + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { + hop(); + } else if (Date.now() - lastActionTimestamp > AFK_TIME) { + // Idle for a while, do something + if (focusedElement === null) { + // Fly to an element + focusOnElement(); + lastActionTimestamp = Date.now(); + } else if (Math.random() < FOCUS_SWITCH_CHANCE) { + // Fly to another element if idle for a longer while + focusOnElement(); + lastActionTimestamp = Date.now(); + } + } + } else if (currentState === States.HOP) { + if (updateParabolicPath(HOP_SPEED)) { + setState(States.IDLE); + } + } + + // Double the chance of a feather if recently pet + const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1; + if (visible && Math.random() < FEATHER_CHANCE * petMod) { + lastPetTimestamp = 0; + activateFeather(); + } + updateFeather(); + } + + function draw() { + requestAnimationFrame(draw); + + if (!visible) { return; } - insertMenu(); - }); - canvas.addEventListener("mouseover", () => { - lastActionTimestamp = Date.now(); + updateFocusedElementBounds(); + + // Update the bird's position if (currentState === States.IDLE) { - petStack.push(Date.now()); - if (petStack.length > 10) { - petStack.shift(); + if (focusedElement && !isWithinHorizontalBounds()) { + focusOnGround(); } - const pets = petStack.filter((time) => Date.now() - time < 1000).length; - if (pets >= 3) { - pet(); - // Clear the stack - petStack = []; + birdY = getFocusedY(); + } else if (currentState === States.FLYING) { + // Fly to target location (even if in the air) + if (updateParabolicPath(FLY_SPEED)) { + setState(States.IDLE); } } - }); - canvas.addEventListener("touchmove", (e) => { - pet(); - }); - - drawStickyNotes(); - - let lastUrl = (window.location.href ?? "").split("?")[0]; - setInterval(() => { - const currentUrl = (window.location.href ?? "").split("?")[0]; - if (currentUrl !== lastUrl) { - log("URL changed, updating sticky notes"); - lastUrl = currentUrl; - drawStickyNotes(); - } - }, URL_CHECK_INTERVAL); - - setInterval(update, UPDATE_INTERVAL); - } - - function update() { - ticks++; - - // Hide bird if the browser is fullscreen - if (document.fullscreenElement) { - hideBirb(); - // Won't be restored on fullscreen exit - } - - if (currentState === States.IDLE && !frozen && !isMenuOpen()) { - if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { - hop(); - } else if (Date.now() - lastActionTimestamp > AFK_TIME) { - // Idle for a while, do something - if (focusedElement === null) { - // Fly to an element - focusOnElement(); - lastActionTimestamp = Date.now(); - } else if (Math.random() < FOCUS_SWITCH_CHANCE) { - // Fly to another element if idle for a longer while - focusOnElement(); - lastActionTimestamp = Date.now(); - } - } - } else if (currentState === States.HOP) { - if (updateParabolicPath(HOP_SPEED)) { - setState(States.IDLE); - } - } - - // Double the chance of a feather if recently pet - const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1; - if (visible && Math.random() < FEATHER_CHANCE * petMod) { - lastPetTimestamp = 0; - activateFeather(); - } - updateFeather(); - } - - function draw() { - requestAnimationFrame(draw); - - if (!visible) { - return; - } - - updateFocusedElementBounds(); - - // Update the bird's position - if (currentState === States.IDLE) { - if (focusedElement && !isWithinHorizontalBounds()) { + const oldTargetY = targetY; + targetY = getFocusedY(); + // Adjust startY to account for scrolling + startY += targetY - oldTargetY; + if (targetY < 0 || targetY > window.innerHeight) { + // Fly to ground if the focused element moves out of bounds focusOnGround(); } - birdY = getFocusedY(); - } else if (currentState === States.FLYING) { - // Fly to target location (even if in the air) - if (updateParabolicPath(FLY_SPEED)) { - setState(States.IDLE); + + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) { + setAnimation(Animations.STILL); } + + // Update HTML element position + setX(birdX); + setY(birdY); } - const oldTargetY = targetY; - targetY = getFocusedY(); - // Adjust startY to account for scrolling - startY += targetY - oldTargetY; - if (targetY < 0 || targetY > window.innerHeight) { - // Fly to ground if the focused element moves out of bounds - focusOnGround(); + function newStickyNote() { + const id = Date.now().toString(); + const site = window.location.href; + const stickyNote = new StickyNote(id, site, ""); + const element = renderStickyNote(stickyNote); + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; + stickyNote.top = parseInt(element.style.top, 10); + stickyNote.left = parseInt(element.style.left, 10); + stickyNotes.push(stickyNote); + save(); } - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) { - setAnimation(Animations.STILL); - } - - // Update HTML element position - setX(birdX); - setY(birdY); - } - - function newStickyNote() { - const id = Date.now().toString(); - const site = window.location.href; - const stickyNote = new StickyNote(id, site, ""); - const element = renderStickyNote(stickyNote); - element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; - element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; - stickyNote.top = parseInt(element.style.top, 10); - stickyNote.left = parseInt(element.style.left, 10); - stickyNotes.push(stickyNote); - save(); - } - - /** - * @param {StickyNote} stickyNote - * @returns {HTMLElement} - */ - function renderStickyNote(stickyNote) { - let html = ` + /** + * @param {StickyNote} stickyNote + * @returns {HTMLElement} + */ + function renderStickyNote(stickyNote) { + let html = `
Sticky Note
x
-
` - const noteElement = makeElement("birb-window"); - noteElement.classList.add("birb-sticky-note"); - noteElement.innerHTML = html; + `; + const noteElement = makeElement("birb-window"); + noteElement.classList.add("birb-sticky-note"); + noteElement.innerHTML = html; - noteElement.style.top = `${stickyNote.top}px`; - noteElement.style.left = `${stickyNote.left}px`; - document.body.appendChild(noteElement); + noteElement.style.top = `${stickyNote.top}px`; + noteElement.style.left = `${stickyNote.left}px`; + document.body.appendChild(noteElement); - makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { - stickyNote.top = top; - stickyNote.left = left; - save(); - }); - - const closeButton = noteElement.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - if (confirm("Are you sure you want to delete this sticky note?")) { - deleteStickyNote(stickyNote); - noteElement.remove(); - } - }, closeButton); - } - - const textarea = noteElement.querySelector(".birb-sticky-note-input"); - if (textarea && textarea instanceof HTMLTextAreaElement) { - /** @type {NodeJS.Timeout | undefined} */ - let saveTimeout; - // Save after debounce - textarea.addEventListener("input", () => { - stickyNote.content = textarea.value; - if (saveTimeout) { - clearTimeout(saveTimeout); - } - saveTimeout = setTimeout(() => { - save(); - }, 250); + makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + save(); }); + + const closeButton = noteElement.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + if (confirm("Are you sure you want to delete this sticky note?")) { + deleteStickyNote(stickyNote); + noteElement.remove(); + } + }, closeButton); + } + + const textarea = noteElement.querySelector(".birb-sticky-note-input"); + if (textarea && textarea instanceof HTMLTextAreaElement) { + /** @type {NodeJS.Timeout | undefined} */ + let saveTimeout; + // Save after debounce + textarea.addEventListener("input", () => { + stickyNote.content = textarea.value; + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + save(); + }, 250); + }); + } + + // On window resize + window.addEventListener("resize", () => { + const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; + const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; + noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + }); + + return noteElement; } - // On window resize - window.addEventListener("resize", () => { - const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; - const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; - noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; - }); - - return noteElement; - } - - /** - * @param {StickyNote} stickyNote - */ - function deleteStickyNote(stickyNote) { - stickyNotes = stickyNotes.filter(note => note.id !== stickyNote.id); - save(); - } - - /** - * @param {StickyNote} stickyNote - * @returns {boolean} Whether the given sticky note is applicable to the current site/page - */ - function isStickyNoteApplicable(stickyNote) { - const stickyNoteUrl = stickyNote.site; - const currentUrl = window.location.href; - const stickyNoteWebsite = stickyNoteUrl.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - debug("Comparing " + stickyNoteUrl + " with " + currentUrl); - - if (stickyNoteWebsite !== currentWebsite) { - return false; + /** + * @param {StickyNote} stickyNote + */ + function deleteStickyNote(stickyNote) { + stickyNotes = stickyNotes.filter(note => note.id !== stickyNote.id); + save(); } - const stickyNoteParams = parseUrlParams(stickyNoteUrl); - const currentParams = parseUrlParams(currentUrl); + /** + * @param {StickyNote} stickyNote + * @returns {boolean} Whether the given sticky note is applicable to the current site/page + */ + function isStickyNoteApplicable(stickyNote) { + const stickyNoteUrl = stickyNote.site; + const currentUrl = window.location.href; + const stickyNoteWebsite = stickyNoteUrl.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; - debug("Comparing params: ", stickyNoteParams, currentParams); + debug("Comparing " + stickyNoteUrl + " with " + currentUrl); - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) { + if (stickyNoteWebsite !== currentWebsite) { return false; } - } - return true; - } - function drawStickyNotes() { - // Remove all existing sticky notes - const existingNotes = document.querySelectorAll(".birb-sticky-note"); - existingNotes.forEach(note => note.remove()); - // Render all sticky notes - for (let stickyNote of stickyNotes) { - if (isStickyNoteApplicable(stickyNote)) { - renderStickyNote(stickyNote); + const stickyNoteParams = parseUrlParams(stickyNoteUrl); + const currentParams = parseUrlParams(currentUrl); + + debug("Comparing params: ", stickyNoteParams, currentParams); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) { + return false; + } + } + return true; + } + + function drawStickyNotes() { + // Remove all existing sticky notes + const existingNotes = document.querySelectorAll(".birb-sticky-note"); + existingNotes.forEach(note => note.remove()); + // Render all sticky notes + for (let stickyNote of stickyNotes) { + if (isStickyNoteApplicable(stickyNote)) { + renderStickyNote(stickyNote); + } } } - } - /** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ - function makeElement(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - if (textContent) { - element.textContent = textContent; + /** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ + function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } + return element; } - if (id) { - element.id = id; - } - return element; - } - /** - * Create a window element with header and content - * @param {string} id - * @param {string} title - * @param {string} contentHtml - * @param {() => void} [onClose] - * @returns {HTMLElement} - */ - function createWindow(id, title, contentHtml, onClose) { - const window = makeElement("birb-window", undefined, id); - window.innerHTML = ` + /** + * Create a window element with header and content + * @param {string} id + * @param {string} title + * @param {string} contentHtml + * @param {() => void} [onClose] + * @returns {HTMLElement} + */ + function createWindow(id, title, contentHtml, onClose) { + const window = makeElement("birb-window", undefined, id); + window.innerHTML = `
${title}
x
@@ -1495,143 +1500,123 @@ Promise.all([
`; - document.body.appendChild(window); - makeDraggable(window.querySelector(".birb-window-header")); + document.body.appendChild(window); + makeDraggable(window.querySelector(".birb-window-header")); - const closeButton = window.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - if (onClose) { - onClose(); - } - window.remove(); - }, closeButton); - } - - 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; - } - const speciesToUnlock = Object.keys(species).filter((species) => !unlockedSpecies.includes(species)); - if (speciesToUnlock.length === 0) { - // No more species to unlock - return; - } - const birdType = speciesToUnlock[Math.floor(Math.random() * speciesToUnlock.length)]; - insertFeather(birdType); - } - - /** - * @param {string} birdType - */ - function insertFeather(birdType) { - let type = species[birdType]; - const featherCanvas = document.createElement("canvas"); - featherCanvas.id = FEATHER_ID; - featherCanvas.classList.add("birb-decoration"); - featherCanvas.width = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - featherCanvas.height = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - const x = featherCanvas.width * 2 + Math.random() * (window.innerWidth - featherCanvas.width * 4); - featherCanvas.style.marginLeft = `${x}px`; - featherCanvas.style.top = `${-featherCanvas.height}px`; - const featherCtx = featherCanvas.getContext("2d"); - if (!featherCtx) { - return; - } - FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), type); - document.body.appendChild(featherCanvas); - onClick(featherCanvas, () => { - unlockBird(birdType); - removeFeather(); - if (document.querySelector("#" + FIELD_GUIDE_ID)) { - removeFieldGuide(); - insertFieldGuide(); + const closeButton = window.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + window.remove(); + }, closeButton); } - }); - } - function removeFeather() { - const feather = document.querySelector("#" + FEATHER_ID); - if (feather) { - feather.remove(); - } - } - - /** - * @param {string} birdType - */ - function unlockBird(birdType) { - if (!unlockedSpecies.includes(birdType)) { - unlockedSpecies.push(birdType); - insertModal("New Bird Unlocked!", `You've found a ${species[birdType].name} feather! Use the Field Guide to switch your bird's species.`); - } - save(); - } - - function updateFeather() { - const feather = document.querySelector("#birb-feather"); - if (!feather || !(feather instanceof HTMLElement)) { - return; - } - const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED; - feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`; - if (y < window.innerHeight - feather.offsetHeight) { - feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; - } - } - - /** - * @param {HTMLElement} element - */ - function centerElement(element) { - element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; - element.style.top = `${window.innerHeight / 2 - element.offsetHeight / 2}px`; - } - - /** - * @param {string} title - * @param {string} message - */ - function insertModal(title, message) { - if (document.querySelector("#" + FIELD_GUIDE_ID)) { - return; + return window; } - const modal = createWindow("birb-modal", title, ` + function activateFeather() { + if (document.querySelector("#" + FEATHER_ID)) { + return; + } + const speciesToUnlock = Object.keys(species).filter((species) => !unlockedSpecies.includes(species)); + if (speciesToUnlock.length === 0) { + // No more species to unlock + return; + } + const birdType = speciesToUnlock[Math.floor(Math.random() * speciesToUnlock.length)]; + insertFeather(birdType); + } + + /** + * @param {string} birdType + */ + function insertFeather(birdType) { + let type = species[birdType]; + const featherCanvas = document.createElement("canvas"); + featherCanvas.id = FEATHER_ID; + featherCanvas.classList.add("birb-decoration"); + featherCanvas.width = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + featherCanvas.height = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + const x = featherCanvas.width * 2 + Math.random() * (window.innerWidth - featherCanvas.width * 4); + featherCanvas.style.marginLeft = `${x}px`; + featherCanvas.style.top = `${-featherCanvas.height}px`; + const featherCtx = featherCanvas.getContext("2d"); + if (!featherCtx) { + return; + } + FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), type); + document.body.appendChild(featherCanvas); + onClick(featherCanvas, () => { + unlockBird(birdType); + removeFeather(); + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + removeFieldGuide(); + insertFieldGuide(); + } + }); + } + + function removeFeather() { + const feather = document.querySelector("#" + FEATHER_ID); + if (feather) { + feather.remove(); + } + } + + /** + * @param {string} birdType + */ + function unlockBird(birdType) { + if (!unlockedSpecies.includes(birdType)) { + unlockedSpecies.push(birdType); + insertModal("New Bird Unlocked!", `You've found a ${species[birdType].name} feather! Use the Field Guide to switch your bird's species.`); + } + save(); + } + + function updateFeather() { + const feather = document.querySelector("#birb-feather"); + if (!feather || !(feather instanceof HTMLElement)) { + return; + } + const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED; + feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`; + if (y < window.innerHeight - feather.offsetHeight) { + feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; + } + } + + /** + * @param {HTMLElement} element + */ + function centerElement(element) { + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${window.innerHeight / 2 - element.offsetHeight / 2}px`; + } + + /** + * @param {string} title + * @param {string} message + */ + function insertModal(title, message) { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + + const modal = createWindow("birb-modal", title, `
${message}
`); - modal.style.width = "270px"; - centerElement(modal); - } - - function insertFieldGuide() { - if (document.querySelector("#" + FIELD_GUIDE_ID)) { - return; + modal.style.width = "270px"; + centerElement(modal); } - let html = ` + + function insertFieldGuide() { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + let html = `
Field Guide
x
@@ -1639,601 +1624,595 @@ Promise.all([
-
` - const fieldGuide = makeElement("birb-window", undefined, FIELD_GUIDE_ID); - fieldGuide.innerHTML = html; - document.body.appendChild(fieldGuide); - makeDraggable(fieldGuide.querySelector(".birb-window-header")); +
`; + const fieldGuide = makeElement("birb-window", undefined, FIELD_GUIDE_ID); + fieldGuide.innerHTML = html; + document.body.appendChild(fieldGuide); + makeDraggable(fieldGuide.querySelector(".birb-window-header")); - const closeButton = fieldGuide.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - fieldGuide.remove(); - }, closeButton); - } - - const content = fieldGuide.querySelector(".birb-grid-content"); - if (!content) { - return; - } - content.innerHTML = ""; - - const generateDescription = (/** @type {string} */ speciesId) => { - const type = species[speciesId]; - const unlocked = unlockedSpecies.includes(speciesId); - return "" + type.name + "
" + (!unlocked ? "Not yet unlocked" : type.description); - }; - - const description = fieldGuide.querySelector(".birb-field-guide-description"); - if (!description) { - return; - } - description.innerHTML = generateDescription(currentSpecies); - for (const [id, type] of Object.entries(species)) { - const unlocked = unlockedSpecies.includes(id); - const speciesElement = makeElement("birb-grid-item"); - if (id === currentSpecies) { - speciesElement.classList.add("birb-grid-item-selected"); + const closeButton = fieldGuide.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + fieldGuide.remove(); + }, closeButton); } - const speciesCanvas = document.createElement("canvas"); - speciesCanvas.width = SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - speciesCanvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; - const speciesCtx = speciesCanvas.getContext("2d"); - if (!speciesCtx) { + + const content = fieldGuide.querySelector(".birb-grid-content"); + if (!content) { return; } - birbFrames.base.draw(speciesCtx, Directions.RIGHT, type); - speciesElement.appendChild(speciesCanvas); - content.appendChild(speciesElement); - if (unlocked) { - onClick(speciesElement, () => { - switchSpecies(id); - document.querySelectorAll(".birb-grid-item").forEach((element) => { - element.classList.remove("birb-grid-item-selected"); - }); + content.innerHTML = ""; + + const generateDescription = (/** @type {string} */ speciesId) => { + const type = species[speciesId]; + const unlocked = unlockedSpecies.includes(speciesId); + return "" + type.name + "
" + (!unlocked ? "Not yet unlocked" : type.description); + }; + + const description = fieldGuide.querySelector(".birb-field-guide-description"); + if (!description) { + return; + } + description.innerHTML = generateDescription(currentSpecies); + for (const [id, type] of Object.entries(species)) { + const unlocked = unlockedSpecies.includes(id); + const speciesElement = makeElement("birb-grid-item"); + if (id === currentSpecies) { speciesElement.classList.add("birb-grid-item-selected"); + } + const speciesCanvas = document.createElement("canvas"); + speciesCanvas.width = SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + speciesCanvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; + const speciesCtx = speciesCanvas.getContext("2d"); + if (!speciesCtx) { + return; + } + birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); + speciesElement.appendChild(speciesCanvas); + content.appendChild(speciesElement); + if (unlocked) { + onClick(speciesElement, () => { + switchSpecies(id); + document.querySelectorAll(".birb-grid-item").forEach((element) => { + element.classList.remove("birb-grid-item-selected"); + }); + speciesElement.classList.add("birb-grid-item-selected"); + }); + } else { + speciesElement.classList.add("birb-grid-item-locked"); + } + speciesElement.addEventListener("mouseover", () => { + log("mouseover"); + description.innerHTML = generateDescription(id); + }); + speciesElement.addEventListener("mouseout", () => { + description.innerHTML = generateDescription(currentSpecies); }); - } else { - speciesElement.classList.add("birb-grid-item-locked"); } - speciesElement.addEventListener("mouseover", () => { - log("mouseover"); - description.innerHTML = generateDescription(id); - }); - speciesElement.addEventListener("mouseout", () => { - description.innerHTML = generateDescription(currentSpecies); - }); + centerElement(fieldGuide); } - centerElement(fieldGuide); - } - /** - * @param {() => void} func - * @param {Element} [closeButton] - */ - function makeClosable(func, closeButton) { - if (closeButton) { - onClick(closeButton, func); + /** + * @param {() => void} func + * @param {Element} [closeButton] + */ + function makeClosable(func, closeButton) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); } - document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { + + function removeFieldGuide() { + const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); + if (fieldGuide) { + fieldGuide.remove(); + } + } + + /** + * @param {string} type + */ + function switchSpecies(type) { + currentSpecies = type; + // Update CSS variable --birb-highlight to be wing color + document.documentElement.style.setProperty("--birb-highlight", species[type].colors[THEME_HIGHLIGHT]); + save(); + } + + /** + * Add the menu to the page if it doesn't already exist + */ + function insertMenu() { + if (document.querySelector("#" + MENU_ID)) { return; } - if (e.key === "Escape") { - func(); + let menu = makeElement("birb-window", undefined, MENU_ID); + let header = makeElement("birb-window-header"); + header.innerHTML = `
${birdBirb().toLowerCase()}OS
`; + let content = makeElement("birb-window-content"); + for (const item of menuItems) { + if (!item.isDebug || debugMode) { + content.appendChild(makeMenuItem(item)); + } } - }); - } + menu.appendChild(header); + menu.appendChild(content); + document.body.appendChild(menu); + makeDraggable(document.querySelector(".birb-window-header")); - function removeFieldGuide() { - const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); - if (fieldGuide) { - fieldGuide.remove(); - } - } - - function isSafari() { - return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - } - - /** - * @param {string} type - */ - function switchSpecies(type) { - currentSpecies = type; - // Update CSS variable --birb-highlight to be wing color - document.documentElement.style.setProperty("--birb-highlight", species[type].colors[THEME_HIGHLIGHT]); - save(); - } - - /** - * Add the menu to the page if it doesn't already exist - */ - function insertMenu() { - if (document.querySelector("#" + MENU_ID)) { - return; - } - let menu = makeElement("birb-window", undefined, MENU_ID); - let header = makeElement("birb-window-header"); - header.innerHTML = `
${birdBirb().toLowerCase()}OS
`; - let content = makeElement("birb-window-content"); - for (const item of menuItems) { - if (!item.isDebug || debugMode) { - content.appendChild(makeMenuItem(item)); - } - } - menu.appendChild(header); - menu.appendChild(content); - document.body.appendChild(menu); - makeDraggable(document.querySelector(".birb-window-header")); - - let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); - onClick(menuExit, () => { - removeMenu(); - }); - document.body.appendChild(menuExit); - makeClosable(removeMenu); - - updateMenuLocation(menu); - } - - /** - * Update the menu's location based on the bird's position - * @param {HTMLElement} menu - */ - function updateMenuLocation(menu) { - let x = birdX; - let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; - const offset = 20; - if (x < window.innerWidth / 2) { - // Left side - x += offset; - } else { - // Right side - x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; - } - if (y > window.innerHeight / 2) { - // Top side - y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; - } else { - // Bottom side - y += offset; - } - menu.style.left = `${x}px`; - menu.style.top = `${y}px`; - } - - /** - * @param {MenuItem[]} menuItems - */ - function switchMenuItems(menuItems) { - const menu = document.querySelector("#" + MENU_ID); - if (!menu || !(menu instanceof HTMLElement)) { - return; - } - const content = menu.querySelector(".birb-window-content"); - if (!content) { - error("Content not found"); - return; - } - content.innerHTML = ""; - for (const item of menuItems) { - if (!item.isDebug || debugMode) { - content.appendChild(makeMenuItem(item)); - } - } - updateMenuLocation(menu); - } - - /** - * @param {MenuItem} item - * @returns {HTMLElement} - */ - function makeMenuItem(item) { - if (item instanceof Separator) { - return makeElement("birb-window-separator"); - } - let menuItem = makeElement("birb-menu-item", item.text); - onClick(menuItem, () => { - if (item.removeMenu) { + let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); + onClick(menuExit, () => { removeMenu(); - } - item.action(); - }); - return menuItem; - } + }); + document.body.appendChild(menuExit); + makeClosable(removeMenu); - /** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ - function onClick(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); - } - - /** - * Remove the menu from the page - */ - function removeMenu() { - const menu = document.querySelector("#" + MENU_ID); - if (menu) { - menu.remove(); - } - const exitMenu = document.querySelector("#" + MENU_EXIT_ID); - if (exitMenu) { - exitMenu.remove(); - } - } - - /** - * @returns {boolean} Whether the menu element is on the page - */ - function isMenuOpen() { - return document.querySelector("#" + MENU_ID) !== null; - } - - /** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ - function makeDraggable(element, parent = true, callback = () => { }) { - if (!element) { - return; + updateMenuLocation(menu); } - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - error("Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); - } - - /** - * @param {string[][]} array - * @param {number} sprite - * @param {number} [width] - * @returns {string[][]} - */ - function getLayer(array, sprite, width = SPRITE_WIDTH) { - // From an array of a horizontal sprite sheet, get the layer for a specific sprite - const layer = []; - for (let y = 0; y < width; y++) { - layer.push(array[y].slice(sprite * width, (sprite + 1) * width)); - } - return layer; - } - - /** - * Update the birds location from the start to the target location on a parabolic path - * @param {number} speed The speed of the bird along the path - * @param {number} [intensity] The intensity of the parabolic path - * @returns {boolean} Whether the bird has reached the target location - */ - function updateParabolicPath(speed, intensity = 2.5) { - const dx = targetX - startX; - const dy = targetY - startY; - const distance = Math.sqrt(dx * dx + dy * dy); - const time = Date.now() - stateStart; - if (distance > Math.max(window.innerWidth, window.innerHeight) / 2) { - speed *= 1.3; - } - const amount = Math.min(1, time / (distance / speed)); - const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity); - birdX = x; - birdY = y; - const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1; - if (complete) { - birdX = targetX; - birdY = targetY; - } else { - direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT; - } - return complete; - } - - function getFocusedElementRandomX() { - return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; - } - - function isWithinHorizontalBounds() { - return birdX >= focusedBounds.left && birdX <= focusedBounds.right; - } - - function getFocusedY() { - return getFullWindowHeight() - focusedBounds.top; - } - - /** - * @returns The render-safe height of the inner browser window - */ - function getSafeWindowHeight() { - // Necessary because iOS 26 Safari is terrible and won't render - // fixed elements behind the address bar - return window.innerHeight; - } - - /** - * @returns The true height of the inner browser window - */ - function getFullWindowHeight() { - return document.documentElement.clientHeight; - } - - function focusOnGround() { - console.log("Focusing on ground"); - focusedElement = null; - focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; - flyTo(Math.random() * window.innerWidth, 0); - } - - function focusOnElement() { - if (frozen) { - return; - } - const elements = document.querySelectorAll("img, video"); - const inWindow = Array.from(elements).filter((img) => { - const rect = img.getBoundingClientRect(); - return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight; - }); - /** @type {HTMLElement[]} */ - // @ts-expect-error - const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - if (largeElements.length === 0) { - return; - } - const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)]; - focusedElement = randomElement; - log("Focusing on element: ", focusedElement); - updateFocusedElementBounds(); - flyTo(getFocusedElementRandomX(), getFocusedY()); - } - - function updateFocusedElementBounds() { - if (focusedElement === null) { - // Update ground location to bottom of window - focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; - return; - } - const { left, right, top } = focusedElement.getBoundingClientRect(); - focusedBounds = { left, right, top }; - } - - function getCanvasWidth() { - return canvas.width * BIRB_CSS_SCALE - } - - function getCanvasHeight() { - return canvas.height * BIRB_CSS_SCALE - } - - function hop() { - if (frozen) { - return; - } - if (currentState === States.IDLE) { - setState(States.HOP); - setAnimation(Animations.FLYING); - if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { - targetX = birdX - HOP_DISTANCE; + /** + * Update the menu's location based on the bird's position + * @param {HTMLElement} menu + */ + function updateMenuLocation(menu) { + let x = birdX; + let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; + const offset = 20; + if (x < window.innerWidth / 2) { + // Left side + x += offset; } else { - targetX = birdX + HOP_DISTANCE; + // Right side + x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; } - targetY = getFocusedY(); + if (y > window.innerHeight / 2) { + // Top side + y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; + } else { + // Bottom side + y += offset; + } + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + } + + /** + * @param {MenuItem[]} menuItems + */ + function switchMenuItems(menuItems) { + const menu = document.querySelector("#" + MENU_ID); + if (!menu || !(menu instanceof HTMLElement)) { + return; + } + const content = menu.querySelector(".birb-window-content"); + if (!content) { + error("Content not found"); + return; + } + content.innerHTML = ""; + for (const item of menuItems) { + if (!item.isDebug || debugMode) { + content.appendChild(makeMenuItem(item)); + } + } + updateMenuLocation(menu); + } + + /** + * @param {MenuItem} item + * @returns {HTMLElement} + */ + function makeMenuItem(item) { + if (item instanceof Separator) { + return makeElement("birb-window-separator"); + } + let menuItem = makeElement("birb-menu-item", item.text); + onClick(menuItem, () => { + if (item.removeMenu) { + removeMenu(); + } + item.action(); + }); + return menuItem; + } + + /** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ + function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); + } + + /** + * Remove the menu from the page + */ + function removeMenu() { + const menu = document.querySelector("#" + MENU_ID); + if (menu) { + menu.remove(); + } + const exitMenu = document.querySelector("#" + MENU_EXIT_ID); + if (exitMenu) { + exitMenu.remove(); + } + } + + /** + * @returns {boolean} Whether the menu element is on the page + */ + function isMenuOpen() { + return document.querySelector("#" + MENU_ID) !== null; + } + + /** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ + function makeDraggable(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + error("Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); + } + + /** + * @param {string[][]} array + * @param {number} sprite + * @param {number} [width] + * @returns {string[][]} + */ + function getLayer(array, sprite, width = SPRITE_WIDTH) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < width; y++) { + layer.push(array[y].slice(sprite * width, (sprite + 1) * width)); + } + return layer; + } + + /** + * Update the birds location from the start to the target location on a parabolic path + * @param {number} speed The speed of the bird along the path + * @param {number} [intensity] The intensity of the parabolic path + * @returns {boolean} Whether the bird has reached the target location + */ + function updateParabolicPath(speed, intensity = 2.5) { + const dx = targetX - startX; + const dy = targetY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const time = Date.now() - stateStart; + if (distance > Math.max(window.innerWidth, window.innerHeight) / 2) { + speed *= 1.3; + } + const amount = Math.min(1, time / (distance / speed)); + const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity); + birdX = x; + birdY = y; + const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1; + if (complete) { + birdX = targetX; + birdY = targetY; + } else { + direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT; + } + return complete; + } + + function getFocusedElementRandomX() { + return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; + } + + function isWithinHorizontalBounds() { + return birdX >= focusedBounds.left && birdX <= focusedBounds.right; + } + + function getFocusedY() { + return getFullWindowHeight() - focusedBounds.top; + } + + /** + * @returns The render-safe height of the inner browser window + */ + function getSafeWindowHeight() { + // Necessary because iOS 26 Safari is terrible and won't render + // fixed elements behind the address bar + return window.innerHeight; + } + + /** + * @returns The true height of the inner browser window + */ + function getFullWindowHeight() { + return document.documentElement.clientHeight; + } + + function focusOnGround() { + console.log("Focusing on ground"); + focusedElement = null; + focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; + flyTo(Math.random() * window.innerWidth, 0); + } + + function focusOnElement() { + if (frozen) { + return; + } + const elements = document.querySelectorAll("img, video"); + const inWindow = Array.from(elements).filter((img) => { + const rect = img.getBoundingClientRect(); + return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight; + }); + /** @type {HTMLElement[]} */ + // @ts-expect-error + const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); + if (largeElements.length === 0) { + return; + } + const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)]; + focusedElement = randomElement; + log("Focusing on element: ", focusedElement); + updateFocusedElementBounds(); + flyTo(getFocusedElementRandomX(), getFocusedY()); + } + + function updateFocusedElementBounds() { + if (focusedElement === null) { + // Update ground location to bottom of window + focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; + return; + } + const { left, right, top } = focusedElement.getBoundingClientRect(); + focusedBounds = { left, right, top }; + } + + function getCanvasWidth() { + return canvas.width * BIRB_CSS_SCALE + } + + function hop() { + if (frozen) { + return; + } + if (currentState === States.IDLE) { + setState(States.HOP); + setAnimation(Animations.FLYING); + if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { + targetX = birdX - HOP_DISTANCE; + } else { + targetX = birdX + HOP_DISTANCE; + } + targetY = getFocusedY(); + } + } + + function pet() { + if (currentState === States.IDLE && currentAnimation !== Animations.HEART) { + setAnimation(Animations.HEART); + lastPetTimestamp = Date.now(); + } + } + + function hideBirb() { + canvas.style.display = "none"; + visible = false; + } + + /** + * @param {number} x + * @param {number} y + */ + function flyTo(x, y) { + targetX = x; + targetY = y; + setState(States.FLYING); + setAnimation(Animations.FLYING); + } + + /** + * @returns {boolean} Whether the bird should be absolutely positioned + */ + function isAbsolute() { + return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); + } + + /** + * Set the current animation and reset the animation timer + * @param {Anim} animation + */ + function setAnimation(animation) { + currentAnimation = animation; + animStart = Date.now(); + } + + /** + * Set the current state and reset the state timer + * @param {string} state + */ + function setState(state) { + stateStart = Date.now(); + startX = birdX; + startY = birdY; + currentState = state; + if (state === States.IDLE) { + setAnimation(Animations.BOB); + } + if (isAbsolute()) { + canvas.classList.add("birb-absolute"); + } else { + canvas.classList.remove("birb-absolute"); + } + setY(birdY); + } + + /** + * @param {number} x + */ + function setX(x) { + let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2)); + canvas.style.left = `${x + mod}px`; + } + + /** + * @param {number} y + */ + function setY(y) { + let bottom; + if (isAbsolute()) { + // Position is absolute, convert from fixed + bottom = y - window.scrollY; + } else { + // Position is fixed + bottom = y; + } + canvas.style.bottom = `${bottom}px`; + } + + // Helper functions + + /** + * @param {number} startX + * @param {number} startY + * @param {number} endX + * @param {number} endY + * @param {number} amount + * @param {number} [intensity] + * @returns {{x: number, y: number}} + */ + function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) { + const dx = endX - startX; + const dy = endY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx); + const midX = startX + Math.cos(angle) * distance / 2; + const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity; + const t = amount; + const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX; + const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY; + return { x, y }; + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + + // Run the birb + init(); + draw(); + }).catch((e) => { + error("Error while loading sprite sheets: ", e); + }); + + /** + * @returns {boolean} Whether the user is on a mobile device + */ + function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + } + + function log() { + console.log("Birb: ", ...arguments); + } + + function debug() { + if (debugMode) { + console.debug("Birb: ", ...arguments); } } - function pet() { - if (currentState === States.IDLE && currentAnimation !== Animations.HEART) { - setAnimation(Animations.HEART); - lastPetTimestamp = Date.now(); - } + function error() { + console.error("Birb: ", ...arguments); } - function hideBirb() { - canvas.style.display = "none"; - visible = false; - } - - /** - * @param {number} x - * @param {number} y - */ - function flyTo(x, y) { - targetX = x; - targetY = y; - setState(States.FLYING); - setAnimation(Animations.FLYING); - } - - /** - * @returns {boolean} Whether the bird should be absolutely positioned - */ - function isAbsolute() { - return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); - } - - /** - * Set the current animation and reset the animation timer - * @param {Anim} animation - */ - function setAnimation(animation) { - currentAnimation = animation; - animStart = Date.now(); - } - - /** - * Set the current state and reset the state timer - * @param {string} state - */ - function setState(state) { - stateStart = Date.now(); - startX = birdX; - startY = birdY; - currentState = state; - if (state === States.IDLE) { - setAnimation(Animations.BOB); - } - if (isAbsolute()) { - canvas.classList.add("birb-absolute"); - } else { - canvas.classList.remove("birb-absolute"); - } - setY(birdY); - } - - /** - * @param {number} x - */ - function setX(x) { - let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2)); - canvas.style.left = `${x + mod}px`; - } - - /** - * @param {number} y - */ - function setY(y) { - let bottom; - if (isAbsolute()) { - // Position is absolute, convert from fixed - bottom = y - window.scrollY; - } else { - // Position is fixed - bottom = y; - } - canvas.style.bottom = `${bottom}px`; - } - - // Helper functions - - /** - * @param {number} startX - * @param {number} startY - * @param {number} endX - * @param {number} endY - * @param {number} amount - * @param {number} [intensity] - * @returns {{x: number, y: number}} - */ - function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) { - const dx = endX - startX; - const dy = endY - startY; - const distance = Math.sqrt(dx * dx + dy * dy); - const angle = Math.atan2(dy, dx); - const midX = startX + Math.cos(angle) * distance / 2; - const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity; - const t = amount; - const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX; - const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY; - return { x, y }; - } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - - // Run the birb - init(); - draw(); -}).catch((e) => { - error("Error while loading sprite sheets: ", e); -}); - -/** - * @returns {boolean} Whether the user is on a mobile device - */ -function isMobile() { - return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); -} - -function log() { - console.log("Birb: ", ...arguments); -} - -function debug() { - if (debugMode) { - console.debug("Birb: ", ...arguments); - } -} - -function error() { - console.error("Birb: ", ...arguments); -} \ No newline at end of file +})(); diff --git a/dist/birb.user.js b/dist/birb.user.js index 1a06cad..4af6af2 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -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,64 +12,230 @@ // @grant GM_deleteValue // ==/UserScript== -// @ts-check +(function () { + 'use strict'; -const SHARED_CONFIG = { - birbCssScale: 1, - uiCssScale: 1, - canvasPixelSize: 1, - hopSpeed: 0.07, - hopDistance: 45, -}; + // @ts-check -const DESKTOP_CONFIG = { - flySpeed: 0.25 -}; + // 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"; -const MOBILE_CONFIG = { - uiCssScale: 0.9, - flySpeed: 0.125, -}; + /** @type {Record} */ + 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 CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG }; + const Directions = { + LEFT: -1, + RIGHT: 1, + }; -let debugMode = location.hostname === "127.0.0.1"; -let frozen = false; + // @ts-check -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; + class Layer { + /** + * @param {string[][]} pixels + * @param {string} [tag] + */ + constructor(pixels, tag = "default") { + this.pixels = pixels; + this.tag = tag; + } + } -const DEFAULT_SETTINGS = { - birbMode: false -}; + // @ts-check -/** - * @typedef {typeof DEFAULT_SETTINGS} Settings - */ -/** - * @typedef {Object} SavedStickyNote - * @property {string} id - * @property {string} site - * @property {string} content - * @property {number} top - * @property {number} left - */ + class BirdType { + /** + * @param {string} name + * @param {string} description + * @param {Record} 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} */ + this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.tags = tags; + } + } -/** - * @typedef {Object} BirbSaveData - * @property {string[]} unlockedSpecies - * @property {string} currentSpecies - * @property {Partial} settings - * @property {SavedStickyNote[]} [stickyNotes] - */ + // @ts-check -/** @type {Partial} */ -let userSettings = {}; + class Frame { -const STYLESHEET = `:root { + /** @type {{ [tag: string]: string[][] }} */ + #pixelsByTag = {}; + + /** + * @param {Layer[]} layers + */ + constructor(layers) { + /** @type {Set} */ + 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 = { + flySpeed: 0.25 + }; + + const MOBILE_CONFIG = { + uiCssScale: 0.9, + flySpeed: 0.125, + }; + + const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG }; + + 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 DEFAULT_SETTINGS = { + birbMode: false + }; + + /** + * @typedef {typeof DEFAULT_SETTINGS} Settings + */ + + /** + * @typedef {Object} SavedStickyNote + * @property {string} id + * @property {string} site + * @property {string} content + * @property {number} top + * @property {number} left + */ + + /** + * @typedef {Object} BirbSaveData + * @property {string[]} unlockedSpecies + * @property {string} currentSpecies + * @property {Partial} settings + * @property {SavedStickyNote[]} [stickyNotes] + */ + + /** @type {Partial} */ + let userSettings = {}; + + 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,782 +580,621 @@ 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} */ - let tags = new Set(); - for (let layer of layers) { - tags.add(layer.tag); + 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; } - 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"); + + getAnimationDuration() { + return this.durations.reduce((a, b) => a + b, 0); + } + + /** + * @param {CanvasRenderingContext2D} ctx + * @param {number} direction + * @param {number} timeStart The start time of the animation in milliseconds + * @param {BirdType} [species] The species to use for the animation + * @returns {boolean} Whether the animation is complete + */ + draw(ctx, direction, timeStart, species) { + let time = Date.now() - timeStart; + const duration = this.getAnimationDuration(); + if (this.loop) { + time %= duration; } - 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)); + let totalDuration = 0; + for (let i = 0; i < this.durations.length; i++) { + totalDuration += this.durations[i]; + if (time < totalDuration) { + this.frames[i].draw(ctx, direction, CANVAS_PIXEL_SIZE, species); + return false; + } } - // 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]; + // Draw the last frame if the animation is complete + this.frames[this.frames.length - 1].draw(ctx, direction, CANVAS_PIXEL_SIZE, species); + return true; + } + } + + /** @type {Record} */ + const species = { + bluebird: new BirdType("Eastern Bluebird", + "Native to North American and very social, though can be timid around people.", { + [FOOT]: "#af8e75", + [FACE]: "#639bff", + [BELLY]: "#f8b143", + [UNDERBELLY]: "#ec8637", + [WING]: "#578ae6", + [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.", { + [FOOT]: "#af8e75", + [FACE]: "#ffffff", + [BELLY]: "#ebe9e8", + [UNDERBELLY]: "#ebd9d0", + [WING]: "#f3d3c1", + [WING_EDGE]: "#2d2d2dff", + [THEME_HIGHLIGHT]: "#d7ac93", + }), + tuftedTitmouse: new BirdType("Tufted Titmouse", + "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", { + [FOOT]: "#af8e75", + [FACE]: "#c7cad7", + [BELLY]: "#e4e5eb", + [UNDERBELLY]: "#d7cfcb", + [WING]: "#b1b5c5", + [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.", { + [FOOT]: "#af8e75", + [FACE]: "#ffaf34", + [HOOD]: "#aaa094", + [BELLY]: "#ffaf34", + [UNDERBELLY]: "#babec2", + [WING]: "#aaa094", + [WING_EDGE]: "#888580", + [THEME_HIGHLIGHT]: "#ffaf34", + }), + redCardinal: new BirdType("Red Cardinal", + "Native to the eastern United States, this strikingly red bird is hard to miss.", { + [BEAK]: "#d93619", + [FOOT]: "#af8e75", + [FACE]: "#31353d", + [HOOD]: "#e83a1b", + [BELLY]: "#e83a1b", + [UNDERBELLY]: "#dc3719", + [WING]: "#d23215", + [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.", { + [BEAK]: "#ffaf34", + [FOOT]: "#af8e75", + [FACE]: "#fff255", + [NOSE]: "#383838", + [HOOD]: "#383838", + [BELLY]: "#fff255", + [UNDERBELLY]: "#f5ea63", + [WING]: "#e8e079", + [WING_EDGE]: "#191919", + [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.", { + [FOOT]: "#af8e75", + [FACE]: "#db7c4d", + [BELLY]: "#f7e1c9", + [UNDERBELLY]: "#ebc9a3", + [WING]: "#2252a9", + [WING_EDGE]: "#1c448b", + [HOOD]: "#2252a9", + }), + mistletoebird: new BirdType("Mistletoebird", + "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", { + [FOOT]: "#6c6a7c", + [FACE]: "#352e6d", + [BELLY]: "#fd6833", + [UNDERBELLY]: "#e6e1d8", + [WING]: "#342b7c", + [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.", { + [BEAK]: "#f71919", + [FOOT]: "#af7575", + [FACE]: "#cb092b", + [BELLY]: "#ae1724", + [UNDERBELLY]: "#831b24", + [WING]: "#7e3030", + [WING_EDGE]: "#490f0f", + }), + scarletRobin: new BirdType("Scarlet Robin", + "Native to Australia, this striking robin can be found in Eucalyptus forests.", { + [FOOT]: "#494949", + [FACE]: "#3d3d3d", + [BELLY]: "#fc5633", + [UNDERBELLY]: "#dcdcdc", + [WING]: "#2b2b2b", + [WING_EDGE]: "#ebebeb", + [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.", { + [BEAK]: "#e89f30", + [FOOT]: "#9f8075", + [FACE]: "#2d2d2d", + [BELLY]: "#eb7a3a", + [UNDERBELLY]: "#eb7a3a", + [WING]: "#444444", + [WING_EDGE]: "#232323", + [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.", { + [FOOT]: "#af8e75", + [FACE]: "#edc7a9", + [NOSE]: "#f7eee5", + [HOOD]: "#c58a5b", + [BELLY]: "#e1b796", + [UNDERBELLY]: "#c79e7c", + [WING]: "#c58a5b", + [WING_EDGE]: "#866348", + }), + }; + + const DEFAULT_BIRD = "bluebird"; + + const SPRITE_WIDTH = 32; + const SPRITE_HEIGHT = 32; + const DECORATIONS_SPRITE_WIDTH = 48; + const FEATHER_SPRITE_WIDTH = 32; + + 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 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} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(TRANSPARENT); + continue; } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + log("Loading sprite sheets..."); + + Promise.all([ + loadSpriteSheetPixels(SPRITE_SHEET), + loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), + loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) + ]).then(([birbPixels, decorationPixels, featherPixels]) => { + + const SPRITE_SHEET = birbPixels; + const DECORATIONS_SPRITE_SHEET = decorationPixels; + const FEATHER_SPRITE_SHEET = featherPixels; + + const layers = { + base: new Layer(getLayer(SPRITE_SHEET, 0)), + down: new Layer(getLayer(SPRITE_SHEET, 1)), + heartOne: new Layer(getLayer(SPRITE_SHEET, 2)), + heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), + heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), + tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"), + tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"), + wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)), + wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)), + happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), + }; + + const decorationLayers = { + mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)), + }; + + const featherLayers = { + feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), + }; + + const birbFrames = { + base: new Frame([layers.base, layers.tuftBase]), + headDown: new Frame([layers.down, layers.tuftDown]), + wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]), + wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]), + heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]), + heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), + heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]), + heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), + }; + + ({ + mac: new Frame([decorationLayers.mac]), + }); + + const featherFrames = { + feather: new Frame([featherLayers.feather]), + }; + + const Animations = { + STILL: new Anim([birbFrames.base], [1000]), + BOB: new Anim([ + birbFrames.base, + birbFrames.headDown + ], [ + 420, + 420 + ]), + FLYING: new Anim([ + birbFrames.base, + birbFrames.wingsUp, + birbFrames.headDown, + birbFrames.wingsDown, + ], [ + 30, + 80, + 30, + 60, + ]), + HEART: new Anim([ + birbFrames.heartOne, + birbFrames.heartTwo, + birbFrames.heartThree, + birbFrames.heartFour, + birbFrames.heartThree, + birbFrames.heartFour, + birbFrames.heartThree, + birbFrames.heartFour, + ], [ + 60, + 80, + 250, + 250, + 250, + 250, + 250, + 250, + ], false), + }; + + const FEATHER_ANIMATIONS = { + feather: new Anim([ + featherFrames.feather, + ], [ + 1000, + ]), + }; + + class MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {boolean} [removeMenu] + * @param {boolean} [isDebug] + */ + constructor(text, action, removeMenu = true, isDebug = false) { + this.text = text; + this.action = action; + this.removeMenu = removeMenu; + this.isDebug = isDebug; + } + } + + class DebugMenuItem extends MenuItem { + /** + * @param {string} text + * @param {() => void} action + */ + constructor(text, action, removeMenu = true) { + super(text, action, removeMenu, true); + } + } + + class Separator extends MenuItem { + constructor() { + super("", () => { }); + } + } + + class StickyNote { + /** + * @param {string} id + * @param {string} [site] + * @param {string} [content] + * @param {number} [top] + * @param {number} [left] + */ + constructor(id, site = "", content = "", top = 0, left = 0) { + this.id = id; + this.site = site; + this.content = content; + this.top = top; + this.left = left; + } + } + + const menuItems = [ + new MenuItem(`Pet ${birdBirb()}`, pet), + new MenuItem("Field Guide", insertFieldGuide), + new MenuItem("Sticky Note", newStickyNote), + new MenuItem(`Hide ${birdBirb()}`, hideBirb), + new DebugMenuItem("Freeze/Unfreeze", () => { + frozen = !frozen; + }), + new DebugMenuItem("Reset Data", resetSaveData), + new DebugMenuItem("Unlock All", () => { + for (let type in species) { + unlockBird(type); + } + }), + new DebugMenuItem("Disable Debug", () => { + debugMode = false; + }), + new Separator(), + new MenuItem("Settings", () => switchMenuItems(settingsItems), false), + ]; + + const settingsItems = [ + new MenuItem("Go Back", () => switchMenuItems(menuItems), false), + new Separator(), + new MenuItem("Toggle Birb Mode", () => { + userSettings.birbMode = !userSettings.birbMode; + save(); + insertModal(`${birdBirb()} Mode`, `Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"${userSettings.birbMode ? "\n\nWelcome back to 2012" : ""}`); + }) + ]; + + const styleElement = document.createElement("style"); + const canvas = document.createElement("canvas"); + + /** @type {CanvasRenderingContext2D} */ + // @ts-expect-error + const ctx = canvas.getContext("2d"); + + const States = { + IDLE: "idle", + HOP: "hop", + FLYING: "flying", + }; + + let stateStart = Date.now(); + let currentState = States.IDLE; + let animStart = Date.now(); + let currentAnimation = Animations.BOB; + let direction = Directions.RIGHT; + let ticks = 0; + // Bird's current position + let birdY = 0; + let birdX = 40; + // Bird's starting position (when flying) + let startX = 0; + let startY = 0; + // Bird's target position (when flying) + let targetX = 0; + let targetY = 0; + /** @type {HTMLElement|null} */ + let focusedElement = null; + let focusedBounds = { left: 0, right: 0, top: 0 }; + let lastActionTimestamp = Date.now(); + /** @type {number[]} */ + let petStack = []; + let currentSpecies = DEFAULT_BIRD; + let unlockedSpecies = [DEFAULT_BIRD]; + let visible = true; + let lastPetTimestamp = 0; + /** @type {StickyNote[]} */ + let stickyNotes = []; + + /** + * @returns {boolean} Whether the script is running in a userscript extension context + */ + function isUserScript() { + // @ts-expect-error + return typeof GM_getValue === "function"; + } + + function isTestEnvironment() { + return window.location.hostname === "127.0.0.1" + || window.location.hostname === "localhost" + || window.location.hostname.startsWith("192.168."); + } + + function load() { + /** @type {Record} */ + let saveData = {}; + + if (isUserScript()) { + log("Loading save data from UserScript storage"); + // @ts-expect-error + saveData = GM_getValue("birbSaveData", {}) ?? {}; + } else if (isTestEnvironment()) { + log("Test environment detected, loading save data from localStorage"); + saveData = JSON.parse(localStorage.getItem("birbSaveData") ?? "{}"); + } else { + log("Not a UserScript"); + } + + debug("Loaded data: " + JSON.stringify(saveData)); + + if (!saveData.settings) { + log("No user settings found in save data, starting fresh"); + } + + userSettings = saveData.settings ?? {}; + unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; + currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; + stickyNotes = []; + + if (saveData.stickyNotes) { + for (let note of saveData.stickyNotes) { + if (note.id) { + stickyNotes.push(new StickyNote(note.id, note.site, note.content, note.top, note.left)); } } } - this.#pixelsByTag[tag] = this.pixels.map(row => row.slice()); + + log(stickyNotes.length + " sticky notes loaded"); + switchSpecies(currentSpecies); } - } - /** - * @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); + function save() { + /** @type {BirbSaveData} */ + const saveData = { + unlockedSpecies, + currentSpecies, + settings: userSettings }; - }; - } -} -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; - } + if (stickyNotes.length > 0) { + saveData.stickyNotes = stickyNotes.map(note => ({ + id: note.id, + site: note.site, + content: note.content, + top: note.top, + left: note.left + })); + } - getAnimationDuration() { - return this.durations.reduce((a, b) => a + b, 0); - } - - /** - * @param {CanvasRenderingContext2D} ctx - * @param {number} direction - * @param {number} timeStart The start time of the animation in milliseconds - * @param {BirdType} [species] The species to use for the animation - * @returns {boolean} Whether the animation is complete - */ - draw(ctx, direction, timeStart, species) { - let time = Date.now() - timeStart; - const duration = this.getAnimationDuration(); - if (this.loop) { - time %= duration; - } - let totalDuration = 0; - for (let i = 0; i < this.durations.length; i++) { - totalDuration += this.durations[i]; - if (time < totalDuration) { - this.frames[i].draw(ctx, direction, species); - return false; + if (isUserScript()) { + log("Saving data to UserScript storage"); + // @ts-expect-error + GM_setValue("birbSaveData", saveData); + } else if (isTestEnvironment()) { + log("Test environment detected, saving data to localStorage"); + localStorage.setItem("birbSaveData", JSON.stringify(saveData)); + } else { + log("Not a UserScript"); } } - // Draw the last frame if the animation is complete - this.frames[this.frames.length - 1].draw(ctx, direction, 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"; + function resetSaveData() { + if (isUserScript()) { + log("Resetting save data in UserScript storage"); + // @ts-expect-error + GM_deleteValue("birbSaveData"); + } else if (isTestEnvironment()) { + log("Test environment detected, resetting save data in localStorage"); + localStorage.removeItem("birbSaveData"); + } else { + log("Not a UserScript"); + } + load(); + } -/** @type {Record} */ -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, -}; + /** + * Get the user settings merged with default settings + * @returns {Settings} The merged settings + */ + function settings() { + return { ...DEFAULT_SETTINGS, ...userSettings }; + } -class BirdType { - /** - * @param {string} name - * @param {string} description - * @param {Record} 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} */ - this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; - this.tags = tags; - } -} + /** + * Bird or birb, you decide + */ + function birdBirb() { + return settings().birbMode ? "Birb" : "Bird"; + } -/** @type {Record} */ -const species = { - bluebird: new BirdType("Eastern Bluebird", - "Native to North American and very social, though can be timid around people.", { - [FOOT]: "#af8e75", - [FACE]: "#639bff", - [BELLY]: "#f8b143", - [UNDERBELLY]: "#ec8637", - [WING]: "#578ae6", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#ffffff", - [BELLY]: "#ebe9e8", - [UNDERBELLY]: "#ebd9d0", - [WING]: "#f3d3c1", - [WING_EDGE]: "#2d2d2dff", - [THEME_HIGHLIGHT]: "#d7ac93", - }), - tuftedTitmouse: new BirdType("Tufted Titmouse", - "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", { - [FOOT]: "#af8e75", - [FACE]: "#c7cad7", - [BELLY]: "#e4e5eb", - [UNDERBELLY]: "#d7cfcb", - [WING]: "#b1b5c5", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#ffaf34", - [HOOD]: "#aaa094", - [BELLY]: "#ffaf34", - [UNDERBELLY]: "#babec2", - [WING]: "#aaa094", - [WING_EDGE]: "#888580", - [THEME_HIGHLIGHT]: "#ffaf34", - }), - redCardinal: new BirdType("Red Cardinal", - "Native to the eastern United States, this strikingly red bird is hard to miss.", { - [BEAK]: "#d93619", - [FOOT]: "#af8e75", - [FACE]: "#31353d", - [HOOD]: "#e83a1b", - [BELLY]: "#e83a1b", - [UNDERBELLY]: "#dc3719", - [WING]: "#d23215", - [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.", { - [BEAK]: "#ffaf34", - [FOOT]: "#af8e75", - [FACE]: "#fff255", - [NOSE]: "#383838", - [HOOD]: "#383838", - [BELLY]: "#fff255", - [UNDERBELLY]: "#f5ea63", - [WING]: "#e8e079", - [WING_EDGE]: "#191919", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#db7c4d", - [BELLY]: "#f7e1c9", - [UNDERBELLY]: "#ebc9a3", - [WING]: "#2252a9", - [WING_EDGE]: "#1c448b", - [HOOD]: "#2252a9", - }), - mistletoebird: new BirdType("Mistletoebird", - "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", { - [FOOT]: "#6c6a7c", - [FACE]: "#352e6d", - [BELLY]: "#fd6833", - [UNDERBELLY]: "#e6e1d8", - [WING]: "#342b7c", - [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.", { - [BEAK]: "#f71919", - [FOOT]: "#af7575", - [FACE]: "#cb092b", - [BELLY]: "#ae1724", - [UNDERBELLY]: "#831b24", - [WING]: "#7e3030", - [WING_EDGE]: "#490f0f", - }), - scarletRobin: new BirdType("Scarlet Robin", - "Native to Australia, this striking robin can be found in Eucalyptus forests.", { - [FOOT]: "#494949", - [FACE]: "#3d3d3d", - [BELLY]: "#fc5633", - [UNDERBELLY]: "#dcdcdc", - [WING]: "#2b2b2b", - [WING_EDGE]: "#ebebeb", - [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.", { - [BEAK]: "#e89f30", - [FOOT]: "#9f8075", - [FACE]: "#2d2d2d", - [BELLY]: "#eb7a3a", - [UNDERBELLY]: "#eb7a3a", - [WING]: "#444444", - [WING_EDGE]: "#232323", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#edc7a9", - [NOSE]: "#f7eee5", - [HOOD]: "#c58a5b", - [BELLY]: "#e1b796", - [UNDERBELLY]: "#c79e7c", - [WING]: "#c58a5b", - [WING_EDGE]: "#866348", - }), -}; - -const DEFAULT_BIRD = "bluebird"; - - -const Directions = { - LEFT: -1, - RIGHT: 1, -}; - -const SPRITE_WIDTH = 32; -const SPRITE_HEIGHT = 32; -const DECORATIONS_SPRITE_WIDTH = 48; -const FEATHER_SPRITE_WIDTH = 32; - -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 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} - */ -function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); + function init() { + if (window !== window.top) { + // Skip installation if within an iframe + log("In iframe, skipping Birb script initialization"); return; } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); -} + log("Sprite sheets loaded successfully, initializing bird..."); -log("Loading sprite sheets..."); + // Preload font + const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf"; + const fontLink = document.createElement("link"); + fontLink.rel = "stylesheet"; + fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`; + document.head.appendChild(fontLink); -Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) -]).then(([birbPixels, decorationPixels, featherPixels]) => { - - const SPRITE_SHEET = birbPixels; - const DECORATIONS_SPRITE_SHEET = decorationPixels; - const FEATHER_SPRITE_SHEET = featherPixels; - - const layers = { - base: new Layer(getLayer(SPRITE_SHEET, 0)), - down: new Layer(getLayer(SPRITE_SHEET, 1)), - heartOne: new Layer(getLayer(SPRITE_SHEET, 2)), - heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), - heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), - tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"), - tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"), - wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)), - wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)), - happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), - }; - - const decorationLayers = { - mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)), - }; - - const featherLayers = { - feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), - }; - - const birbFrames = { - base: new Frame([layers.base, layers.tuftBase]), - headDown: new Frame([layers.down, layers.tuftDown]), - wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]), - wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]), - heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]), - heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]), - heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - }; - - const decorationFrames = { - mac: new Frame([decorationLayers.mac]), - }; - - const featherFrames = { - feather: new Frame([featherLayers.feather]), - }; - - const Animations = { - STILL: new Anim([birbFrames.base], [1000]), - BOB: new Anim([ - birbFrames.base, - birbFrames.headDown - ], [ - 420, - 420 - ]), - FLYING: new Anim([ - birbFrames.base, - birbFrames.wingsUp, - birbFrames.headDown, - birbFrames.wingsDown, - ], [ - 30, - 80, - 30, - 60, - ]), - HEART: new Anim([ - birbFrames.heartOne, - birbFrames.heartTwo, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - ], [ - 60, - 80, - 250, - 250, - 250, - 250, - 250, - 250, - ], false), - }; - - const DECORATION_ANIMATIONS = { - mac: new Anim([ - decorationFrames.mac, - ], [ - 1000, - ]), - }; - - const FEATHER_ANIMATIONS = { - feather: new Anim([ - featherFrames.feather, - ], [ - 1000, - ]), - }; - - class MenuItem { - /** - * @param {string} text - * @param {() => void} action - * @param {boolean} [removeMenu] - * @param {boolean} [isDebug] - */ - constructor(text, action, removeMenu = true, isDebug = false) { - this.text = text; - this.action = action; - this.removeMenu = removeMenu; - this.isDebug = isDebug; - } - } - - class DebugMenuItem extends MenuItem { - /** - * @param {string} text - * @param {() => void} action - */ - constructor(text, action, removeMenu = true) { - super(text, action, removeMenu, true); - } - } - - class Separator extends MenuItem { - constructor() { - super("", () => { }); - } - } - - class StickyNote { - /** - * @param {string} id - * @param {string} [site] - * @param {string} [content] - * @param {number} [top] - * @param {number} [left] - */ - constructor(id, site = "", content = "", top = 0, left = 0) { - this.id = id; - this.site = site; - this.content = content; - this.top = top; - this.left = left; - } - } - - const menuItems = [ - new MenuItem(`Pet ${birdBirb()}`, pet), - new MenuItem("Field Guide", insertFieldGuide), - new MenuItem("Sticky Note", newStickyNote), - new MenuItem(`Hide ${birdBirb()}`, hideBirb), - new DebugMenuItem("Freeze/Unfreeze", () => { - frozen = !frozen; - }), - new DebugMenuItem("Reset Data", resetSaveData), - new DebugMenuItem("Unlock All", () => { - for (let type in species) { - unlockBird(type); - } - }), - new DebugMenuItem("Disable Debug", () => { - debugMode = false; - }), - new Separator(), - new MenuItem("Settings", () => switchMenuItems(settingsItems), false), - ]; - - const settingsItems = [ - new MenuItem("Go Back", () => switchMenuItems(menuItems), false), - new Separator(), - new MenuItem("Toggle Birb Mode", () => { - userSettings.birbMode = !userSettings.birbMode; - save(); - insertModal(`${birdBirb()} Mode`, `Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"${userSettings.birbMode ? "\n\nWelcome back to 2012" : ""}`); - }) - ]; - - const styleElement = document.createElement("style"); - const canvas = document.createElement("canvas"); - - /** @type {CanvasRenderingContext2D} */ - // @ts-expect-error - const ctx = canvas.getContext("2d"); - - const States = { - IDLE: "idle", - HOP: "hop", - FLYING: "flying", - }; - - let stateStart = Date.now(); - let currentState = States.IDLE; - let animStart = Date.now(); - let currentAnimation = Animations.BOB; - let direction = Directions.RIGHT; - let ticks = 0; - // Bird's current position - let birdY = 0; - let birdX = 40; - // Bird's starting position (when flying) - let startX = 0; - let startY = 0; - // Bird's target position (when flying) - let targetX = 0; - let targetY = 0; - /** @type {HTMLElement|null} */ - let focusedElement = null; - let focusedBounds = { left: 0, right: 0, top: 0 }; - let lastActionTimestamp = Date.now(); - /** @type {number[]} */ - let petStack = []; - let currentSpecies = DEFAULT_BIRD; - let unlockedSpecies = [DEFAULT_BIRD]; - let visible = true; - let lastPetTimestamp = 0; - /** @type {StickyNote[]} */ - let stickyNotes = []; - - /** - * @returns {boolean} Whether the script is running in a userscript extension context - */ - function isUserScript() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - function isTestEnvironment() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - function load() { - /** @type {Record} */ - let saveData = {}; - - if (isUserScript()) { - log("Loading save data from UserScript storage"); - // @ts-expect-error - saveData = GM_getValue("birbSaveData", {}) ?? {}; - } else if (isTestEnvironment()) { - log("Test environment detected, loading save data from localStorage"); - saveData = JSON.parse(localStorage.getItem("birbSaveData") ?? "{}"); - } else { - log("Not a UserScript"); - } - - debug("Loaded data: " + JSON.stringify(saveData)); - - if (!saveData.settings) { - log("No user settings found in save data, starting fresh"); - } - - userSettings = saveData.settings ?? {}; - unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; - currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; - stickyNotes = []; - - if (saveData.stickyNotes) { - for (let note of saveData.stickyNotes) { - if (note.id) { - stickyNotes.push(new StickyNote(note.id, note.site, note.content, note.top, note.left)); - } - } - } - - log(stickyNotes.length + " sticky notes loaded"); - switchSpecies(currentSpecies); - } - - function save() { - /** @type {BirbSaveData} */ - const saveData = { - unlockedSpecies, - currentSpecies, - settings: userSettings - }; - - if (stickyNotes.length > 0) { - saveData.stickyNotes = stickyNotes.map(note => ({ - id: note.id, - site: note.site, - content: note.content, - top: note.top, - left: note.left - })); - } - - if (isUserScript()) { - log("Saving data to UserScript storage"); - // @ts-expect-error - GM_setValue("birbSaveData", saveData); - } else if (isTestEnvironment()) { - log("Test environment detected, saving data to localStorage"); - localStorage.setItem("birbSaveData", JSON.stringify(saveData)); - } else { - log("Not a UserScript"); - } - } - - function resetSaveData() { - if (isUserScript()) { - log("Resetting save data in UserScript storage"); - // @ts-expect-error - GM_deleteValue("birbSaveData"); - } else if (isTestEnvironment()) { - log("Test environment detected, resetting save data in localStorage"); - localStorage.removeItem("birbSaveData"); - } else { - log("Not a UserScript"); - } - load(); - } - - /** - * Get the user settings merged with default settings - * @returns {Settings} The merged settings - */ - function settings() { - return { ...DEFAULT_SETTINGS, ...userSettings }; - } - - /** - * Bird or birb, you decide - */ - function birdBirb() { - return settings().birbMode ? "Birb" : "Bird"; - } - - function init() { - if (window !== window.top) { - // Skip installation if within an iframe - log("In iframe, skipping Birb script initialization"); - return; - } - log("Sprite sheets loaded successfully, initializing bird..."); - - // Preload font - const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf"; - const fontLink = document.createElement("link"); - fontLink.rel = "stylesheet"; - fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`; - document.head.appendChild(fontLink); - - // Add stylesheet font-face - const fontFace = ` + // Add stylesheet font-face + const fontFace = ` @font-face { font-family: 'Monocraft'; src: url(${MONOCRAFT_SRC}) format('opentype'); @@ -1197,309 +1202,309 @@ Promise.all([ font-style: normal; } `; - const fontStyle = document.createElement("style"); - fontStyle.innerHTML = fontFace; - document.head.appendChild(fontStyle); + const fontStyle = document.createElement("style"); + fontStyle.innerHTML = fontFace; + document.head.appendChild(fontStyle); - load(); + load(); - styleElement.innerHTML = STYLESHEET; - document.head.appendChild(styleElement); + styleElement.innerHTML = STYLESHEET; + document.head.appendChild(styleElement); - canvas.id = "birb"; - canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE; - canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; - document.body.appendChild(canvas); + canvas.id = "birb"; + canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE; + canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; + document.body.appendChild(canvas); - window.addEventListener("scroll", () => { - lastActionTimestamp = Date.now(); - }); + window.addEventListener("scroll", () => { + lastActionTimestamp = Date.now(); + }); - onClick(document, (e) => { - lastActionTimestamp = Date.now(); - if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { - removeMenu(); + onClick(document, (e) => { + lastActionTimestamp = Date.now(); + if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { + removeMenu(); + } + }); + + onClick(canvas, () => { + if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { + // Currently being pet, don't open menu + return; + } + insertMenu(); + }); + + canvas.addEventListener("mouseover", () => { + lastActionTimestamp = Date.now(); + if (currentState === States.IDLE) { + petStack.push(Date.now()); + if (petStack.length > 10) { + petStack.shift(); + } + const pets = petStack.filter((time) => Date.now() - time < 1000).length; + if (pets >= 3) { + pet(); + // Clear the stack + petStack = []; + } + } + }); + + canvas.addEventListener("touchmove", (e) => { + pet(); + }); + + drawStickyNotes(); + + let lastUrl = (window.location.href ?? "").split("?")[0]; + setInterval(() => { + const currentUrl = (window.location.href ?? "").split("?")[0]; + if (currentUrl !== lastUrl) { + log("URL changed, updating sticky notes"); + lastUrl = currentUrl; + drawStickyNotes(); + } + }, URL_CHECK_INTERVAL); + + setInterval(update, UPDATE_INTERVAL); + } + + function update() { + ticks++; + + // Hide bird if the browser is fullscreen + if (document.fullscreenElement) { + hideBirb(); + // Won't be restored on fullscreen exit } - }); - onClick(canvas, () => { - if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { - // Currently being pet, don't open menu + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { + hop(); + } else if (Date.now() - lastActionTimestamp > AFK_TIME) { + // Idle for a while, do something + if (focusedElement === null) { + // Fly to an element + focusOnElement(); + lastActionTimestamp = Date.now(); + } else if (Math.random() < FOCUS_SWITCH_CHANCE) { + // Fly to another element if idle for a longer while + focusOnElement(); + lastActionTimestamp = Date.now(); + } + } + } else if (currentState === States.HOP) { + if (updateParabolicPath(HOP_SPEED)) { + setState(States.IDLE); + } + } + + // Double the chance of a feather if recently pet + const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1; + if (visible && Math.random() < FEATHER_CHANCE * petMod) { + lastPetTimestamp = 0; + activateFeather(); + } + updateFeather(); + } + + function draw() { + requestAnimationFrame(draw); + + if (!visible) { return; } - insertMenu(); - }); - canvas.addEventListener("mouseover", () => { - lastActionTimestamp = Date.now(); + updateFocusedElementBounds(); + + // Update the bird's position if (currentState === States.IDLE) { - petStack.push(Date.now()); - if (petStack.length > 10) { - petStack.shift(); + if (focusedElement && !isWithinHorizontalBounds()) { + focusOnGround(); } - const pets = petStack.filter((time) => Date.now() - time < 1000).length; - if (pets >= 3) { - pet(); - // Clear the stack - petStack = []; + birdY = getFocusedY(); + } else if (currentState === States.FLYING) { + // Fly to target location (even if in the air) + if (updateParabolicPath(FLY_SPEED)) { + setState(States.IDLE); } } - }); - canvas.addEventListener("touchmove", (e) => { - pet(); - }); - - drawStickyNotes(); - - let lastUrl = (window.location.href ?? "").split("?")[0]; - setInterval(() => { - const currentUrl = (window.location.href ?? "").split("?")[0]; - if (currentUrl !== lastUrl) { - log("URL changed, updating sticky notes"); - lastUrl = currentUrl; - drawStickyNotes(); - } - }, URL_CHECK_INTERVAL); - - setInterval(update, UPDATE_INTERVAL); - } - - function update() { - ticks++; - - // Hide bird if the browser is fullscreen - if (document.fullscreenElement) { - hideBirb(); - // Won't be restored on fullscreen exit - } - - if (currentState === States.IDLE && !frozen && !isMenuOpen()) { - if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { - hop(); - } else if (Date.now() - lastActionTimestamp > AFK_TIME) { - // Idle for a while, do something - if (focusedElement === null) { - // Fly to an element - focusOnElement(); - lastActionTimestamp = Date.now(); - } else if (Math.random() < FOCUS_SWITCH_CHANCE) { - // Fly to another element if idle for a longer while - focusOnElement(); - lastActionTimestamp = Date.now(); - } - } - } else if (currentState === States.HOP) { - if (updateParabolicPath(HOP_SPEED)) { - setState(States.IDLE); - } - } - - // Double the chance of a feather if recently pet - const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1; - if (visible && Math.random() < FEATHER_CHANCE * petMod) { - lastPetTimestamp = 0; - activateFeather(); - } - updateFeather(); - } - - function draw() { - requestAnimationFrame(draw); - - if (!visible) { - return; - } - - updateFocusedElementBounds(); - - // Update the bird's position - if (currentState === States.IDLE) { - if (focusedElement && !isWithinHorizontalBounds()) { + const oldTargetY = targetY; + targetY = getFocusedY(); + // Adjust startY to account for scrolling + startY += targetY - oldTargetY; + if (targetY < 0 || targetY > window.innerHeight) { + // Fly to ground if the focused element moves out of bounds focusOnGround(); } - birdY = getFocusedY(); - } else if (currentState === States.FLYING) { - // Fly to target location (even if in the air) - if (updateParabolicPath(FLY_SPEED)) { - setState(States.IDLE); + + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) { + setAnimation(Animations.STILL); } + + // Update HTML element position + setX(birdX); + setY(birdY); } - const oldTargetY = targetY; - targetY = getFocusedY(); - // Adjust startY to account for scrolling - startY += targetY - oldTargetY; - if (targetY < 0 || targetY > window.innerHeight) { - // Fly to ground if the focused element moves out of bounds - focusOnGround(); + function newStickyNote() { + const id = Date.now().toString(); + const site = window.location.href; + const stickyNote = new StickyNote(id, site, ""); + const element = renderStickyNote(stickyNote); + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; + stickyNote.top = parseInt(element.style.top, 10); + stickyNote.left = parseInt(element.style.left, 10); + stickyNotes.push(stickyNote); + save(); } - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) { - setAnimation(Animations.STILL); - } - - // Update HTML element position - setX(birdX); - setY(birdY); - } - - function newStickyNote() { - const id = Date.now().toString(); - const site = window.location.href; - const stickyNote = new StickyNote(id, site, ""); - const element = renderStickyNote(stickyNote); - element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; - element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; - stickyNote.top = parseInt(element.style.top, 10); - stickyNote.left = parseInt(element.style.left, 10); - stickyNotes.push(stickyNote); - save(); - } - - /** - * @param {StickyNote} stickyNote - * @returns {HTMLElement} - */ - function renderStickyNote(stickyNote) { - let html = ` + /** + * @param {StickyNote} stickyNote + * @returns {HTMLElement} + */ + function renderStickyNote(stickyNote) { + let html = `
Sticky Note
x
-
` - const noteElement = makeElement("birb-window"); - noteElement.classList.add("birb-sticky-note"); - noteElement.innerHTML = html; + `; + const noteElement = makeElement("birb-window"); + noteElement.classList.add("birb-sticky-note"); + noteElement.innerHTML = html; - noteElement.style.top = `${stickyNote.top}px`; - noteElement.style.left = `${stickyNote.left}px`; - document.body.appendChild(noteElement); + noteElement.style.top = `${stickyNote.top}px`; + noteElement.style.left = `${stickyNote.left}px`; + document.body.appendChild(noteElement); - makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { - stickyNote.top = top; - stickyNote.left = left; - save(); - }); - - const closeButton = noteElement.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - if (confirm("Are you sure you want to delete this sticky note?")) { - deleteStickyNote(stickyNote); - noteElement.remove(); - } - }, closeButton); - } - - const textarea = noteElement.querySelector(".birb-sticky-note-input"); - if (textarea && textarea instanceof HTMLTextAreaElement) { - /** @type {NodeJS.Timeout | undefined} */ - let saveTimeout; - // Save after debounce - textarea.addEventListener("input", () => { - stickyNote.content = textarea.value; - if (saveTimeout) { - clearTimeout(saveTimeout); - } - saveTimeout = setTimeout(() => { - save(); - }, 250); + makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + save(); }); + + const closeButton = noteElement.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + if (confirm("Are you sure you want to delete this sticky note?")) { + deleteStickyNote(stickyNote); + noteElement.remove(); + } + }, closeButton); + } + + const textarea = noteElement.querySelector(".birb-sticky-note-input"); + if (textarea && textarea instanceof HTMLTextAreaElement) { + /** @type {NodeJS.Timeout | undefined} */ + let saveTimeout; + // Save after debounce + textarea.addEventListener("input", () => { + stickyNote.content = textarea.value; + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + save(); + }, 250); + }); + } + + // On window resize + window.addEventListener("resize", () => { + const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; + const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; + noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + }); + + return noteElement; } - // On window resize - window.addEventListener("resize", () => { - const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; - const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; - noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; - }); - - return noteElement; - } - - /** - * @param {StickyNote} stickyNote - */ - function deleteStickyNote(stickyNote) { - stickyNotes = stickyNotes.filter(note => note.id !== stickyNote.id); - save(); - } - - /** - * @param {StickyNote} stickyNote - * @returns {boolean} Whether the given sticky note is applicable to the current site/page - */ - function isStickyNoteApplicable(stickyNote) { - const stickyNoteUrl = stickyNote.site; - const currentUrl = window.location.href; - const stickyNoteWebsite = stickyNoteUrl.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - debug("Comparing " + stickyNoteUrl + " with " + currentUrl); - - if (stickyNoteWebsite !== currentWebsite) { - return false; + /** + * @param {StickyNote} stickyNote + */ + function deleteStickyNote(stickyNote) { + stickyNotes = stickyNotes.filter(note => note.id !== stickyNote.id); + save(); } - const stickyNoteParams = parseUrlParams(stickyNoteUrl); - const currentParams = parseUrlParams(currentUrl); + /** + * @param {StickyNote} stickyNote + * @returns {boolean} Whether the given sticky note is applicable to the current site/page + */ + function isStickyNoteApplicable(stickyNote) { + const stickyNoteUrl = stickyNote.site; + const currentUrl = window.location.href; + const stickyNoteWebsite = stickyNoteUrl.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; - debug("Comparing params: ", stickyNoteParams, currentParams); + debug("Comparing " + stickyNoteUrl + " with " + currentUrl); - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) { + if (stickyNoteWebsite !== currentWebsite) { return false; } - } - return true; - } - function drawStickyNotes() { - // Remove all existing sticky notes - const existingNotes = document.querySelectorAll(".birb-sticky-note"); - existingNotes.forEach(note => note.remove()); - // Render all sticky notes - for (let stickyNote of stickyNotes) { - if (isStickyNoteApplicable(stickyNote)) { - renderStickyNote(stickyNote); + const stickyNoteParams = parseUrlParams(stickyNoteUrl); + const currentParams = parseUrlParams(currentUrl); + + debug("Comparing params: ", stickyNoteParams, currentParams); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) { + return false; + } + } + return true; + } + + function drawStickyNotes() { + // Remove all existing sticky notes + const existingNotes = document.querySelectorAll(".birb-sticky-note"); + existingNotes.forEach(note => note.remove()); + // Render all sticky notes + for (let stickyNote of stickyNotes) { + if (isStickyNoteApplicable(stickyNote)) { + renderStickyNote(stickyNote); + } } } - } - /** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ - function makeElement(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - if (textContent) { - element.textContent = textContent; + /** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ + function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } + return element; } - if (id) { - element.id = id; - } - return element; - } - /** - * Create a window element with header and content - * @param {string} id - * @param {string} title - * @param {string} contentHtml - * @param {() => void} [onClose] - * @returns {HTMLElement} - */ - function createWindow(id, title, contentHtml, onClose) { - const window = makeElement("birb-window", undefined, id); - window.innerHTML = ` + /** + * Create a window element with header and content + * @param {string} id + * @param {string} title + * @param {string} contentHtml + * @param {() => void} [onClose] + * @returns {HTMLElement} + */ + function createWindow(id, title, contentHtml, onClose) { + const window = makeElement("birb-window", undefined, id); + window.innerHTML = `
${title}
x
@@ -1509,143 +1514,123 @@ Promise.all([
`; - document.body.appendChild(window); - makeDraggable(window.querySelector(".birb-window-header")); + document.body.appendChild(window); + makeDraggable(window.querySelector(".birb-window-header")); - const closeButton = window.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - if (onClose) { - onClose(); - } - window.remove(); - }, closeButton); - } - - 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; - } - const speciesToUnlock = Object.keys(species).filter((species) => !unlockedSpecies.includes(species)); - if (speciesToUnlock.length === 0) { - // No more species to unlock - return; - } - const birdType = speciesToUnlock[Math.floor(Math.random() * speciesToUnlock.length)]; - insertFeather(birdType); - } - - /** - * @param {string} birdType - */ - function insertFeather(birdType) { - let type = species[birdType]; - const featherCanvas = document.createElement("canvas"); - featherCanvas.id = FEATHER_ID; - featherCanvas.classList.add("birb-decoration"); - featherCanvas.width = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - featherCanvas.height = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - const x = featherCanvas.width * 2 + Math.random() * (window.innerWidth - featherCanvas.width * 4); - featherCanvas.style.marginLeft = `${x}px`; - featherCanvas.style.top = `${-featherCanvas.height}px`; - const featherCtx = featherCanvas.getContext("2d"); - if (!featherCtx) { - return; - } - FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), type); - document.body.appendChild(featherCanvas); - onClick(featherCanvas, () => { - unlockBird(birdType); - removeFeather(); - if (document.querySelector("#" + FIELD_GUIDE_ID)) { - removeFieldGuide(); - insertFieldGuide(); + const closeButton = window.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + window.remove(); + }, closeButton); } - }); - } - function removeFeather() { - const feather = document.querySelector("#" + FEATHER_ID); - if (feather) { - feather.remove(); - } - } - - /** - * @param {string} birdType - */ - function unlockBird(birdType) { - if (!unlockedSpecies.includes(birdType)) { - unlockedSpecies.push(birdType); - insertModal("New Bird Unlocked!", `You've found a ${species[birdType].name} feather! Use the Field Guide to switch your bird's species.`); - } - save(); - } - - function updateFeather() { - const feather = document.querySelector("#birb-feather"); - if (!feather || !(feather instanceof HTMLElement)) { - return; - } - const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED; - feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`; - if (y < window.innerHeight - feather.offsetHeight) { - feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; - } - } - - /** - * @param {HTMLElement} element - */ - function centerElement(element) { - element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; - element.style.top = `${window.innerHeight / 2 - element.offsetHeight / 2}px`; - } - - /** - * @param {string} title - * @param {string} message - */ - function insertModal(title, message) { - if (document.querySelector("#" + FIELD_GUIDE_ID)) { - return; + return window; } - const modal = createWindow("birb-modal", title, ` + function activateFeather() { + if (document.querySelector("#" + FEATHER_ID)) { + return; + } + const speciesToUnlock = Object.keys(species).filter((species) => !unlockedSpecies.includes(species)); + if (speciesToUnlock.length === 0) { + // No more species to unlock + return; + } + const birdType = speciesToUnlock[Math.floor(Math.random() * speciesToUnlock.length)]; + insertFeather(birdType); + } + + /** + * @param {string} birdType + */ + function insertFeather(birdType) { + let type = species[birdType]; + const featherCanvas = document.createElement("canvas"); + featherCanvas.id = FEATHER_ID; + featherCanvas.classList.add("birb-decoration"); + featherCanvas.width = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + featherCanvas.height = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + const x = featherCanvas.width * 2 + Math.random() * (window.innerWidth - featherCanvas.width * 4); + featherCanvas.style.marginLeft = `${x}px`; + featherCanvas.style.top = `${-featherCanvas.height}px`; + const featherCtx = featherCanvas.getContext("2d"); + if (!featherCtx) { + return; + } + FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), type); + document.body.appendChild(featherCanvas); + onClick(featherCanvas, () => { + unlockBird(birdType); + removeFeather(); + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + removeFieldGuide(); + insertFieldGuide(); + } + }); + } + + function removeFeather() { + const feather = document.querySelector("#" + FEATHER_ID); + if (feather) { + feather.remove(); + } + } + + /** + * @param {string} birdType + */ + function unlockBird(birdType) { + if (!unlockedSpecies.includes(birdType)) { + unlockedSpecies.push(birdType); + insertModal("New Bird Unlocked!", `You've found a ${species[birdType].name} feather! Use the Field Guide to switch your bird's species.`); + } + save(); + } + + function updateFeather() { + const feather = document.querySelector("#birb-feather"); + if (!feather || !(feather instanceof HTMLElement)) { + return; + } + const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED; + feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`; + if (y < window.innerHeight - feather.offsetHeight) { + feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; + } + } + + /** + * @param {HTMLElement} element + */ + function centerElement(element) { + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${window.innerHeight / 2 - element.offsetHeight / 2}px`; + } + + /** + * @param {string} title + * @param {string} message + */ + function insertModal(title, message) { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + + const modal = createWindow("birb-modal", title, `
${message}
`); - modal.style.width = "270px"; - centerElement(modal); - } - - function insertFieldGuide() { - if (document.querySelector("#" + FIELD_GUIDE_ID)) { - return; + modal.style.width = "270px"; + centerElement(modal); } - let html = ` + + function insertFieldGuide() { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + let html = `
Field Guide
x
@@ -1653,601 +1638,595 @@ Promise.all([
-
` - const fieldGuide = makeElement("birb-window", undefined, FIELD_GUIDE_ID); - fieldGuide.innerHTML = html; - document.body.appendChild(fieldGuide); - makeDraggable(fieldGuide.querySelector(".birb-window-header")); +
`; + const fieldGuide = makeElement("birb-window", undefined, FIELD_GUIDE_ID); + fieldGuide.innerHTML = html; + document.body.appendChild(fieldGuide); + makeDraggable(fieldGuide.querySelector(".birb-window-header")); - const closeButton = fieldGuide.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - fieldGuide.remove(); - }, closeButton); - } - - const content = fieldGuide.querySelector(".birb-grid-content"); - if (!content) { - return; - } - content.innerHTML = ""; - - const generateDescription = (/** @type {string} */ speciesId) => { - const type = species[speciesId]; - const unlocked = unlockedSpecies.includes(speciesId); - return "" + type.name + "
" + (!unlocked ? "Not yet unlocked" : type.description); - }; - - const description = fieldGuide.querySelector(".birb-field-guide-description"); - if (!description) { - return; - } - description.innerHTML = generateDescription(currentSpecies); - for (const [id, type] of Object.entries(species)) { - const unlocked = unlockedSpecies.includes(id); - const speciesElement = makeElement("birb-grid-item"); - if (id === currentSpecies) { - speciesElement.classList.add("birb-grid-item-selected"); + const closeButton = fieldGuide.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + fieldGuide.remove(); + }, closeButton); } - const speciesCanvas = document.createElement("canvas"); - speciesCanvas.width = SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - speciesCanvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; - const speciesCtx = speciesCanvas.getContext("2d"); - if (!speciesCtx) { + + const content = fieldGuide.querySelector(".birb-grid-content"); + if (!content) { return; } - birbFrames.base.draw(speciesCtx, Directions.RIGHT, type); - speciesElement.appendChild(speciesCanvas); - content.appendChild(speciesElement); - if (unlocked) { - onClick(speciesElement, () => { - switchSpecies(id); - document.querySelectorAll(".birb-grid-item").forEach((element) => { - element.classList.remove("birb-grid-item-selected"); - }); + content.innerHTML = ""; + + const generateDescription = (/** @type {string} */ speciesId) => { + const type = species[speciesId]; + const unlocked = unlockedSpecies.includes(speciesId); + return "" + type.name + "
" + (!unlocked ? "Not yet unlocked" : type.description); + }; + + const description = fieldGuide.querySelector(".birb-field-guide-description"); + if (!description) { + return; + } + description.innerHTML = generateDescription(currentSpecies); + for (const [id, type] of Object.entries(species)) { + const unlocked = unlockedSpecies.includes(id); + const speciesElement = makeElement("birb-grid-item"); + if (id === currentSpecies) { speciesElement.classList.add("birb-grid-item-selected"); + } + const speciesCanvas = document.createElement("canvas"); + speciesCanvas.width = SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + speciesCanvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; + const speciesCtx = speciesCanvas.getContext("2d"); + if (!speciesCtx) { + return; + } + birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); + speciesElement.appendChild(speciesCanvas); + content.appendChild(speciesElement); + if (unlocked) { + onClick(speciesElement, () => { + switchSpecies(id); + document.querySelectorAll(".birb-grid-item").forEach((element) => { + element.classList.remove("birb-grid-item-selected"); + }); + speciesElement.classList.add("birb-grid-item-selected"); + }); + } else { + speciesElement.classList.add("birb-grid-item-locked"); + } + speciesElement.addEventListener("mouseover", () => { + log("mouseover"); + description.innerHTML = generateDescription(id); + }); + speciesElement.addEventListener("mouseout", () => { + description.innerHTML = generateDescription(currentSpecies); }); - } else { - speciesElement.classList.add("birb-grid-item-locked"); } - speciesElement.addEventListener("mouseover", () => { - log("mouseover"); - description.innerHTML = generateDescription(id); - }); - speciesElement.addEventListener("mouseout", () => { - description.innerHTML = generateDescription(currentSpecies); - }); + centerElement(fieldGuide); } - centerElement(fieldGuide); - } - /** - * @param {() => void} func - * @param {Element} [closeButton] - */ - function makeClosable(func, closeButton) { - if (closeButton) { - onClick(closeButton, func); + /** + * @param {() => void} func + * @param {Element} [closeButton] + */ + function makeClosable(func, closeButton) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); } - document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { + + function removeFieldGuide() { + const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); + if (fieldGuide) { + fieldGuide.remove(); + } + } + + /** + * @param {string} type + */ + function switchSpecies(type) { + currentSpecies = type; + // Update CSS variable --birb-highlight to be wing color + document.documentElement.style.setProperty("--birb-highlight", species[type].colors[THEME_HIGHLIGHT]); + save(); + } + + /** + * Add the menu to the page if it doesn't already exist + */ + function insertMenu() { + if (document.querySelector("#" + MENU_ID)) { return; } - if (e.key === "Escape") { - func(); + let menu = makeElement("birb-window", undefined, MENU_ID); + let header = makeElement("birb-window-header"); + header.innerHTML = `
${birdBirb().toLowerCase()}OS
`; + let content = makeElement("birb-window-content"); + for (const item of menuItems) { + if (!item.isDebug || debugMode) { + content.appendChild(makeMenuItem(item)); + } } - }); - } + menu.appendChild(header); + menu.appendChild(content); + document.body.appendChild(menu); + makeDraggable(document.querySelector(".birb-window-header")); - function removeFieldGuide() { - const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); - if (fieldGuide) { - fieldGuide.remove(); - } - } - - function isSafari() { - return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - } - - /** - * @param {string} type - */ - function switchSpecies(type) { - currentSpecies = type; - // Update CSS variable --birb-highlight to be wing color - document.documentElement.style.setProperty("--birb-highlight", species[type].colors[THEME_HIGHLIGHT]); - save(); - } - - /** - * Add the menu to the page if it doesn't already exist - */ - function insertMenu() { - if (document.querySelector("#" + MENU_ID)) { - return; - } - let menu = makeElement("birb-window", undefined, MENU_ID); - let header = makeElement("birb-window-header"); - header.innerHTML = `
${birdBirb().toLowerCase()}OS
`; - let content = makeElement("birb-window-content"); - for (const item of menuItems) { - if (!item.isDebug || debugMode) { - content.appendChild(makeMenuItem(item)); - } - } - menu.appendChild(header); - menu.appendChild(content); - document.body.appendChild(menu); - makeDraggable(document.querySelector(".birb-window-header")); - - let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); - onClick(menuExit, () => { - removeMenu(); - }); - document.body.appendChild(menuExit); - makeClosable(removeMenu); - - updateMenuLocation(menu); - } - - /** - * Update the menu's location based on the bird's position - * @param {HTMLElement} menu - */ - function updateMenuLocation(menu) { - let x = birdX; - let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; - const offset = 20; - if (x < window.innerWidth / 2) { - // Left side - x += offset; - } else { - // Right side - x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; - } - if (y > window.innerHeight / 2) { - // Top side - y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; - } else { - // Bottom side - y += offset; - } - menu.style.left = `${x}px`; - menu.style.top = `${y}px`; - } - - /** - * @param {MenuItem[]} menuItems - */ - function switchMenuItems(menuItems) { - const menu = document.querySelector("#" + MENU_ID); - if (!menu || !(menu instanceof HTMLElement)) { - return; - } - const content = menu.querySelector(".birb-window-content"); - if (!content) { - error("Content not found"); - return; - } - content.innerHTML = ""; - for (const item of menuItems) { - if (!item.isDebug || debugMode) { - content.appendChild(makeMenuItem(item)); - } - } - updateMenuLocation(menu); - } - - /** - * @param {MenuItem} item - * @returns {HTMLElement} - */ - function makeMenuItem(item) { - if (item instanceof Separator) { - return makeElement("birb-window-separator"); - } - let menuItem = makeElement("birb-menu-item", item.text); - onClick(menuItem, () => { - if (item.removeMenu) { + let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); + onClick(menuExit, () => { removeMenu(); - } - item.action(); - }); - return menuItem; - } + }); + document.body.appendChild(menuExit); + makeClosable(removeMenu); - /** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ - function onClick(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); - } - - /** - * Remove the menu from the page - */ - function removeMenu() { - const menu = document.querySelector("#" + MENU_ID); - if (menu) { - menu.remove(); - } - const exitMenu = document.querySelector("#" + MENU_EXIT_ID); - if (exitMenu) { - exitMenu.remove(); - } - } - - /** - * @returns {boolean} Whether the menu element is on the page - */ - function isMenuOpen() { - return document.querySelector("#" + MENU_ID) !== null; - } - - /** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ - function makeDraggable(element, parent = true, callback = () => { }) { - if (!element) { - return; + updateMenuLocation(menu); } - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - error("Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); - } - - /** - * @param {string[][]} array - * @param {number} sprite - * @param {number} [width] - * @returns {string[][]} - */ - function getLayer(array, sprite, width = SPRITE_WIDTH) { - // From an array of a horizontal sprite sheet, get the layer for a specific sprite - const layer = []; - for (let y = 0; y < width; y++) { - layer.push(array[y].slice(sprite * width, (sprite + 1) * width)); - } - return layer; - } - - /** - * Update the birds location from the start to the target location on a parabolic path - * @param {number} speed The speed of the bird along the path - * @param {number} [intensity] The intensity of the parabolic path - * @returns {boolean} Whether the bird has reached the target location - */ - function updateParabolicPath(speed, intensity = 2.5) { - const dx = targetX - startX; - const dy = targetY - startY; - const distance = Math.sqrt(dx * dx + dy * dy); - const time = Date.now() - stateStart; - if (distance > Math.max(window.innerWidth, window.innerHeight) / 2) { - speed *= 1.3; - } - const amount = Math.min(1, time / (distance / speed)); - const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity); - birdX = x; - birdY = y; - const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1; - if (complete) { - birdX = targetX; - birdY = targetY; - } else { - direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT; - } - return complete; - } - - function getFocusedElementRandomX() { - return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; - } - - function isWithinHorizontalBounds() { - return birdX >= focusedBounds.left && birdX <= focusedBounds.right; - } - - function getFocusedY() { - return getFullWindowHeight() - focusedBounds.top; - } - - /** - * @returns The render-safe height of the inner browser window - */ - function getSafeWindowHeight() { - // Necessary because iOS 26 Safari is terrible and won't render - // fixed elements behind the address bar - return window.innerHeight; - } - - /** - * @returns The true height of the inner browser window - */ - function getFullWindowHeight() { - return document.documentElement.clientHeight; - } - - function focusOnGround() { - console.log("Focusing on ground"); - focusedElement = null; - focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; - flyTo(Math.random() * window.innerWidth, 0); - } - - function focusOnElement() { - if (frozen) { - return; - } - const elements = document.querySelectorAll("img, video"); - const inWindow = Array.from(elements).filter((img) => { - const rect = img.getBoundingClientRect(); - return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight; - }); - /** @type {HTMLElement[]} */ - // @ts-expect-error - const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - if (largeElements.length === 0) { - return; - } - const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)]; - focusedElement = randomElement; - log("Focusing on element: ", focusedElement); - updateFocusedElementBounds(); - flyTo(getFocusedElementRandomX(), getFocusedY()); - } - - function updateFocusedElementBounds() { - if (focusedElement === null) { - // Update ground location to bottom of window - focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; - return; - } - const { left, right, top } = focusedElement.getBoundingClientRect(); - focusedBounds = { left, right, top }; - } - - function getCanvasWidth() { - return canvas.width * BIRB_CSS_SCALE - } - - function getCanvasHeight() { - return canvas.height * BIRB_CSS_SCALE - } - - function hop() { - if (frozen) { - return; - } - if (currentState === States.IDLE) { - setState(States.HOP); - setAnimation(Animations.FLYING); - if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { - targetX = birdX - HOP_DISTANCE; + /** + * Update the menu's location based on the bird's position + * @param {HTMLElement} menu + */ + function updateMenuLocation(menu) { + let x = birdX; + let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; + const offset = 20; + if (x < window.innerWidth / 2) { + // Left side + x += offset; } else { - targetX = birdX + HOP_DISTANCE; + // Right side + x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; } - targetY = getFocusedY(); + if (y > window.innerHeight / 2) { + // Top side + y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; + } else { + // Bottom side + y += offset; + } + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + } + + /** + * @param {MenuItem[]} menuItems + */ + function switchMenuItems(menuItems) { + const menu = document.querySelector("#" + MENU_ID); + if (!menu || !(menu instanceof HTMLElement)) { + return; + } + const content = menu.querySelector(".birb-window-content"); + if (!content) { + error("Content not found"); + return; + } + content.innerHTML = ""; + for (const item of menuItems) { + if (!item.isDebug || debugMode) { + content.appendChild(makeMenuItem(item)); + } + } + updateMenuLocation(menu); + } + + /** + * @param {MenuItem} item + * @returns {HTMLElement} + */ + function makeMenuItem(item) { + if (item instanceof Separator) { + return makeElement("birb-window-separator"); + } + let menuItem = makeElement("birb-menu-item", item.text); + onClick(menuItem, () => { + if (item.removeMenu) { + removeMenu(); + } + item.action(); + }); + return menuItem; + } + + /** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ + function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); + } + + /** + * Remove the menu from the page + */ + function removeMenu() { + const menu = document.querySelector("#" + MENU_ID); + if (menu) { + menu.remove(); + } + const exitMenu = document.querySelector("#" + MENU_EXIT_ID); + if (exitMenu) { + exitMenu.remove(); + } + } + + /** + * @returns {boolean} Whether the menu element is on the page + */ + function isMenuOpen() { + return document.querySelector("#" + MENU_ID) !== null; + } + + /** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ + function makeDraggable(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + error("Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); + } + + /** + * @param {string[][]} array + * @param {number} sprite + * @param {number} [width] + * @returns {string[][]} + */ + function getLayer(array, sprite, width = SPRITE_WIDTH) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < width; y++) { + layer.push(array[y].slice(sprite * width, (sprite + 1) * width)); + } + return layer; + } + + /** + * Update the birds location from the start to the target location on a parabolic path + * @param {number} speed The speed of the bird along the path + * @param {number} [intensity] The intensity of the parabolic path + * @returns {boolean} Whether the bird has reached the target location + */ + function updateParabolicPath(speed, intensity = 2.5) { + const dx = targetX - startX; + const dy = targetY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const time = Date.now() - stateStart; + if (distance > Math.max(window.innerWidth, window.innerHeight) / 2) { + speed *= 1.3; + } + const amount = Math.min(1, time / (distance / speed)); + const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity); + birdX = x; + birdY = y; + const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1; + if (complete) { + birdX = targetX; + birdY = targetY; + } else { + direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT; + } + return complete; + } + + function getFocusedElementRandomX() { + return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; + } + + function isWithinHorizontalBounds() { + return birdX >= focusedBounds.left && birdX <= focusedBounds.right; + } + + function getFocusedY() { + return getFullWindowHeight() - focusedBounds.top; + } + + /** + * @returns The render-safe height of the inner browser window + */ + function getSafeWindowHeight() { + // Necessary because iOS 26 Safari is terrible and won't render + // fixed elements behind the address bar + return window.innerHeight; + } + + /** + * @returns The true height of the inner browser window + */ + function getFullWindowHeight() { + return document.documentElement.clientHeight; + } + + function focusOnGround() { + console.log("Focusing on ground"); + focusedElement = null; + focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; + flyTo(Math.random() * window.innerWidth, 0); + } + + function focusOnElement() { + if (frozen) { + return; + } + const elements = document.querySelectorAll("img, video"); + const inWindow = Array.from(elements).filter((img) => { + const rect = img.getBoundingClientRect(); + return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight; + }); + /** @type {HTMLElement[]} */ + // @ts-expect-error + const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); + if (largeElements.length === 0) { + return; + } + const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)]; + focusedElement = randomElement; + log("Focusing on element: ", focusedElement); + updateFocusedElementBounds(); + flyTo(getFocusedElementRandomX(), getFocusedY()); + } + + function updateFocusedElementBounds() { + if (focusedElement === null) { + // Update ground location to bottom of window + focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; + return; + } + const { left, right, top } = focusedElement.getBoundingClientRect(); + focusedBounds = { left, right, top }; + } + + function getCanvasWidth() { + return canvas.width * BIRB_CSS_SCALE + } + + function hop() { + if (frozen) { + return; + } + if (currentState === States.IDLE) { + setState(States.HOP); + setAnimation(Animations.FLYING); + if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { + targetX = birdX - HOP_DISTANCE; + } else { + targetX = birdX + HOP_DISTANCE; + } + targetY = getFocusedY(); + } + } + + function pet() { + if (currentState === States.IDLE && currentAnimation !== Animations.HEART) { + setAnimation(Animations.HEART); + lastPetTimestamp = Date.now(); + } + } + + function hideBirb() { + canvas.style.display = "none"; + visible = false; + } + + /** + * @param {number} x + * @param {number} y + */ + function flyTo(x, y) { + targetX = x; + targetY = y; + setState(States.FLYING); + setAnimation(Animations.FLYING); + } + + /** + * @returns {boolean} Whether the bird should be absolutely positioned + */ + function isAbsolute() { + return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); + } + + /** + * Set the current animation and reset the animation timer + * @param {Anim} animation + */ + function setAnimation(animation) { + currentAnimation = animation; + animStart = Date.now(); + } + + /** + * Set the current state and reset the state timer + * @param {string} state + */ + function setState(state) { + stateStart = Date.now(); + startX = birdX; + startY = birdY; + currentState = state; + if (state === States.IDLE) { + setAnimation(Animations.BOB); + } + if (isAbsolute()) { + canvas.classList.add("birb-absolute"); + } else { + canvas.classList.remove("birb-absolute"); + } + setY(birdY); + } + + /** + * @param {number} x + */ + function setX(x) { + let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2)); + canvas.style.left = `${x + mod}px`; + } + + /** + * @param {number} y + */ + function setY(y) { + let bottom; + if (isAbsolute()) { + // Position is absolute, convert from fixed + bottom = y - window.scrollY; + } else { + // Position is fixed + bottom = y; + } + canvas.style.bottom = `${bottom}px`; + } + + // Helper functions + + /** + * @param {number} startX + * @param {number} startY + * @param {number} endX + * @param {number} endY + * @param {number} amount + * @param {number} [intensity] + * @returns {{x: number, y: number}} + */ + function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) { + const dx = endX - startX; + const dy = endY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx); + const midX = startX + Math.cos(angle) * distance / 2; + const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity; + const t = amount; + const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX; + const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY; + return { x, y }; + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + + // Run the birb + init(); + draw(); + }).catch((e) => { + error("Error while loading sprite sheets: ", e); + }); + + /** + * @returns {boolean} Whether the user is on a mobile device + */ + function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + } + + function log() { + console.log("Birb: ", ...arguments); + } + + function debug() { + if (debugMode) { + console.debug("Birb: ", ...arguments); } } - function pet() { - if (currentState === States.IDLE && currentAnimation !== Animations.HEART) { - setAnimation(Animations.HEART); - lastPetTimestamp = Date.now(); - } + function error() { + console.error("Birb: ", ...arguments); } - function hideBirb() { - canvas.style.display = "none"; - visible = false; - } - - /** - * @param {number} x - * @param {number} y - */ - function flyTo(x, y) { - targetX = x; - targetY = y; - setState(States.FLYING); - setAnimation(Animations.FLYING); - } - - /** - * @returns {boolean} Whether the bird should be absolutely positioned - */ - function isAbsolute() { - return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); - } - - /** - * Set the current animation and reset the animation timer - * @param {Anim} animation - */ - function setAnimation(animation) { - currentAnimation = animation; - animStart = Date.now(); - } - - /** - * Set the current state and reset the state timer - * @param {string} state - */ - function setState(state) { - stateStart = Date.now(); - startX = birdX; - startY = birdY; - currentState = state; - if (state === States.IDLE) { - setAnimation(Animations.BOB); - } - if (isAbsolute()) { - canvas.classList.add("birb-absolute"); - } else { - canvas.classList.remove("birb-absolute"); - } - setY(birdY); - } - - /** - * @param {number} x - */ - function setX(x) { - let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2)); - canvas.style.left = `${x + mod}px`; - } - - /** - * @param {number} y - */ - function setY(y) { - let bottom; - if (isAbsolute()) { - // Position is absolute, convert from fixed - bottom = y - window.scrollY; - } else { - // Position is fixed - bottom = y; - } - canvas.style.bottom = `${bottom}px`; - } - - // Helper functions - - /** - * @param {number} startX - * @param {number} startY - * @param {number} endX - * @param {number} endY - * @param {number} amount - * @param {number} [intensity] - * @returns {{x: number, y: number}} - */ - function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) { - const dx = endX - startX; - const dy = endY - startY; - const distance = Math.sqrt(dx * dx + dy * dy); - const angle = Math.atan2(dy, dx); - const midX = startX + Math.cos(angle) * distance / 2; - const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity; - const t = amount; - const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX; - const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY; - return { x, y }; - } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - - // Run the birb - init(); - draw(); -}).catch((e) => { - error("Error while loading sprite sheets: ", e); -}); - -/** - * @returns {boolean} Whether the user is on a mobile device - */ -function isMobile() { - return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); -} - -function log() { - console.log("Birb: ", ...arguments); -} - -function debug() { - if (debugMode) { - console.debug("Birb: ", ...arguments); - } -} - -function error() { - console.error("Birb: ", ...arguments); -} \ No newline at end of file +})(); diff --git a/manifest.json b/manifest.json index 519db20..cc961e7 100644 --- a/manifest.json +++ b/manifest.json @@ -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": [ { diff --git a/package-lock.json b/package-lock.json index 9acd8f4..90cd936 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4b4f18e..3b6afda 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/Frame.js b/src/Frame.js new file mode 100644 index 0000000..1310561 --- /dev/null +++ b/src/Frame.js @@ -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} */ + 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; \ No newline at end of file diff --git a/src/Layer.js b/src/Layer.js new file mode 100644 index 0000000..84c6661 --- /dev/null +++ b/src/Layer.js @@ -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; \ No newline at end of file diff --git a/birb.js b/src/birb.js similarity index 92% rename from birb.js rename to src/birb.js index 3f00d3e..bef0e6c 100644 --- a/birb.js +++ b/src/birb.js @@ -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} */ - 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} */ -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} 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} */ - this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; - this.tags = tags; - } -} - /** @type {Record} */ 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) { diff --git a/src/birdType.js b/src/birdType.js new file mode 100644 index 0000000..d2629f5 --- /dev/null +++ b/src/birdType.js @@ -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} 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} */ + this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.tags = tags; + } +} + +export default BirdType; \ No newline at end of file diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..5c064b2 --- /dev/null +++ b/src/constants.js @@ -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} */ +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, +}; \ No newline at end of file From eda3f9fbc158589c0f05160a72fa7d67e68d24c0 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 13:22:24 -0400 Subject: [PATCH 02/16] Change files to use a consistent case --- src/Frame.js | 2 +- src/birb.js | 4 +-- src/frame.js | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/layer.js | 14 ++++++++++ 4 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 src/frame.js create mode 100644 src/layer.js diff --git a/src/Frame.js b/src/Frame.js index 1310561..14ecb33 100644 --- a/src/Frame.js +++ b/src/Frame.js @@ -1,6 +1,6 @@ // @ts-check import { TRANSPARENT, Directions } from './constants.js'; -import Layer from './Layer.js'; +import Layer from './layer.js'; import BirdType from './birdType.js'; class Frame { diff --git a/src/birb.js b/src/birb.js index bef0e6c..0b9d39d 100644 --- a/src/birb.js +++ b/src/birb.js @@ -23,8 +23,8 @@ import { Directions } from './constants.js'; -import Frame from './Frame.js'; -import Layer from './Layer.js'; +import Frame from './frame.js'; +import Layer from './layer.js'; import BirdType from './birdType.js'; // @ts-ignore diff --git a/src/frame.js b/src/frame.js new file mode 100644 index 0000000..14ecb33 --- /dev/null +++ b/src/frame.js @@ -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} */ + 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; \ No newline at end of file diff --git a/src/layer.js b/src/layer.js new file mode 100644 index 0000000..84c6661 --- /dev/null +++ b/src/layer.js @@ -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; \ No newline at end of file From 4d12eb46a29afe293567daee3f49e3ee65d45a12 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 13:47:22 -0400 Subject: [PATCH 03/16] Reorganize components --- dist/birb.js | 413 ++++++++++++++++++++-------------------- dist/birb.user.js | 415 +++++++++++++++++++++-------------------- manifest.json | 2 +- src/Frame.js | 8 +- src/birb.js | 179 ++---------------- src/birdType.js | 47 ----- src/constants.js | 47 ----- src/frame.js | 8 +- src/sharedConstants.js | 6 + src/sprites.js | 201 ++++++++++++++++++++ 10 files changed, 650 insertions(+), 676 deletions(-) delete mode 100644 src/birdType.js delete mode 100644 src/constants.js create mode 100644 src/sharedConstants.js create mode 100644 src/sprites.js diff --git a/dist/birb.js b/dist/birb.js index 3d88a7c..e6b8f0c 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -3,47 +3,6 @@ // @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} */ - 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, @@ -51,19 +10,48 @@ // @ts-check - class Layer { - /** - * @param {string[][]} pixels - * @param {string} [tag] - */ - constructor(pixels, tag = "default") { - this.pixels = pixels; - this.tag = tag; - } - } - - // @ts-check + // Sprite theme color indicators + 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} */ + 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, + }; class BirdType { /** @@ -76,24 +64,165 @@ 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, + [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} */ - this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.tags = tags; } } + /** @type {Record} */ + 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", + }), + }; + + // @ts-check + + class Layer { + /** + * @param {string[][]} pixels + * @param {string} [tag] + */ + constructor(pixels, tag = "default") { + this.pixels = pixels; + this.tag = tag; + } + } + // @ts-check class Frame { @@ -119,7 +248,7 @@ 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)); + this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT)); } // Combine layers for (let i = 1; i < layers.length; i++) { @@ -128,7 +257,7 @@ 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.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; } } } @@ -609,134 +738,6 @@ } } - /** @type {Record} */ - const species = { - bluebird: new BirdType("Eastern Bluebird", - "Native to North American and very social, though can be timid around people.", { - [FOOT]: "#af8e75", - [FACE]: "#639bff", - [BELLY]: "#f8b143", - [UNDERBELLY]: "#ec8637", - [WING]: "#578ae6", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#ffffff", - [BELLY]: "#ebe9e8", - [UNDERBELLY]: "#ebd9d0", - [WING]: "#f3d3c1", - [WING_EDGE]: "#2d2d2dff", - [THEME_HIGHLIGHT]: "#d7ac93", - }), - tuftedTitmouse: new BirdType("Tufted Titmouse", - "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", { - [FOOT]: "#af8e75", - [FACE]: "#c7cad7", - [BELLY]: "#e4e5eb", - [UNDERBELLY]: "#d7cfcb", - [WING]: "#b1b5c5", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#ffaf34", - [HOOD]: "#aaa094", - [BELLY]: "#ffaf34", - [UNDERBELLY]: "#babec2", - [WING]: "#aaa094", - [WING_EDGE]: "#888580", - [THEME_HIGHLIGHT]: "#ffaf34", - }), - redCardinal: new BirdType("Red Cardinal", - "Native to the eastern United States, this strikingly red bird is hard to miss.", { - [BEAK]: "#d93619", - [FOOT]: "#af8e75", - [FACE]: "#31353d", - [HOOD]: "#e83a1b", - [BELLY]: "#e83a1b", - [UNDERBELLY]: "#dc3719", - [WING]: "#d23215", - [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.", { - [BEAK]: "#ffaf34", - [FOOT]: "#af8e75", - [FACE]: "#fff255", - [NOSE]: "#383838", - [HOOD]: "#383838", - [BELLY]: "#fff255", - [UNDERBELLY]: "#f5ea63", - [WING]: "#e8e079", - [WING_EDGE]: "#191919", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#db7c4d", - [BELLY]: "#f7e1c9", - [UNDERBELLY]: "#ebc9a3", - [WING]: "#2252a9", - [WING_EDGE]: "#1c448b", - [HOOD]: "#2252a9", - }), - mistletoebird: new BirdType("Mistletoebird", - "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", { - [FOOT]: "#6c6a7c", - [FACE]: "#352e6d", - [BELLY]: "#fd6833", - [UNDERBELLY]: "#e6e1d8", - [WING]: "#342b7c", - [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.", { - [BEAK]: "#f71919", - [FOOT]: "#af7575", - [FACE]: "#cb092b", - [BELLY]: "#ae1724", - [UNDERBELLY]: "#831b24", - [WING]: "#7e3030", - [WING_EDGE]: "#490f0f", - }), - scarletRobin: new BirdType("Scarlet Robin", - "Native to Australia, this striking robin can be found in Eucalyptus forests.", { - [FOOT]: "#494949", - [FACE]: "#3d3d3d", - [BELLY]: "#fc5633", - [UNDERBELLY]: "#dcdcdc", - [WING]: "#2b2b2b", - [WING_EDGE]: "#ebebeb", - [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.", { - [BEAK]: "#e89f30", - [FOOT]: "#9f8075", - [FACE]: "#2d2d2d", - [BELLY]: "#eb7a3a", - [UNDERBELLY]: "#eb7a3a", - [WING]: "#444444", - [WING_EDGE]: "#232323", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#edc7a9", - [NOSE]: "#f7eee5", - [HOOD]: "#c58a5b", - [BELLY]: "#e1b796", - [UNDERBELLY]: "#c79e7c", - [WING]: "#c58a5b", - [WING_EDGE]: "#866348", - }), - }; - const DEFAULT_BIRD = "bluebird"; const SPRITE_WIDTH = 32; @@ -808,7 +809,7 @@ const b = pixels[index + 2]; const a = pixels[index + 3]; if (a === 0) { - row.push(TRANSPARENT); + row.push(Sprite.TRANSPARENT); continue; } const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; @@ -818,7 +819,7 @@ } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { error(`Unknown color: ${hex}`); - row.push(TRANSPARENT); + row.push(Sprite.TRANSPARENT); } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } @@ -839,7 +840,7 @@ loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) ]).then(([birbPixels, decorationPixels, featherPixels]) => { - + const SPRITE_SHEET = birbPixels; const DECORATIONS_SPRITE_SHEET = decorationPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -991,7 +992,7 @@ }), new DebugMenuItem("Reset Data", resetSaveData), new DebugMenuItem("Unlock All", () => { - for (let type in species) { + for (let type in SPECIES) { unlockBird(type); } }), @@ -1327,7 +1328,7 @@ } ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) { + if (currentAnimation.draw(ctx, direction, animStart, SPECIES[currentSpecies])) { setAnimation(Animations.STILL); } @@ -1517,7 +1518,7 @@ if (document.querySelector("#" + FEATHER_ID)) { return; } - const speciesToUnlock = Object.keys(species).filter((species) => !unlockedSpecies.includes(species)); + const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); if (speciesToUnlock.length === 0) { // No more species to unlock return; @@ -1530,7 +1531,7 @@ * @param {string} birdType */ function insertFeather(birdType) { - let type = species[birdType]; + let type = SPECIES[birdType]; const featherCanvas = document.createElement("canvas"); featherCanvas.id = FEATHER_ID; featherCanvas.classList.add("birb-decoration"); @@ -1568,7 +1569,7 @@ function unlockBird(birdType) { if (!unlockedSpecies.includes(birdType)) { unlockedSpecies.push(birdType); - insertModal("New Bird Unlocked!", `You've found a ${species[birdType].name} feather! Use the Field Guide to switch your bird's species.`); + insertModal("New Bird Unlocked!", `You've found a ${SPECIES[birdType].name} feather! Use the Field Guide to switch your bird's species.`); } save(); } @@ -1644,7 +1645,7 @@ content.innerHTML = ""; const generateDescription = (/** @type {string} */ speciesId) => { - const type = species[speciesId]; + const type = SPECIES[speciesId]; const unlocked = unlockedSpecies.includes(speciesId); return "" + type.name + "
" + (!unlocked ? "Not yet unlocked" : type.description); }; @@ -1654,7 +1655,7 @@ return; } description.innerHTML = generateDescription(currentSpecies); - for (const [id, type] of Object.entries(species)) { + for (const [id, type] of Object.entries(SPECIES)) { const unlocked = unlockedSpecies.includes(id); const speciesElement = makeElement("birb-grid-item"); if (id === currentSpecies) { @@ -1723,7 +1724,7 @@ function switchSpecies(type) { currentSpecies = type; // Update CSS variable --birb-highlight to be wing color - document.documentElement.style.setProperty("--birb-highlight", species[type].colors[THEME_HIGHLIGHT]); + document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[Sprite.THEME_HIGHLIGHT]); save(); } diff --git a/dist/birb.user.js b/dist/birb.user.js index 4af6af2..a524d73 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.16 +// @version 2025.10.26.37 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -17,47 +17,6 @@ // @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} */ - 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, @@ -65,19 +24,48 @@ // @ts-check - class Layer { - /** - * @param {string[][]} pixels - * @param {string} [tag] - */ - constructor(pixels, tag = "default") { - this.pixels = pixels; - this.tag = tag; - } - } - - // @ts-check + // Sprite theme color indicators + 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} */ + 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, + }; class BirdType { /** @@ -90,24 +78,165 @@ 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, + [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} */ - this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.tags = tags; } } + /** @type {Record} */ + 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", + }), + }; + + // @ts-check + + class Layer { + /** + * @param {string[][]} pixels + * @param {string} [tag] + */ + constructor(pixels, tag = "default") { + this.pixels = pixels; + this.tag = tag; + } + } + // @ts-check class Frame { @@ -133,7 +262,7 @@ 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)); + this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT)); } // Combine layers for (let i = 1; i < layers.length; i++) { @@ -142,7 +271,7 @@ 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.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; } } } @@ -623,134 +752,6 @@ } } - /** @type {Record} */ - const species = { - bluebird: new BirdType("Eastern Bluebird", - "Native to North American and very social, though can be timid around people.", { - [FOOT]: "#af8e75", - [FACE]: "#639bff", - [BELLY]: "#f8b143", - [UNDERBELLY]: "#ec8637", - [WING]: "#578ae6", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#ffffff", - [BELLY]: "#ebe9e8", - [UNDERBELLY]: "#ebd9d0", - [WING]: "#f3d3c1", - [WING_EDGE]: "#2d2d2dff", - [THEME_HIGHLIGHT]: "#d7ac93", - }), - tuftedTitmouse: new BirdType("Tufted Titmouse", - "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", { - [FOOT]: "#af8e75", - [FACE]: "#c7cad7", - [BELLY]: "#e4e5eb", - [UNDERBELLY]: "#d7cfcb", - [WING]: "#b1b5c5", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#ffaf34", - [HOOD]: "#aaa094", - [BELLY]: "#ffaf34", - [UNDERBELLY]: "#babec2", - [WING]: "#aaa094", - [WING_EDGE]: "#888580", - [THEME_HIGHLIGHT]: "#ffaf34", - }), - redCardinal: new BirdType("Red Cardinal", - "Native to the eastern United States, this strikingly red bird is hard to miss.", { - [BEAK]: "#d93619", - [FOOT]: "#af8e75", - [FACE]: "#31353d", - [HOOD]: "#e83a1b", - [BELLY]: "#e83a1b", - [UNDERBELLY]: "#dc3719", - [WING]: "#d23215", - [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.", { - [BEAK]: "#ffaf34", - [FOOT]: "#af8e75", - [FACE]: "#fff255", - [NOSE]: "#383838", - [HOOD]: "#383838", - [BELLY]: "#fff255", - [UNDERBELLY]: "#f5ea63", - [WING]: "#e8e079", - [WING_EDGE]: "#191919", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#db7c4d", - [BELLY]: "#f7e1c9", - [UNDERBELLY]: "#ebc9a3", - [WING]: "#2252a9", - [WING_EDGE]: "#1c448b", - [HOOD]: "#2252a9", - }), - mistletoebird: new BirdType("Mistletoebird", - "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", { - [FOOT]: "#6c6a7c", - [FACE]: "#352e6d", - [BELLY]: "#fd6833", - [UNDERBELLY]: "#e6e1d8", - [WING]: "#342b7c", - [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.", { - [BEAK]: "#f71919", - [FOOT]: "#af7575", - [FACE]: "#cb092b", - [BELLY]: "#ae1724", - [UNDERBELLY]: "#831b24", - [WING]: "#7e3030", - [WING_EDGE]: "#490f0f", - }), - scarletRobin: new BirdType("Scarlet Robin", - "Native to Australia, this striking robin can be found in Eucalyptus forests.", { - [FOOT]: "#494949", - [FACE]: "#3d3d3d", - [BELLY]: "#fc5633", - [UNDERBELLY]: "#dcdcdc", - [WING]: "#2b2b2b", - [WING_EDGE]: "#ebebeb", - [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.", { - [BEAK]: "#e89f30", - [FOOT]: "#9f8075", - [FACE]: "#2d2d2d", - [BELLY]: "#eb7a3a", - [UNDERBELLY]: "#eb7a3a", - [WING]: "#444444", - [WING_EDGE]: "#232323", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#edc7a9", - [NOSE]: "#f7eee5", - [HOOD]: "#c58a5b", - [BELLY]: "#e1b796", - [UNDERBELLY]: "#c79e7c", - [WING]: "#c58a5b", - [WING_EDGE]: "#866348", - }), - }; - const DEFAULT_BIRD = "bluebird"; const SPRITE_WIDTH = 32; @@ -822,7 +823,7 @@ const b = pixels[index + 2]; const a = pixels[index + 3]; if (a === 0) { - row.push(TRANSPARENT); + row.push(Sprite.TRANSPARENT); continue; } const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; @@ -832,7 +833,7 @@ } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { error(`Unknown color: ${hex}`); - row.push(TRANSPARENT); + row.push(Sprite.TRANSPARENT); } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } @@ -853,7 +854,7 @@ loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) ]).then(([birbPixels, decorationPixels, featherPixels]) => { - + const SPRITE_SHEET = birbPixels; const DECORATIONS_SPRITE_SHEET = decorationPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -1005,7 +1006,7 @@ }), new DebugMenuItem("Reset Data", resetSaveData), new DebugMenuItem("Unlock All", () => { - for (let type in species) { + for (let type in SPECIES) { unlockBird(type); } }), @@ -1341,7 +1342,7 @@ } ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) { + if (currentAnimation.draw(ctx, direction, animStart, SPECIES[currentSpecies])) { setAnimation(Animations.STILL); } @@ -1531,7 +1532,7 @@ if (document.querySelector("#" + FEATHER_ID)) { return; } - const speciesToUnlock = Object.keys(species).filter((species) => !unlockedSpecies.includes(species)); + const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); if (speciesToUnlock.length === 0) { // No more species to unlock return; @@ -1544,7 +1545,7 @@ * @param {string} birdType */ function insertFeather(birdType) { - let type = species[birdType]; + let type = SPECIES[birdType]; const featherCanvas = document.createElement("canvas"); featherCanvas.id = FEATHER_ID; featherCanvas.classList.add("birb-decoration"); @@ -1582,7 +1583,7 @@ function unlockBird(birdType) { if (!unlockedSpecies.includes(birdType)) { unlockedSpecies.push(birdType); - insertModal("New Bird Unlocked!", `You've found a ${species[birdType].name} feather! Use the Field Guide to switch your bird's species.`); + insertModal("New Bird Unlocked!", `You've found a ${SPECIES[birdType].name} feather! Use the Field Guide to switch your bird's species.`); } save(); } @@ -1658,7 +1659,7 @@ content.innerHTML = ""; const generateDescription = (/** @type {string} */ speciesId) => { - const type = species[speciesId]; + const type = SPECIES[speciesId]; const unlocked = unlockedSpecies.includes(speciesId); return "" + type.name + "
" + (!unlocked ? "Not yet unlocked" : type.description); }; @@ -1668,7 +1669,7 @@ return; } description.innerHTML = generateDescription(currentSpecies); - for (const [id, type] of Object.entries(species)) { + for (const [id, type] of Object.entries(SPECIES)) { const unlocked = unlockedSpecies.includes(id); const speciesElement = makeElement("birb-grid-item"); if (id === currentSpecies) { @@ -1737,7 +1738,7 @@ function switchSpecies(type) { currentSpecies = type; // Update CSS variable --birb-highlight to be wing color - document.documentElement.style.setProperty("--birb-highlight", species[type].colors[THEME_HIGHLIGHT]); + document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[Sprite.THEME_HIGHLIGHT]); save(); } diff --git a/manifest.json b/manifest.json index cc961e7..c4161df 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.16", + "version": "2025.10.26.37", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/Frame.js b/src/Frame.js index 14ecb33..2f57faa 100644 --- a/src/Frame.js +++ b/src/Frame.js @@ -1,7 +1,7 @@ // @ts-check -import { TRANSPARENT, Directions } from './constants.js'; +import { Directions } from './sharedConstants.js'; +import { Sprite, BirdType } from './sprites.js'; import Layer from './layer.js'; -import BirdType from './birdType.js'; class Frame { @@ -26,7 +26,7 @@ class Frame { 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)); + this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT)); } // Combine layers for (let i = 1; i < layers.length; i++) { @@ -35,7 +35,7 @@ class Frame { 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.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; } } } diff --git a/src/birb.js b/src/birb.js index 0b9d39d..085388d 100644 --- a/src/birb.js +++ b/src/birb.js @@ -1,31 +1,18 @@ // @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'; +} from './sharedConstants.js'; + +import { + Sprite, + SPRITE_SHEET_COLOR_MAP, + SPECIES, + BirdType +} from './sprites.js'; import Frame from './frame.js'; import Layer from './layer.js'; -import BirdType from './birdType.js'; // @ts-ignore const SHARED_CONFIG = { @@ -128,134 +115,6 @@ class Anim { } } -/** @type {Record} */ -const species = { - bluebird: new BirdType("Eastern Bluebird", - "Native to North American and very social, though can be timid around people.", { - [FOOT]: "#af8e75", - [FACE]: "#639bff", - [BELLY]: "#f8b143", - [UNDERBELLY]: "#ec8637", - [WING]: "#578ae6", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#ffffff", - [BELLY]: "#ebe9e8", - [UNDERBELLY]: "#ebd9d0", - [WING]: "#f3d3c1", - [WING_EDGE]: "#2d2d2dff", - [THEME_HIGHLIGHT]: "#d7ac93", - }), - tuftedTitmouse: new BirdType("Tufted Titmouse", - "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", { - [FOOT]: "#af8e75", - [FACE]: "#c7cad7", - [BELLY]: "#e4e5eb", - [UNDERBELLY]: "#d7cfcb", - [WING]: "#b1b5c5", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#ffaf34", - [HOOD]: "#aaa094", - [BELLY]: "#ffaf34", - [UNDERBELLY]: "#babec2", - [WING]: "#aaa094", - [WING_EDGE]: "#888580", - [THEME_HIGHLIGHT]: "#ffaf34", - }), - redCardinal: new BirdType("Red Cardinal", - "Native to the eastern United States, this strikingly red bird is hard to miss.", { - [BEAK]: "#d93619", - [FOOT]: "#af8e75", - [FACE]: "#31353d", - [HOOD]: "#e83a1b", - [BELLY]: "#e83a1b", - [UNDERBELLY]: "#dc3719", - [WING]: "#d23215", - [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.", { - [BEAK]: "#ffaf34", - [FOOT]: "#af8e75", - [FACE]: "#fff255", - [NOSE]: "#383838", - [HOOD]: "#383838", - [BELLY]: "#fff255", - [UNDERBELLY]: "#f5ea63", - [WING]: "#e8e079", - [WING_EDGE]: "#191919", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#db7c4d", - [BELLY]: "#f7e1c9", - [UNDERBELLY]: "#ebc9a3", - [WING]: "#2252a9", - [WING_EDGE]: "#1c448b", - [HOOD]: "#2252a9", - }), - mistletoebird: new BirdType("Mistletoebird", - "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", { - [FOOT]: "#6c6a7c", - [FACE]: "#352e6d", - [BELLY]: "#fd6833", - [UNDERBELLY]: "#e6e1d8", - [WING]: "#342b7c", - [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.", { - [BEAK]: "#f71919", - [FOOT]: "#af7575", - [FACE]: "#cb092b", - [BELLY]: "#ae1724", - [UNDERBELLY]: "#831b24", - [WING]: "#7e3030", - [WING_EDGE]: "#490f0f", - }), - scarletRobin: new BirdType("Scarlet Robin", - "Native to Australia, this striking robin can be found in Eucalyptus forests.", { - [FOOT]: "#494949", - [FACE]: "#3d3d3d", - [BELLY]: "#fc5633", - [UNDERBELLY]: "#dcdcdc", - [WING]: "#2b2b2b", - [WING_EDGE]: "#ebebeb", - [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.", { - [BEAK]: "#e89f30", - [FOOT]: "#9f8075", - [FACE]: "#2d2d2d", - [BELLY]: "#eb7a3a", - [UNDERBELLY]: "#eb7a3a", - [WING]: "#444444", - [WING_EDGE]: "#232323", - [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.", { - [FOOT]: "#af8e75", - [FACE]: "#edc7a9", - [NOSE]: "#f7eee5", - [HOOD]: "#c58a5b", - [BELLY]: "#e1b796", - [UNDERBELLY]: "#c79e7c", - [WING]: "#c58a5b", - [WING_EDGE]: "#866348", - }), -}; - const DEFAULT_BIRD = "bluebird"; const SPRITE_WIDTH = 32; @@ -327,7 +186,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) { const b = pixels[index + 2]; const a = pixels[index + 3]; if (a === 0) { - row.push(TRANSPARENT); + row.push(Sprite.TRANSPARENT); continue; } const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; @@ -337,7 +196,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) { } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { error(`Unknown color: ${hex}`); - row.push(TRANSPARENT); + row.push(Sprite.TRANSPARENT); } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } @@ -358,7 +217,7 @@ Promise.all([ loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) ]).then(([birbPixels, decorationPixels, featherPixels]) => { - + const SPRITE_SHEET = birbPixels; const DECORATIONS_SPRITE_SHEET = decorationPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -518,7 +377,7 @@ Promise.all([ }), new DebugMenuItem("Reset Data", resetSaveData), new DebugMenuItem("Unlock All", () => { - for (let type in species) { + for (let type in SPECIES) { unlockBird(type); } }), @@ -854,7 +713,7 @@ Promise.all([ } ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) { + if (currentAnimation.draw(ctx, direction, animStart, SPECIES[currentSpecies])) { setAnimation(Animations.STILL); } @@ -1064,7 +923,7 @@ Promise.all([ if (document.querySelector("#" + FEATHER_ID)) { return; } - const speciesToUnlock = Object.keys(species).filter((species) => !unlockedSpecies.includes(species)); + const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); if (speciesToUnlock.length === 0) { // No more species to unlock return; @@ -1077,7 +936,7 @@ Promise.all([ * @param {string} birdType */ function insertFeather(birdType) { - let type = species[birdType]; + let type = SPECIES[birdType]; const featherCanvas = document.createElement("canvas"); featherCanvas.id = FEATHER_ID; featherCanvas.classList.add("birb-decoration"); @@ -1115,7 +974,7 @@ Promise.all([ function unlockBird(birdType) { if (!unlockedSpecies.includes(birdType)) { unlockedSpecies.push(birdType); - insertModal("New Bird Unlocked!", `You've found a ${species[birdType].name} feather! Use the Field Guide to switch your bird's species.`); + insertModal("New Bird Unlocked!", `You've found a ${SPECIES[birdType].name} feather! Use the Field Guide to switch your bird's species.`); } save(); } @@ -1191,7 +1050,7 @@ Promise.all([ content.innerHTML = ""; const generateDescription = (/** @type {string} */ speciesId) => { - const type = species[speciesId]; + const type = SPECIES[speciesId]; const unlocked = unlockedSpecies.includes(speciesId); return "" + type.name + "
" + (!unlocked ? "Not yet unlocked" : type.description); }; @@ -1201,7 +1060,7 @@ Promise.all([ return; } description.innerHTML = generateDescription(currentSpecies); - for (const [id, type] of Object.entries(species)) { + for (const [id, type] of Object.entries(SPECIES)) { const unlocked = unlockedSpecies.includes(id); const speciesElement = makeElement("birb-grid-item"); if (id === currentSpecies) { @@ -1274,7 +1133,7 @@ Promise.all([ function switchSpecies(type) { currentSpecies = type; // Update CSS variable --birb-highlight to be wing color - document.documentElement.style.setProperty("--birb-highlight", species[type].colors[THEME_HIGHLIGHT]); + document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[Sprite.THEME_HIGHLIGHT]); save(); } diff --git a/src/birdType.js b/src/birdType.js deleted file mode 100644 index d2629f5..0000000 --- a/src/birdType.js +++ /dev/null @@ -1,47 +0,0 @@ -// @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} 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} */ - this.colors = { ...defaultColors, ...colors, [THEME_HIGHLIGHT]: colors[THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; - this.tags = tags; - } -} - -export default BirdType; \ No newline at end of file diff --git a/src/constants.js b/src/constants.js deleted file mode 100644 index 5c064b2..0000000 --- a/src/constants.js +++ /dev/null @@ -1,47 +0,0 @@ -// @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} */ -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, -}; \ No newline at end of file diff --git a/src/frame.js b/src/frame.js index 14ecb33..2f57faa 100644 --- a/src/frame.js +++ b/src/frame.js @@ -1,7 +1,7 @@ // @ts-check -import { TRANSPARENT, Directions } from './constants.js'; +import { Directions } from './sharedConstants.js'; +import { Sprite, BirdType } from './sprites.js'; import Layer from './layer.js'; -import BirdType from './birdType.js'; class Frame { @@ -26,7 +26,7 @@ class Frame { 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)); + this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT)); } // Combine layers for (let i = 1; i < layers.length; i++) { @@ -35,7 +35,7 @@ class Frame { 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.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; } } } diff --git a/src/sharedConstants.js b/src/sharedConstants.js new file mode 100644 index 0000000..603675f --- /dev/null +++ b/src/sharedConstants.js @@ -0,0 +1,6 @@ +// @ts-check + +export const Directions = { + LEFT: -1, + RIGHT: 1, +}; \ No newline at end of file diff --git a/src/sprites.js b/src/sprites.js new file mode 100644 index 0000000..3ec7b06 --- /dev/null +++ b/src/sprites.js @@ -0,0 +1,201 @@ +// @ts-check + +// Sprite theme color indicators +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} */ +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} 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} */ + this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.tags = tags; + } +} + +/** @type {Record} */ +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", + }), +}; \ No newline at end of file From b1a4dfac7ca63802d205e671cedcdf120bb707ab Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 13:52:13 -0400 Subject: [PATCH 04/16] Add jsconfig file --- dist/birb.js | 15 +-------------- dist/birb.user.js | 17 ++--------------- jsconfig.json | 7 +++++++ manifest.json | 2 +- src/Frame.js | 1 - src/Layer.js | 2 -- src/birb.js | 4 ---- src/frame.js | 1 - src/layer.js | 2 -- src/sharedConstants.js | 2 -- src/sprites.js | 4 +--- 11 files changed, 12 insertions(+), 45 deletions(-) create mode 100644 jsconfig.json diff --git a/dist/birb.js b/dist/birb.js index e6b8f0c..c62d152 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -1,16 +1,12 @@ (function () { 'use strict'; - // @ts-check - const Directions = { LEFT: -1, RIGHT: 1, }; - // @ts-check - - // Sprite theme color indicators + /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", TRANSPARENT: "transparent", @@ -210,8 +206,6 @@ }), }; - // @ts-check - class Layer { /** * @param {string[][]} pixels @@ -223,8 +217,6 @@ } } - // @ts-check - class Frame { /** @type {{ [tag: string]: string[][] }} */ @@ -291,9 +283,6 @@ } } } } - // @ts-check - - // @ts-ignore const SHARED_CONFIG = { birbCssScale: 1, @@ -1017,7 +1006,6 @@ const canvas = document.createElement("canvas"); /** @type {CanvasRenderingContext2D} */ - // @ts-expect-error const ctx = canvas.getContext("2d"); const States = { @@ -1389,7 +1377,6 @@ const textarea = noteElement.querySelector(".birb-sticky-note-input"); if (textarea && textarea instanceof HTMLTextAreaElement) { - /** @type {NodeJS.Timeout | undefined} */ let saveTimeout; // Save after debounce textarea.addEventListener("input", () => { diff --git a/dist/birb.user.js b/dist/birb.user.js index a524d73..6ea24a4 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.37 +// @version 2025.10.26.43 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -15,16 +15,12 @@ (function () { 'use strict'; - // @ts-check - const Directions = { LEFT: -1, RIGHT: 1, }; - // @ts-check - - // Sprite theme color indicators + /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", TRANSPARENT: "transparent", @@ -224,8 +220,6 @@ }), }; - // @ts-check - class Layer { /** * @param {string[][]} pixels @@ -237,8 +231,6 @@ } } - // @ts-check - class Frame { /** @type {{ [tag: string]: string[][] }} */ @@ -305,9 +297,6 @@ } } } } - // @ts-check - - // @ts-ignore const SHARED_CONFIG = { birbCssScale: 1, @@ -1031,7 +1020,6 @@ const canvas = document.createElement("canvas"); /** @type {CanvasRenderingContext2D} */ - // @ts-expect-error const ctx = canvas.getContext("2d"); const States = { @@ -1403,7 +1391,6 @@ const textarea = noteElement.querySelector(".birb-sticky-note-input"); if (textarea && textarea instanceof HTMLTextAreaElement) { - /** @type {NodeJS.Timeout | undefined} */ let saveTimeout; // Save after debounce textarea.addEventListener("input", () => { diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..3eb5dfa --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "checkJs": true, + "target": "es2017" + }, + "include": ["src"] +} \ No newline at end of file diff --git a/manifest.json b/manifest.json index c4161df..f5cd6bc 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.37", + "version": "2025.10.26.43", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/Frame.js b/src/Frame.js index 2f57faa..2400ec2 100644 --- a/src/Frame.js +++ b/src/Frame.js @@ -1,4 +1,3 @@ -// @ts-check import { Directions } from './sharedConstants.js'; import { Sprite, BirdType } from './sprites.js'; import Layer from './layer.js'; diff --git a/src/Layer.js b/src/Layer.js index 84c6661..cd33990 100644 --- a/src/Layer.js +++ b/src/Layer.js @@ -1,5 +1,3 @@ -// @ts-check - class Layer { /** * @param {string[][]} pixels diff --git a/src/birb.js b/src/birb.js index 085388d..14c6962 100644 --- a/src/birb.js +++ b/src/birb.js @@ -1,5 +1,3 @@ -// @ts-check - import { Directions } from './sharedConstants.js'; @@ -402,7 +400,6 @@ Promise.all([ const canvas = document.createElement("canvas"); /** @type {CanvasRenderingContext2D} */ - // @ts-expect-error const ctx = canvas.getContext("2d"); const States = { @@ -774,7 +771,6 @@ Promise.all([ const textarea = noteElement.querySelector(".birb-sticky-note-input"); if (textarea && textarea instanceof HTMLTextAreaElement) { - /** @type {NodeJS.Timeout | undefined} */ let saveTimeout; // Save after debounce textarea.addEventListener("input", () => { diff --git a/src/frame.js b/src/frame.js index 2f57faa..2400ec2 100644 --- a/src/frame.js +++ b/src/frame.js @@ -1,4 +1,3 @@ -// @ts-check import { Directions } from './sharedConstants.js'; import { Sprite, BirdType } from './sprites.js'; import Layer from './layer.js'; diff --git a/src/layer.js b/src/layer.js index 84c6661..cd33990 100644 --- a/src/layer.js +++ b/src/layer.js @@ -1,5 +1,3 @@ -// @ts-check - class Layer { /** * @param {string[][]} pixels diff --git a/src/sharedConstants.js b/src/sharedConstants.js index 603675f..5bd42fa 100644 --- a/src/sharedConstants.js +++ b/src/sharedConstants.js @@ -1,5 +1,3 @@ -// @ts-check - export const Directions = { LEFT: -1, RIGHT: 1, diff --git a/src/sprites.js b/src/sprites.js index 3ec7b06..14884d4 100644 --- a/src/sprites.js +++ b/src/sprites.js @@ -1,6 +1,4 @@ -// @ts-check - -// Sprite theme color indicators +/** Indicators for parts of the base bird sprite sheet */ export const Sprite = { THEME_HIGHLIGHT: "theme-highlight", TRANSPARENT: "transparent", From a9f386b6bdc979e398e8fa0243ff776693ae2cad Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 13:52:54 -0400 Subject: [PATCH 05/16] Rename sprite part constants --- dist/birb.js | 246 ++++++++++++++++++++++----------------------- dist/birb.user.js | 248 +++++++++++++++++++++++----------------------- manifest.json | 2 +- src/Frame.js | 6 +- src/birb.js | 8 +- src/frame.js | 6 +- src/sprites.js | 236 +++++++++++++++++++++---------------------- 7 files changed, 376 insertions(+), 376 deletions(-) diff --git a/dist/birb.js b/dist/birb.js index c62d152..799d569 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -7,7 +7,7 @@ }; /** Indicators for parts of the base bird sprite sheet */ - const Sprite = { + const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", TRANSPARENT: "transparent", OUTLINE: "outline", @@ -30,23 +30,23 @@ /** @type {Record} */ 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, + "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, }; class BirdType { @@ -60,20 +60,20 @@ 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, + [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} */ - this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.colors = { ...defaultColors, ...colors, [SPRITE.THEME_HIGHLIGHT]: colors[SPRITE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.tags = tags; } } @@ -82,127 +82,127 @@ 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", + [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", + [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", + [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", + [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", + [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" + [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", + [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", + [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", + [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", + [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", + [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", + [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", }), }; @@ -240,7 +240,7 @@ 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)); + this.pixels.unshift(new Array(this.pixels[0].length).fill(SPRITE.TRANSPARENT)); } // Combine layers for (let i = 1; i < layers.length; i++) { @@ -249,7 +249,7 @@ 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.pixels[y + topMargin][x] = layerPixels[y][x] !== SPRITE.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; } } } @@ -798,7 +798,7 @@ const b = pixels[index + 2]; const a = pixels[index + 3]; if (a === 0) { - row.push(Sprite.TRANSPARENT); + row.push(SPRITE.TRANSPARENT); continue; } const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; @@ -808,7 +808,7 @@ } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); + row.push(SPRITE.TRANSPARENT); } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } @@ -1711,7 +1711,7 @@ function switchSpecies(type) { currentSpecies = type; // Update CSS variable --birb-highlight to be wing color - document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[Sprite.THEME_HIGHLIGHT]); + document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[SPRITE.THEME_HIGHLIGHT]); save(); } diff --git a/dist/birb.user.js b/dist/birb.user.js index 6ea24a4..062d19f 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.43 +// @version 2025.10.26.44 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -21,7 +21,7 @@ }; /** Indicators for parts of the base bird sprite sheet */ - const Sprite = { + const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", TRANSPARENT: "transparent", OUTLINE: "outline", @@ -44,23 +44,23 @@ /** @type {Record} */ 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, + "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, }; class BirdType { @@ -74,20 +74,20 @@ 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, + [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} */ - this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.colors = { ...defaultColors, ...colors, [SPRITE.THEME_HIGHLIGHT]: colors[SPRITE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.tags = tags; } } @@ -96,127 +96,127 @@ 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", + [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", + [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", + [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", + [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", + [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" + [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", + [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", + [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", + [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", + [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", + [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", + [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", }), }; @@ -254,7 +254,7 @@ 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)); + this.pixels.unshift(new Array(this.pixels[0].length).fill(SPRITE.TRANSPARENT)); } // Combine layers for (let i = 1; i < layers.length; i++) { @@ -263,7 +263,7 @@ 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.pixels[y + topMargin][x] = layerPixels[y][x] !== SPRITE.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; } } } @@ -812,7 +812,7 @@ const b = pixels[index + 2]; const a = pixels[index + 3]; if (a === 0) { - row.push(Sprite.TRANSPARENT); + row.push(SPRITE.TRANSPARENT); continue; } const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; @@ -822,7 +822,7 @@ } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); + row.push(SPRITE.TRANSPARENT); } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } @@ -1725,7 +1725,7 @@ function switchSpecies(type) { currentSpecies = type; // Update CSS variable --birb-highlight to be wing color - document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[Sprite.THEME_HIGHLIGHT]); + document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[SPRITE.THEME_HIGHLIGHT]); save(); } diff --git a/manifest.json b/manifest.json index f5cd6bc..1526bbe 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.43", + "version": "2025.10.26.44", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/Frame.js b/src/Frame.js index 2400ec2..f9dd7e0 100644 --- a/src/Frame.js +++ b/src/Frame.js @@ -1,5 +1,5 @@ import { Directions } from './sharedConstants.js'; -import { Sprite, BirdType } from './sprites.js'; +import { SPRITE, BirdType } from './sprites.js'; import Layer from './layer.js'; class Frame { @@ -25,7 +25,7 @@ class Frame { 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)); + this.pixels.unshift(new Array(this.pixels[0].length).fill(SPRITE.TRANSPARENT)); } // Combine layers for (let i = 1; i < layers.length; i++) { @@ -34,7 +34,7 @@ class Frame { 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.pixels[y + topMargin][x] = layerPixels[y][x] !== SPRITE.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; } } } diff --git a/src/birb.js b/src/birb.js index 14c6962..1fd7e31 100644 --- a/src/birb.js +++ b/src/birb.js @@ -3,7 +3,7 @@ import { } from './sharedConstants.js'; import { - Sprite, + SPRITE, SPRITE_SHEET_COLOR_MAP, SPECIES, BirdType @@ -184,7 +184,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) { const b = pixels[index + 2]; const a = pixels[index + 3]; if (a === 0) { - row.push(Sprite.TRANSPARENT); + row.push(SPRITE.TRANSPARENT); continue; } const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; @@ -194,7 +194,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) { } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); + row.push(SPRITE.TRANSPARENT); } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } @@ -1129,7 +1129,7 @@ Promise.all([ function switchSpecies(type) { currentSpecies = type; // Update CSS variable --birb-highlight to be wing color - document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[Sprite.THEME_HIGHLIGHT]); + document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[SPRITE.THEME_HIGHLIGHT]); save(); } diff --git a/src/frame.js b/src/frame.js index 2400ec2..f9dd7e0 100644 --- a/src/frame.js +++ b/src/frame.js @@ -1,5 +1,5 @@ import { Directions } from './sharedConstants.js'; -import { Sprite, BirdType } from './sprites.js'; +import { SPRITE, BirdType } from './sprites.js'; import Layer from './layer.js'; class Frame { @@ -25,7 +25,7 @@ class Frame { 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)); + this.pixels.unshift(new Array(this.pixels[0].length).fill(SPRITE.TRANSPARENT)); } // Combine layers for (let i = 1; i < layers.length; i++) { @@ -34,7 +34,7 @@ class Frame { 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.pixels[y + topMargin][x] = layerPixels[y][x] !== SPRITE.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; } } } diff --git a/src/sprites.js b/src/sprites.js index 14884d4..e1d808a 100644 --- a/src/sprites.js +++ b/src/sprites.js @@ -1,5 +1,5 @@ /** Indicators for parts of the base bird sprite sheet */ -export const Sprite = { +export const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", TRANSPARENT: "transparent", OUTLINE: "outline", @@ -22,23 +22,23 @@ export const Sprite = { /** @type {Record} */ 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, + "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 { @@ -52,20 +52,20 @@ export class BirdType { 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, + [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} */ - this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.colors = { ...defaultColors, ...colors, [SPRITE.THEME_HIGHLIGHT]: colors[SPRITE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.tags = tags; } } @@ -74,126 +74,126 @@ export class 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", + [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", + [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", + [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", + [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", + [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" + [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", + [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", + [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", + [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", + [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", + [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", + [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", }), }; \ No newline at end of file From 45ed91a5317863aa7d84a44a20be5220a3231951 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 13:56:14 -0400 Subject: [PATCH 06/16] Move anim class and stylesheet --- build.js | 2 +- dist/birb.js | 91 +++++++++++++-------------- dist/birb.user.js | 93 ++++++++++++++-------------- manifest.json | 2 +- src/anim.js | 48 ++++++++++++++ src/birb.js | 50 ++------------- stylesheet.css => src/stylesheet.css | 0 7 files changed, 147 insertions(+), 139 deletions(-) create mode 100644 src/anim.js rename stylesheet.css => src/stylesheet.css (100%) diff --git a/build.js b/build.js index 26c4de0..b954f80 100644 --- a/build.js +++ b/build.js @@ -18,7 +18,7 @@ const spriteSheets = [ } ]; -const STYLESHEET_PATH = "./stylesheet.css"; +const STYLESHEET_PATH = "./src/stylesheet.css"; const STYLESHEET_KEY = "___STYLESHEET___"; const now = new Date(); diff --git a/dist/birb.js b/dist/birb.js index 799d569..8b4c3f9 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -283,6 +283,50 @@ } } } } + 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; + } + + getAnimationDuration() { + return this.durations.reduce((a, b) => a + b, 0); + } + + /** + * @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) { + let time = Date.now() - timeStart; + const duration = this.getAnimationDuration(); + if (this.loop) { + time %= duration; + } + let totalDuration = 0; + for (let i = 0; i < this.durations.length; i++) { + totalDuration += this.durations[i]; + if (time < totalDuration) { + this.frames[i].draw(ctx, direction, canvasPixelSize, species); + return false; + } + } + // Draw the last frame if the animation is complete + this.frames[this.frames.length - 1].draw(ctx, direction, canvasPixelSize, species); + return true; + } + } + // @ts-ignore const SHARED_CONFIG = { birbCssScale: 1, @@ -684,49 +728,6 @@ outline: none; }`; - 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; - } - - getAnimationDuration() { - return this.durations.reduce((a, b) => a + b, 0); - } - - /** - * @param {CanvasRenderingContext2D} ctx - * @param {number} direction - * @param {number} timeStart The start time of the animation in milliseconds - * @param {BirdType} [species] The species to use for the animation - * @returns {boolean} Whether the animation is complete - */ - draw(ctx, direction, timeStart, species) { - let time = Date.now() - timeStart; - const duration = this.getAnimationDuration(); - if (this.loop) { - time %= duration; - } - let totalDuration = 0; - for (let i = 0; i < this.durations.length; i++) { - totalDuration += this.durations[i]; - if (time < totalDuration) { - 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, CANVAS_PIXEL_SIZE, species); - return true; - } - } - const DEFAULT_BIRD = "bluebird"; const SPRITE_WIDTH = 32; @@ -1316,7 +1317,7 @@ } ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, SPECIES[currentSpecies])) { + if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) { setAnimation(Animations.STILL); } @@ -1531,7 +1532,7 @@ if (!featherCtx) { return; } - FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), type); + FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, type); document.body.appendChild(featherCanvas); onClick(featherCanvas, () => { unlockBird(birdType); diff --git a/dist/birb.user.js b/dist/birb.user.js index 062d19f..6d24af6 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.44 +// @version 2025.10.26.55 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -297,6 +297,50 @@ } } } } + 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; + } + + getAnimationDuration() { + return this.durations.reduce((a, b) => a + b, 0); + } + + /** + * @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) { + let time = Date.now() - timeStart; + const duration = this.getAnimationDuration(); + if (this.loop) { + time %= duration; + } + let totalDuration = 0; + for (let i = 0; i < this.durations.length; i++) { + totalDuration += this.durations[i]; + if (time < totalDuration) { + this.frames[i].draw(ctx, direction, canvasPixelSize, species); + return false; + } + } + // Draw the last frame if the animation is complete + this.frames[this.frames.length - 1].draw(ctx, direction, canvasPixelSize, species); + return true; + } + } + // @ts-ignore const SHARED_CONFIG = { birbCssScale: 1, @@ -698,49 +742,6 @@ outline: none; }`; - 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; - } - - getAnimationDuration() { - return this.durations.reduce((a, b) => a + b, 0); - } - - /** - * @param {CanvasRenderingContext2D} ctx - * @param {number} direction - * @param {number} timeStart The start time of the animation in milliseconds - * @param {BirdType} [species] The species to use for the animation - * @returns {boolean} Whether the animation is complete - */ - draw(ctx, direction, timeStart, species) { - let time = Date.now() - timeStart; - const duration = this.getAnimationDuration(); - if (this.loop) { - time %= duration; - } - let totalDuration = 0; - for (let i = 0; i < this.durations.length; i++) { - totalDuration += this.durations[i]; - if (time < totalDuration) { - 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, CANVAS_PIXEL_SIZE, species); - return true; - } - } - const DEFAULT_BIRD = "bluebird"; const SPRITE_WIDTH = 32; @@ -1330,7 +1331,7 @@ } ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, SPECIES[currentSpecies])) { + if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) { setAnimation(Animations.STILL); } @@ -1545,7 +1546,7 @@ if (!featherCtx) { return; } - FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), type); + FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, type); document.body.appendChild(featherCanvas); onClick(featherCanvas, () => { unlockBird(birdType); diff --git a/manifest.json b/manifest.json index 1526bbe..f700c72 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.44", + "version": "2025.10.26.55", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/anim.js b/src/anim.js new file mode 100644 index 0000000..7294edb --- /dev/null +++ b/src/anim.js @@ -0,0 +1,48 @@ +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; + } + + getAnimationDuration() { + return this.durations.reduce((a, b) => a + b, 0); + } + + /** + * @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) { + let time = Date.now() - timeStart; + const duration = this.getAnimationDuration(); + if (this.loop) { + time %= duration; + } + let totalDuration = 0; + for (let i = 0; i < this.durations.length; i++) { + totalDuration += this.durations[i]; + if (time < totalDuration) { + this.frames[i].draw(ctx, direction, canvasPixelSize, species); + return false; + } + } + // Draw the last frame if the animation is complete + this.frames[this.frames.length - 1].draw(ctx, direction, canvasPixelSize, species); + return true; + } +} + +export default Anim; \ No newline at end of file diff --git a/src/birb.js b/src/birb.js index 1fd7e31..8765a5d 100644 --- a/src/birb.js +++ b/src/birb.js @@ -11,6 +11,7 @@ import { import Frame from './frame.js'; import Layer from './layer.js'; +import Anim from './anim.js'; // @ts-ignore const SHARED_CONFIG = { @@ -70,49 +71,6 @@ let userSettings = {}; const STYLESHEET = `___STYLESHEET___`; -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; - } - - getAnimationDuration() { - return this.durations.reduce((a, b) => a + b, 0); - } - - /** - * @param {CanvasRenderingContext2D} ctx - * @param {number} direction - * @param {number} timeStart The start time of the animation in milliseconds - * @param {BirdType} [species] The species to use for the animation - * @returns {boolean} Whether the animation is complete - */ - draw(ctx, direction, timeStart, species) { - let time = Date.now() - timeStart; - const duration = this.getAnimationDuration(); - if (this.loop) { - time %= duration; - } - let totalDuration = 0; - for (let i = 0; i < this.durations.length; i++) { - totalDuration += this.durations[i]; - if (time < totalDuration) { - 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, CANVAS_PIXEL_SIZE, species); - return true; - } -} - const DEFAULT_BIRD = "bluebird"; const SPRITE_WIDTH = 32; @@ -710,7 +668,7 @@ Promise.all([ } ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, SPECIES[currentSpecies])) { + if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) { setAnimation(Animations.STILL); } @@ -909,7 +867,7 @@ Promise.all([ return; } // Draw the decoration - DECORATION_ANIMATIONS.mac.draw(decorationCtx, Directions.LEFT, Date.now()); + DECORATION_ANIMATIONS.mac.draw(decorationCtx, Directions.LEFT, CANVAS_PIXEL_SIZE, Date.now()); // Add the decoration to the page document.body.appendChild(decorationCanvas); makeDraggable(decorationCanvas, false); @@ -945,7 +903,7 @@ Promise.all([ if (!featherCtx) { return; } - FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), type); + FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, type); document.body.appendChild(featherCanvas); onClick(featherCanvas, () => { unlockBird(birdType); diff --git a/stylesheet.css b/src/stylesheet.css similarity index 100% rename from stylesheet.css rename to src/stylesheet.css From ef764153b9f9108a8f80a798a97a3f6d1071b725 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 14:04:40 -0400 Subject: [PATCH 07/16] Separate sticky note logic --- dist/birb.js | 448 ++++++++++++++++++++++++++++---------------- dist/birb.user.js | 450 +++++++++++++++++++++++++++++---------------- manifest.json | 2 +- src/birb.js | 162 +--------------- src/stickyNotes.js | 292 +++++++++++++++++++++++++++++ 5 files changed, 881 insertions(+), 473 deletions(-) create mode 100644 src/stickyNotes.js diff --git a/dist/birb.js b/dist/birb.js index 8b4c3f9..100612c 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -327,6 +327,293 @@ } } + /** + * @typedef {Object} SavedStickyNote + * @property {string} id + * @property {string} site + * @property {string} content + * @property {number} top + * @property {number} left + */ + + class StickyNote { + /** + * @param {string} id + * @param {string} [site] + * @param {string} [content] + * @param {number} [top] + * @param {number} [left] + */ + constructor(id, site = "", content = "", top = 0, left = 0) { + this.id = id; + this.site = site; + this.content = content; + this.top = top; + this.left = left; + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + + /** + * @param {StickyNote} stickyNote + * @returns {boolean} Whether the given sticky note is applicable to the current site/page + */ + function isStickyNoteApplicable(stickyNote) { + const stickyNoteUrl = stickyNote.site; + const currentUrl = window.location.href; + const stickyNoteWebsite = stickyNoteUrl.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const stickyNoteParams = parseUrlParams(stickyNoteUrl); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) { + return false; + } + } + return true; + } + + /** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ + function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + return element; + } + + /** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ + function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); + } + + /** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ + function makeDraggable(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + console.error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); + } + + /** + * @param {() => void} func + * @param {Element} [closeButton] + */ + function makeClosable(func, closeButton) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); + } + + /** + * @param {StickyNote} stickyNote + * @param {() => void} onSave + * @param {() => void} onDelete + * @returns {HTMLElement} + */ + function renderStickyNote(stickyNote, onSave, onDelete) { + let html = ` +
+
Sticky Note
+
x
+
+
+ +
`; + const noteElement = makeElement("birb-window"); + noteElement.classList.add("birb-sticky-note"); + noteElement.innerHTML = html; + + noteElement.style.top = `${stickyNote.top}px`; + noteElement.style.left = `${stickyNote.left}px`; + document.body.appendChild(noteElement); + + makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + onSave(); + }); + + const closeButton = noteElement.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + if (confirm("Are you sure you want to delete this sticky note?")) { + onDelete(); + noteElement.remove(); + } + }, closeButton); + } + + const textarea = noteElement.querySelector(".birb-sticky-note-input"); + if (textarea && textarea instanceof HTMLTextAreaElement) { + let saveTimeout; + // Save after debounce + textarea.addEventListener("input", () => { + stickyNote.content = textarea.value; + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + onSave(); + }, 250); + }); + } + + // On window resize + window.addEventListener("resize", () => { + const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; + const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; + noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + }); + + return noteElement; + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function drawStickyNotes(stickyNotes, onSave, onDelete) { + // Remove all existing sticky notes + const existingNotes = document.querySelectorAll(".birb-sticky-note"); + existingNotes.forEach(note => note.remove()); + // Render all sticky notes + for (let stickyNote of stickyNotes) { + if (isStickyNoteApplicable(stickyNote)) { + renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + } + } + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function createNewStickyNote(stickyNotes, onSave, onDelete) { + const id = Date.now().toString(); + const site = window.location.href; + const stickyNote = new StickyNote(id, site, ""); + const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; + stickyNote.top = parseInt(element.style.top, 10); + stickyNote.left = parseInt(element.style.left, 10); + stickyNotes.push(stickyNote); + onSave(); + } + // @ts-ignore const SHARED_CONFIG = { birbCssScale: 1, @@ -364,12 +651,7 @@ */ /** - * @typedef {Object} SavedStickyNote - * @property {string} id - * @property {string} site - * @property {string} content - * @property {number} top - * @property {number} left + * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote */ /** @@ -955,27 +1237,10 @@ } } - class StickyNote { - /** - * @param {string} id - * @param {string} [site] - * @param {string} [content] - * @param {number} [top] - * @param {number} [left] - */ - constructor(id, site = "", content = "", top = 0, left = 0) { - this.id = id; - this.site = site; - this.content = content; - this.top = top; - this.left = left; - } - } - const menuItems = [ new MenuItem(`Pet ${birdBirb()}`, pet), new MenuItem("Field Guide", insertFieldGuide), - new MenuItem("Sticky Note", newStickyNote), + new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote)), new MenuItem(`Hide ${birdBirb()}`, hideBirb), new DebugMenuItem("Freeze/Unfreeze", () => { frozen = !frozen; @@ -1231,7 +1496,7 @@ pet(); }); - drawStickyNotes(); + drawStickyNotes(stickyNotes, save, deleteStickyNote); let lastUrl = (window.location.href ?? "").split("?")[0]; setInterval(() => { @@ -1239,7 +1504,7 @@ if (currentUrl !== lastUrl) { log("URL changed, updating sticky notes"); lastUrl = currentUrl; - drawStickyNotes(); + drawStickyNotes(stickyNotes, save, deleteStickyNote); } }, URL_CHECK_INTERVAL); @@ -1326,81 +1591,6 @@ setY(birdY); } - function newStickyNote() { - const id = Date.now().toString(); - const site = window.location.href; - const stickyNote = new StickyNote(id, site, ""); - const element = renderStickyNote(stickyNote); - element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; - element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; - stickyNote.top = parseInt(element.style.top, 10); - stickyNote.left = parseInt(element.style.left, 10); - stickyNotes.push(stickyNote); - save(); - } - - /** - * @param {StickyNote} stickyNote - * @returns {HTMLElement} - */ - function renderStickyNote(stickyNote) { - let html = ` -
-
Sticky Note
-
x
-
-
- -
`; - const noteElement = makeElement("birb-window"); - noteElement.classList.add("birb-sticky-note"); - noteElement.innerHTML = html; - - noteElement.style.top = `${stickyNote.top}px`; - noteElement.style.left = `${stickyNote.left}px`; - document.body.appendChild(noteElement); - - makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { - stickyNote.top = top; - stickyNote.left = left; - save(); - }); - - const closeButton = noteElement.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - if (confirm("Are you sure you want to delete this sticky note?")) { - deleteStickyNote(stickyNote); - noteElement.remove(); - } - }, closeButton); - } - - const textarea = noteElement.querySelector(".birb-sticky-note-input"); - if (textarea && textarea instanceof HTMLTextAreaElement) { - let saveTimeout; - // Save after debounce - textarea.addEventListener("input", () => { - stickyNote.content = textarea.value; - if (saveTimeout) { - clearTimeout(saveTimeout); - } - saveTimeout = setTimeout(() => { - save(); - }, 250); - }); - } - - // On window resize - window.addEventListener("resize", () => { - const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; - const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; - noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; - }); - - return noteElement; - } - /** * @param {StickyNote} stickyNote */ @@ -1409,47 +1599,6 @@ save(); } - /** - * @param {StickyNote} stickyNote - * @returns {boolean} Whether the given sticky note is applicable to the current site/page - */ - function isStickyNoteApplicable(stickyNote) { - const stickyNoteUrl = stickyNote.site; - const currentUrl = window.location.href; - const stickyNoteWebsite = stickyNoteUrl.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - debug("Comparing " + stickyNoteUrl + " with " + currentUrl); - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const stickyNoteParams = parseUrlParams(stickyNoteUrl); - const currentParams = parseUrlParams(currentUrl); - - debug("Comparing params: ", stickyNoteParams, currentParams); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) { - return false; - } - } - return true; - } - - function drawStickyNotes() { - // Remove all existing sticky notes - const existingNotes = document.querySelectorAll(".birb-sticky-note"); - existingNotes.forEach(note => note.remove()); - // Render all sticky notes - for (let stickyNote of stickyNotes) { - if (isStickyNoteApplicable(stickyNote)) { - renderStickyNote(stickyNote); - } - } - } - /** * Create an HTML element with the specified parameters * @param {string} className @@ -2161,21 +2310,6 @@ return { x, y }; } - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - // Run the birb init(); draw(); diff --git a/dist/birb.user.js b/dist/birb.user.js index 6d24af6..54a20e1 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.55 +// @version 2025.10.26.76 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -341,6 +341,293 @@ } } + /** + * @typedef {Object} SavedStickyNote + * @property {string} id + * @property {string} site + * @property {string} content + * @property {number} top + * @property {number} left + */ + + class StickyNote { + /** + * @param {string} id + * @param {string} [site] + * @param {string} [content] + * @param {number} [top] + * @param {number} [left] + */ + constructor(id, site = "", content = "", top = 0, left = 0) { + this.id = id; + this.site = site; + this.content = content; + this.top = top; + this.left = left; + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + + /** + * @param {StickyNote} stickyNote + * @returns {boolean} Whether the given sticky note is applicable to the current site/page + */ + function isStickyNoteApplicable(stickyNote) { + const stickyNoteUrl = stickyNote.site; + const currentUrl = window.location.href; + const stickyNoteWebsite = stickyNoteUrl.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const stickyNoteParams = parseUrlParams(stickyNoteUrl); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) { + return false; + } + } + return true; + } + + /** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ + function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + return element; + } + + /** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ + function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); + } + + /** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ + function makeDraggable(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + console.error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); + } + + /** + * @param {() => void} func + * @param {Element} [closeButton] + */ + function makeClosable(func, closeButton) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); + } + + /** + * @param {StickyNote} stickyNote + * @param {() => void} onSave + * @param {() => void} onDelete + * @returns {HTMLElement} + */ + function renderStickyNote(stickyNote, onSave, onDelete) { + let html = ` +
+
Sticky Note
+
x
+
+
+ +
`; + const noteElement = makeElement("birb-window"); + noteElement.classList.add("birb-sticky-note"); + noteElement.innerHTML = html; + + noteElement.style.top = `${stickyNote.top}px`; + noteElement.style.left = `${stickyNote.left}px`; + document.body.appendChild(noteElement); + + makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + onSave(); + }); + + const closeButton = noteElement.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + if (confirm("Are you sure you want to delete this sticky note?")) { + onDelete(); + noteElement.remove(); + } + }, closeButton); + } + + const textarea = noteElement.querySelector(".birb-sticky-note-input"); + if (textarea && textarea instanceof HTMLTextAreaElement) { + let saveTimeout; + // Save after debounce + textarea.addEventListener("input", () => { + stickyNote.content = textarea.value; + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + onSave(); + }, 250); + }); + } + + // On window resize + window.addEventListener("resize", () => { + const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; + const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; + noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + }); + + return noteElement; + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function drawStickyNotes(stickyNotes, onSave, onDelete) { + // Remove all existing sticky notes + const existingNotes = document.querySelectorAll(".birb-sticky-note"); + existingNotes.forEach(note => note.remove()); + // Render all sticky notes + for (let stickyNote of stickyNotes) { + if (isStickyNoteApplicable(stickyNote)) { + renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + } + } + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function createNewStickyNote(stickyNotes, onSave, onDelete) { + const id = Date.now().toString(); + const site = window.location.href; + const stickyNote = new StickyNote(id, site, ""); + const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; + stickyNote.top = parseInt(element.style.top, 10); + stickyNote.left = parseInt(element.style.left, 10); + stickyNotes.push(stickyNote); + onSave(); + } + // @ts-ignore const SHARED_CONFIG = { birbCssScale: 1, @@ -378,12 +665,7 @@ */ /** - * @typedef {Object} SavedStickyNote - * @property {string} id - * @property {string} site - * @property {string} content - * @property {number} top - * @property {number} left + * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote */ /** @@ -969,27 +1251,10 @@ } } - class StickyNote { - /** - * @param {string} id - * @param {string} [site] - * @param {string} [content] - * @param {number} [top] - * @param {number} [left] - */ - constructor(id, site = "", content = "", top = 0, left = 0) { - this.id = id; - this.site = site; - this.content = content; - this.top = top; - this.left = left; - } - } - const menuItems = [ new MenuItem(`Pet ${birdBirb()}`, pet), new MenuItem("Field Guide", insertFieldGuide), - new MenuItem("Sticky Note", newStickyNote), + new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote)), new MenuItem(`Hide ${birdBirb()}`, hideBirb), new DebugMenuItem("Freeze/Unfreeze", () => { frozen = !frozen; @@ -1245,7 +1510,7 @@ pet(); }); - drawStickyNotes(); + drawStickyNotes(stickyNotes, save, deleteStickyNote); let lastUrl = (window.location.href ?? "").split("?")[0]; setInterval(() => { @@ -1253,7 +1518,7 @@ if (currentUrl !== lastUrl) { log("URL changed, updating sticky notes"); lastUrl = currentUrl; - drawStickyNotes(); + drawStickyNotes(stickyNotes, save, deleteStickyNote); } }, URL_CHECK_INTERVAL); @@ -1340,81 +1605,6 @@ setY(birdY); } - function newStickyNote() { - const id = Date.now().toString(); - const site = window.location.href; - const stickyNote = new StickyNote(id, site, ""); - const element = renderStickyNote(stickyNote); - element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; - element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; - stickyNote.top = parseInt(element.style.top, 10); - stickyNote.left = parseInt(element.style.left, 10); - stickyNotes.push(stickyNote); - save(); - } - - /** - * @param {StickyNote} stickyNote - * @returns {HTMLElement} - */ - function renderStickyNote(stickyNote) { - let html = ` -
-
Sticky Note
-
x
-
-
- -
`; - const noteElement = makeElement("birb-window"); - noteElement.classList.add("birb-sticky-note"); - noteElement.innerHTML = html; - - noteElement.style.top = `${stickyNote.top}px`; - noteElement.style.left = `${stickyNote.left}px`; - document.body.appendChild(noteElement); - - makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { - stickyNote.top = top; - stickyNote.left = left; - save(); - }); - - const closeButton = noteElement.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - if (confirm("Are you sure you want to delete this sticky note?")) { - deleteStickyNote(stickyNote); - noteElement.remove(); - } - }, closeButton); - } - - const textarea = noteElement.querySelector(".birb-sticky-note-input"); - if (textarea && textarea instanceof HTMLTextAreaElement) { - let saveTimeout; - // Save after debounce - textarea.addEventListener("input", () => { - stickyNote.content = textarea.value; - if (saveTimeout) { - clearTimeout(saveTimeout); - } - saveTimeout = setTimeout(() => { - save(); - }, 250); - }); - } - - // On window resize - window.addEventListener("resize", () => { - const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; - const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; - noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; - }); - - return noteElement; - } - /** * @param {StickyNote} stickyNote */ @@ -1423,47 +1613,6 @@ save(); } - /** - * @param {StickyNote} stickyNote - * @returns {boolean} Whether the given sticky note is applicable to the current site/page - */ - function isStickyNoteApplicable(stickyNote) { - const stickyNoteUrl = stickyNote.site; - const currentUrl = window.location.href; - const stickyNoteWebsite = stickyNoteUrl.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - debug("Comparing " + stickyNoteUrl + " with " + currentUrl); - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const stickyNoteParams = parseUrlParams(stickyNoteUrl); - const currentParams = parseUrlParams(currentUrl); - - debug("Comparing params: ", stickyNoteParams, currentParams); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) { - return false; - } - } - return true; - } - - function drawStickyNotes() { - // Remove all existing sticky notes - const existingNotes = document.querySelectorAll(".birb-sticky-note"); - existingNotes.forEach(note => note.remove()); - // Render all sticky notes - for (let stickyNote of stickyNotes) { - if (isStickyNoteApplicable(stickyNote)) { - renderStickyNote(stickyNote); - } - } - } - /** * Create an HTML element with the specified parameters * @param {string} className @@ -2175,21 +2324,6 @@ return { x, y }; } - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - // Run the birb init(); draw(); diff --git a/manifest.json b/manifest.json index f700c72..894f595 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.55", + "version": "2025.10.26.76", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/birb.js b/src/birb.js index 8765a5d..2484b25 100644 --- a/src/birb.js +++ b/src/birb.js @@ -12,6 +12,7 @@ import { import Frame from './frame.js'; import Layer from './layer.js'; import Anim from './anim.js'; +import { StickyNote, createNewStickyNote, drawStickyNotes } from './stickyNotes.js'; // @ts-ignore const SHARED_CONFIG = { @@ -50,12 +51,7 @@ const DEFAULT_SETTINGS = { */ /** - * @typedef {Object} SavedStickyNote - * @property {string} id - * @property {string} site - * @property {string} content - * @property {number} top - * @property {number} left + * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote */ /** @@ -306,27 +302,10 @@ Promise.all([ } } - class StickyNote { - /** - * @param {string} id - * @param {string} [site] - * @param {string} [content] - * @param {number} [top] - * @param {number} [left] - */ - constructor(id, site = "", content = "", top = 0, left = 0) { - this.id = id; - this.site = site; - this.content = content; - this.top = top; - this.left = left; - } - } - const menuItems = [ new MenuItem(`Pet ${birdBirb()}`, pet), new MenuItem("Field Guide", insertFieldGuide), - new MenuItem("Sticky Note", newStickyNote), + new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote)), new MenuItem(`Hide ${birdBirb()}`, hideBirb), new DebugMenuItem("Freeze/Unfreeze", () => { frozen = !frozen; @@ -582,7 +561,7 @@ Promise.all([ pet(); }); - drawStickyNotes(); + drawStickyNotes(stickyNotes, save, deleteStickyNote); let lastUrl = (window.location.href ?? "").split("?")[0]; setInterval(() => { @@ -590,7 +569,7 @@ Promise.all([ if (currentUrl !== lastUrl) { log("URL changed, updating sticky notes"); lastUrl = currentUrl; - drawStickyNotes(); + drawStickyNotes(stickyNotes, save, deleteStickyNote); } }, URL_CHECK_INTERVAL); @@ -677,81 +656,6 @@ Promise.all([ setY(birdY); } - function newStickyNote() { - const id = Date.now().toString(); - const site = window.location.href; - const stickyNote = new StickyNote(id, site, ""); - const element = renderStickyNote(stickyNote); - element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; - element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; - stickyNote.top = parseInt(element.style.top, 10); - stickyNote.left = parseInt(element.style.left, 10); - stickyNotes.push(stickyNote); - save(); - } - - /** - * @param {StickyNote} stickyNote - * @returns {HTMLElement} - */ - function renderStickyNote(stickyNote) { - let html = ` -
-
Sticky Note
-
x
-
-
- -
` - const noteElement = makeElement("birb-window"); - noteElement.classList.add("birb-sticky-note"); - noteElement.innerHTML = html; - - noteElement.style.top = `${stickyNote.top}px`; - noteElement.style.left = `${stickyNote.left}px`; - document.body.appendChild(noteElement); - - makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { - stickyNote.top = top; - stickyNote.left = left; - save(); - }); - - const closeButton = noteElement.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - if (confirm("Are you sure you want to delete this sticky note?")) { - deleteStickyNote(stickyNote); - noteElement.remove(); - } - }, closeButton); - } - - const textarea = noteElement.querySelector(".birb-sticky-note-input"); - if (textarea && textarea instanceof HTMLTextAreaElement) { - let saveTimeout; - // Save after debounce - textarea.addEventListener("input", () => { - stickyNote.content = textarea.value; - if (saveTimeout) { - clearTimeout(saveTimeout); - } - saveTimeout = setTimeout(() => { - save(); - }, 250); - }); - } - - // On window resize - window.addEventListener("resize", () => { - const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; - const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; - noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; - }); - - return noteElement; - } - /** * @param {StickyNote} stickyNote */ @@ -760,47 +664,6 @@ Promise.all([ save(); } - /** - * @param {StickyNote} stickyNote - * @returns {boolean} Whether the given sticky note is applicable to the current site/page - */ - function isStickyNoteApplicable(stickyNote) { - const stickyNoteUrl = stickyNote.site; - const currentUrl = window.location.href; - const stickyNoteWebsite = stickyNoteUrl.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - debug("Comparing " + stickyNoteUrl + " with " + currentUrl); - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const stickyNoteParams = parseUrlParams(stickyNoteUrl); - const currentParams = parseUrlParams(currentUrl); - - debug("Comparing params: ", stickyNoteParams, currentParams); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) { - return false; - } - } - return true; - } - - function drawStickyNotes() { - // Remove all existing sticky notes - const existingNotes = document.querySelectorAll(".birb-sticky-note"); - existingNotes.forEach(note => note.remove()); - // Render all sticky notes - for (let stickyNote of stickyNotes) { - if (isStickyNoteApplicable(stickyNote)) { - renderStickyNote(stickyNote); - } - } - } - /** * Create an HTML element with the specified parameters * @param {string} className @@ -1540,21 +1403,6 @@ Promise.all([ return { x, y }; } - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - // Run the birb init(); draw(); diff --git a/src/stickyNotes.js b/src/stickyNotes.js new file mode 100644 index 0000000..5027710 --- /dev/null +++ b/src/stickyNotes.js @@ -0,0 +1,292 @@ +/** + * @typedef {Object} SavedStickyNote + * @property {string} id + * @property {string} site + * @property {string} content + * @property {number} top + * @property {number} left + */ + +export class StickyNote { + /** + * @param {string} id + * @param {string} [site] + * @param {string} [content] + * @param {number} [top] + * @param {number} [left] + */ + constructor(id, site = "", content = "", top = 0, left = 0) { + this.id = id; + this.site = site; + this.content = content; + this.top = top; + this.left = left; + } +} + +/** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ +export function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); +} + +/** + * @param {StickyNote} stickyNote + * @returns {boolean} Whether the given sticky note is applicable to the current site/page + */ +export function isStickyNoteApplicable(stickyNote) { + const stickyNoteUrl = stickyNote.site; + const currentUrl = window.location.href; + const stickyNoteWebsite = stickyNoteUrl.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const stickyNoteParams = parseUrlParams(stickyNoteUrl); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) { + return false; + } + } + return true; +} + +/** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ +function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } + return element; +} + +/** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ +function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); +} + +/** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ +function makeDraggable(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + console.error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); +} + +/** + * @param {() => void} func + * @param {Element} [closeButton] + */ +function makeClosable(func, closeButton) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); +} + +/** + * @param {StickyNote} stickyNote + * @param {() => void} onSave + * @param {() => void} onDelete + * @returns {HTMLElement} + */ +export function renderStickyNote(stickyNote, onSave, onDelete) { + let html = ` +
+
Sticky Note
+
x
+
+
+ +
` + const noteElement = makeElement("birb-window"); + noteElement.classList.add("birb-sticky-note"); + noteElement.innerHTML = html; + + noteElement.style.top = `${stickyNote.top}px`; + noteElement.style.left = `${stickyNote.left}px`; + document.body.appendChild(noteElement); + + makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + onSave(); + }); + + const closeButton = noteElement.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + if (confirm("Are you sure you want to delete this sticky note?")) { + onDelete(); + noteElement.remove(); + } + }, closeButton); + } + + const textarea = noteElement.querySelector(".birb-sticky-note-input"); + if (textarea && textarea instanceof HTMLTextAreaElement) { + let saveTimeout; + // Save after debounce + textarea.addEventListener("input", () => { + stickyNote.content = textarea.value; + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + onSave(); + }, 250); + }); + } + + // On window resize + window.addEventListener("resize", () => { + const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; + const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; + noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + }); + + return noteElement; +} + +/** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ +export function drawStickyNotes(stickyNotes, onSave, onDelete) { + // Remove all existing sticky notes + const existingNotes = document.querySelectorAll(".birb-sticky-note"); + existingNotes.forEach(note => note.remove()); + // Render all sticky notes + for (let stickyNote of stickyNotes) { + if (isStickyNoteApplicable(stickyNote)) { + renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + } + } +} + +/** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ +export function createNewStickyNote(stickyNotes, onSave, onDelete) { + const id = Date.now().toString(); + const site = window.location.href; + const stickyNote = new StickyNote(id, site, ""); + const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; + stickyNote.top = parseInt(element.style.top, 10); + stickyNote.left = parseInt(element.style.left, 10); + stickyNotes.push(stickyNote); + onSave(); +} From 9f157036891bce5e02c0acfbc378cb9f3a5c553e Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 15:29:36 -0400 Subject: [PATCH 08/16] Separate menu components --- dist/birb.js | 620 +++++++++++++++++++++++++++------------------ dist/birb.user.js | 622 ++++++++++++++++++++++++++++------------------ manifest.json | 2 +- src/birb.js | 185 +++----------- src/menu.js | 263 ++++++++++++++++++++ 5 files changed, 1046 insertions(+), 646 deletions(-) create mode 100644 src/menu.js diff --git a/dist/birb.js b/dist/birb.js index 100612c..fa30024 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -393,6 +393,258 @@ return true; } + /** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ + function makeElement$1(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + return element; + } + + /** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ + function onClick$1(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); + } + + /** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ + function makeDraggable$1(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + console.error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); + } + + /** + * @param {() => void} func + * @param {Element} [closeButton] + */ + function makeClosable$1(func, closeButton) { + if (closeButton) { + onClick$1(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); + } + + /** + * @param {StickyNote} stickyNote + * @param {() => void} onSave + * @param {() => void} onDelete + * @returns {HTMLElement} + */ + function renderStickyNote(stickyNote, onSave, onDelete) { + let html = ` +
+
Sticky Note
+
x
+
+
+ +
`; + const noteElement = makeElement$1("birb-window"); + noteElement.classList.add("birb-sticky-note"); + noteElement.innerHTML = html; + + noteElement.style.top = `${stickyNote.top}px`; + noteElement.style.left = `${stickyNote.left}px`; + document.body.appendChild(noteElement); + + makeDraggable$1(noteElement.querySelector(".birb-window-header"), true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + onSave(); + }); + + const closeButton = noteElement.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable$1(() => { + if (confirm("Are you sure you want to delete this sticky note?")) { + onDelete(); + noteElement.remove(); + } + }, closeButton); + } + + const textarea = noteElement.querySelector(".birb-sticky-note-input"); + if (textarea && textarea instanceof HTMLTextAreaElement) { + let saveTimeout; + // Save after debounce + textarea.addEventListener("input", () => { + stickyNote.content = textarea.value; + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + onSave(); + }, 250); + }); + } + + // On window resize + window.addEventListener("resize", () => { + const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; + const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; + noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + }); + + return noteElement; + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function drawStickyNotes(stickyNotes, onSave, onDelete) { + // Remove all existing sticky notes + const existingNotes = document.querySelectorAll(".birb-sticky-note"); + existingNotes.forEach(note => note.remove()); + // Render all sticky notes + for (let stickyNote of stickyNotes) { + if (isStickyNoteApplicable(stickyNote)) { + renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + } + } + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function createNewStickyNote(stickyNotes, onSave, onDelete) { + const id = Date.now().toString(); + const site = window.location.href; + const stickyNote = new StickyNote(id, site, ""); + const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; + stickyNote.top = parseInt(element.style.top, 10); + stickyNote.left = parseInt(element.style.left, 10); + stickyNotes.push(stickyNote); + onSave(); + } + + class MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {boolean} [removeMenu] + * @param {boolean} [isDebug] + */ + constructor(text, action, removeMenu = true, isDebug = false) { + this.text = text; + this.action = action; + this.removeMenu = removeMenu; + this.isDebug = isDebug; + } + } + + class DebugMenuItem extends MenuItem { + /** + * @param {string} text + * @param {() => void} action + */ + constructor(text, action, removeMenu = true) { + super(text, action, removeMenu, true); + } + } + + class Separator extends MenuItem { + constructor() { + super("", () => { }); + } + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -403,6 +655,12 @@ function makeElement(className, textContent, id) { const element = document.createElement("div"); element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } return element; } @@ -502,13 +760,7 @@ * @param {Element} [closeButton] */ function makeClosable(func, closeButton) { - if (closeButton) { - onClick(closeButton, func); - } document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { - return; - } if (e.key === "Escape") { func(); } @@ -516,102 +768,108 @@ } /** - * @param {StickyNote} stickyNote - * @param {() => void} onSave - * @param {() => void} onDelete + * @param {MenuItem} item + * @param {() => void} removeMenuCallback * @returns {HTMLElement} */ - function renderStickyNote(stickyNote, onSave, onDelete) { - let html = ` -
-
Sticky Note
-
x
-
-
- -
`; - const noteElement = makeElement("birb-window"); - noteElement.classList.add("birb-sticky-note"); - noteElement.innerHTML = html; - - noteElement.style.top = `${stickyNote.top}px`; - noteElement.style.left = `${stickyNote.left}px`; - document.body.appendChild(noteElement); - - makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { - stickyNote.top = top; - stickyNote.left = left; - onSave(); - }); - - const closeButton = noteElement.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - if (confirm("Are you sure you want to delete this sticky note?")) { - onDelete(); - noteElement.remove(); - } - }, closeButton); + function makeMenuItem(item, removeMenuCallback) { + if (item instanceof Separator) { + return makeElement("birb-window-separator"); } - - const textarea = noteElement.querySelector(".birb-sticky-note-input"); - if (textarea && textarea instanceof HTMLTextAreaElement) { - let saveTimeout; - // Save after debounce - textarea.addEventListener("input", () => { - stickyNote.content = textarea.value; - if (saveTimeout) { - clearTimeout(saveTimeout); - } - saveTimeout = setTimeout(() => { - onSave(); - }, 250); - }); - } - - // On window resize - window.addEventListener("resize", () => { - const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; - const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; - noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + let menuItem = makeElement("birb-menu-item", item.text); + onClick(menuItem, () => { + if (item.removeMenu) { + removeMenuCallback(); + } + item.action(); }); - - return noteElement; + return menuItem; } /** - * @param {StickyNote[]} stickyNotes - * @param {() => void} onSave - * @param {(note: StickyNote) => void} onDelete + * Add the menu to the page if it doesn't already exist + * @param {string} menuId + * @param {string} menuExitId + * @param {MenuItem[]} menuItems + * @param {string} title + * @param {boolean} debugMode + * @param {(menu: HTMLElement) => void} updateLocationCallback */ - function drawStickyNotes(stickyNotes, onSave, onDelete) { - // Remove all existing sticky notes - const existingNotes = document.querySelectorAll(".birb-sticky-note"); - existingNotes.forEach(note => note.remove()); - // Render all sticky notes - for (let stickyNote of stickyNotes) { - if (isStickyNoteApplicable(stickyNote)) { - renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) { + if (document.querySelector("#" + menuId)) { + return; + } + let menu = makeElement("birb-window", undefined, menuId); + let header = makeElement("birb-window-header"); + header.innerHTML = `
${title}
`; + let content = makeElement("birb-window-content"); + const removeCallback = () => removeMenu(menuId, menuExitId); + for (const item of menuItems) { + if (!item.isDebug || debugMode) { + content.appendChild(makeMenuItem(item, removeCallback)); } } + menu.appendChild(header); + menu.appendChild(content); + document.body.appendChild(menu); + makeDraggable(document.querySelector(".birb-window-header")); + + let menuExit = makeElement("birb-window-exit", undefined, menuExitId); + onClick(menuExit, removeCallback); + document.body.appendChild(menuExit); + makeClosable(removeCallback); + + updateLocationCallback(menu); } /** - * @param {StickyNote[]} stickyNotes - * @param {() => void} onSave - * @param {(note: StickyNote) => void} onDelete + * Remove the menu from the page + * @param {string} menuId + * @param {string} menuExitId */ - function createNewStickyNote(stickyNotes, onSave, onDelete) { - const id = Date.now().toString(); - const site = window.location.href; - const stickyNote = new StickyNote(id, site, ""); - const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); - element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; - element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; - stickyNote.top = parseInt(element.style.top, 10); - stickyNote.left = parseInt(element.style.left, 10); - stickyNotes.push(stickyNote); - onSave(); + function removeMenu(menuId, menuExitId) { + const menu = document.querySelector("#" + menuId); + if (menu) { + menu.remove(); + } + const exitMenu = document.querySelector("#" + menuExitId); + if (exitMenu) { + exitMenu.remove(); + } + } + + /** + * @param {string} menuId + * @returns {boolean} Whether the menu element is on the page + */ + function isMenuOpen(menuId) { + return document.querySelector("#" + menuId) !== null; + } + + /** + * @param {string} menuId + * @param {MenuItem[]} menuItems + * @param {boolean} debugMode + * @param {(menu: HTMLElement) => void} updateLocationCallback + */ + function switchMenuItems(menuId, menuItems, debugMode, updateLocationCallback) { + const menu = document.querySelector("#" + menuId); + if (!menu || !(menu instanceof HTMLElement)) { + return; + } + const content = menu.querySelector(".birb-window-content"); + if (!content) { + console.error("Birb: Content not found"); + return; + } + content.innerHTML = ""; + const removeCallback = () => removeMenu(menuId, menuId + "-exit"); + for (const item of menuItems) { + if (!item.isDebug || debugMode) { + content.appendChild(makeMenuItem(item, removeCallback)); + } + } + updateLocationCallback(menu); } // @ts-ignore @@ -1206,37 +1464,6 @@ ]), }; - class MenuItem { - /** - * @param {string} text - * @param {() => void} action - * @param {boolean} [removeMenu] - * @param {boolean} [isDebug] - */ - constructor(text, action, removeMenu = true, isDebug = false) { - this.text = text; - this.action = action; - this.removeMenu = removeMenu; - this.isDebug = isDebug; - } - } - - class DebugMenuItem extends MenuItem { - /** - * @param {string} text - * @param {() => void} action - */ - constructor(text, action, removeMenu = true) { - super(text, action, removeMenu, true); - } - } - - class Separator extends MenuItem { - constructor() { - super("", () => { }); - } - } - const menuItems = [ new MenuItem(`Pet ${birdBirb()}`, pet), new MenuItem("Field Guide", insertFieldGuide), @@ -1255,11 +1482,11 @@ debugMode = false; }), new Separator(), - new MenuItem("Settings", () => switchMenuItems(settingsItems), false), + new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false), ]; const settingsItems = [ - new MenuItem("Go Back", () => switchMenuItems(menuItems), false), + new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false), new Separator(), new MenuItem("Toggle Birb Mode", () => { userSettings.birbMode = !userSettings.birbMode; @@ -1464,7 +1691,7 @@ onClick(document, (e) => { lastActionTimestamp = Date.now(); if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { - removeMenu(); + removeMenu(MENU_ID, MENU_EXIT_ID); } }); @@ -1473,7 +1700,7 @@ // Currently being pet, don't open menu return; } - insertMenu(); + insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation); }); canvas.addEventListener("mouseover", () => { @@ -1520,7 +1747,7 @@ // Won't be restored on fullscreen exit } - if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (currentState === States.IDLE && !frozen && !isMenuOpen(MENU_ID)) { if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { @@ -1609,9 +1836,6 @@ function makeElement(className, textContent, id) { const element = document.createElement("div"); element.classList.add(className); - if (textContent) { - element.textContent = textContent; - } if (id) { element.id = id; } @@ -1750,6 +1974,30 @@ centerElement(modal); } + /** + * @param {HTMLElement} menu + */ + function updateMenuLocation(menu) { + let x = birdX; + let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; + const offset = 20; + if (x < window.innerWidth / 2) { + // Left side + x += offset; + } else { + // Right side + x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; + } + if (y > window.innerHeight / 2) { + // Top side + y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; + } else { + // Bottom side + y += offset; + } + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + } function insertFieldGuide() { if (document.querySelector("#" + FIELD_GUIDE_ID)) { return; @@ -1865,103 +2113,6 @@ save(); } - /** - * Add the menu to the page if it doesn't already exist - */ - function insertMenu() { - if (document.querySelector("#" + MENU_ID)) { - return; - } - let menu = makeElement("birb-window", undefined, MENU_ID); - let header = makeElement("birb-window-header"); - header.innerHTML = `
${birdBirb().toLowerCase()}OS
`; - let content = makeElement("birb-window-content"); - for (const item of menuItems) { - if (!item.isDebug || debugMode) { - content.appendChild(makeMenuItem(item)); - } - } - menu.appendChild(header); - menu.appendChild(content); - document.body.appendChild(menu); - makeDraggable(document.querySelector(".birb-window-header")); - - let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); - onClick(menuExit, () => { - removeMenu(); - }); - document.body.appendChild(menuExit); - makeClosable(removeMenu); - - updateMenuLocation(menu); - } - - /** - * Update the menu's location based on the bird's position - * @param {HTMLElement} menu - */ - function updateMenuLocation(menu) { - let x = birdX; - let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; - const offset = 20; - if (x < window.innerWidth / 2) { - // Left side - x += offset; - } else { - // Right side - x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; - } - if (y > window.innerHeight / 2) { - // Top side - y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; - } else { - // Bottom side - y += offset; - } - menu.style.left = `${x}px`; - menu.style.top = `${y}px`; - } - - /** - * @param {MenuItem[]} menuItems - */ - function switchMenuItems(menuItems) { - const menu = document.querySelector("#" + MENU_ID); - if (!menu || !(menu instanceof HTMLElement)) { - return; - } - const content = menu.querySelector(".birb-window-content"); - if (!content) { - error("Content not found"); - return; - } - content.innerHTML = ""; - for (const item of menuItems) { - if (!item.isDebug || debugMode) { - content.appendChild(makeMenuItem(item)); - } - } - updateMenuLocation(menu); - } - - /** - * @param {MenuItem} item - * @returns {HTMLElement} - */ - function makeMenuItem(item) { - if (item instanceof Separator) { - return makeElement("birb-window-separator"); - } - let menuItem = makeElement("birb-menu-item", item.text); - onClick(menuItem, () => { - if (item.removeMenu) { - removeMenu(); - } - item.action(); - }); - return menuItem; - } - /** * @param {Document|Element} element * @param {(e: Event) => void} action @@ -1987,27 +2138,6 @@ }); } - /** - * Remove the menu from the page - */ - function removeMenu() { - const menu = document.querySelector("#" + MENU_ID); - if (menu) { - menu.remove(); - } - const exitMenu = document.querySelector("#" + MENU_EXIT_ID); - if (exitMenu) { - exitMenu.remove(); - } - } - - /** - * @returns {boolean} Whether the menu element is on the page - */ - function isMenuOpen() { - return document.querySelector("#" + MENU_ID) !== null; - } - /** * @param {HTMLElement|null} element The element to detect drag events on * @param {boolean} [parent] Whether to move the parent element when the child is dragged diff --git a/dist/birb.user.js b/dist/birb.user.js index 54a20e1..2800cc6 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.76 +// @version 2025.10.26.101 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -407,6 +407,258 @@ return true; } + /** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ + function makeElement$1(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + return element; + } + + /** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ + function onClick$1(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); + } + + /** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ + function makeDraggable$1(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + console.error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); + } + + /** + * @param {() => void} func + * @param {Element} [closeButton] + */ + function makeClosable$1(func, closeButton) { + if (closeButton) { + onClick$1(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); + } + + /** + * @param {StickyNote} stickyNote + * @param {() => void} onSave + * @param {() => void} onDelete + * @returns {HTMLElement} + */ + function renderStickyNote(stickyNote, onSave, onDelete) { + let html = ` +
+
Sticky Note
+
x
+
+
+ +
`; + const noteElement = makeElement$1("birb-window"); + noteElement.classList.add("birb-sticky-note"); + noteElement.innerHTML = html; + + noteElement.style.top = `${stickyNote.top}px`; + noteElement.style.left = `${stickyNote.left}px`; + document.body.appendChild(noteElement); + + makeDraggable$1(noteElement.querySelector(".birb-window-header"), true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + onSave(); + }); + + const closeButton = noteElement.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable$1(() => { + if (confirm("Are you sure you want to delete this sticky note?")) { + onDelete(); + noteElement.remove(); + } + }, closeButton); + } + + const textarea = noteElement.querySelector(".birb-sticky-note-input"); + if (textarea && textarea instanceof HTMLTextAreaElement) { + let saveTimeout; + // Save after debounce + textarea.addEventListener("input", () => { + stickyNote.content = textarea.value; + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + onSave(); + }, 250); + }); + } + + // On window resize + window.addEventListener("resize", () => { + const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; + const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; + noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + }); + + return noteElement; + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function drawStickyNotes(stickyNotes, onSave, onDelete) { + // Remove all existing sticky notes + const existingNotes = document.querySelectorAll(".birb-sticky-note"); + existingNotes.forEach(note => note.remove()); + // Render all sticky notes + for (let stickyNote of stickyNotes) { + if (isStickyNoteApplicable(stickyNote)) { + renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + } + } + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function createNewStickyNote(stickyNotes, onSave, onDelete) { + const id = Date.now().toString(); + const site = window.location.href; + const stickyNote = new StickyNote(id, site, ""); + const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; + stickyNote.top = parseInt(element.style.top, 10); + stickyNote.left = parseInt(element.style.left, 10); + stickyNotes.push(stickyNote); + onSave(); + } + + class MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {boolean} [removeMenu] + * @param {boolean} [isDebug] + */ + constructor(text, action, removeMenu = true, isDebug = false) { + this.text = text; + this.action = action; + this.removeMenu = removeMenu; + this.isDebug = isDebug; + } + } + + class DebugMenuItem extends MenuItem { + /** + * @param {string} text + * @param {() => void} action + */ + constructor(text, action, removeMenu = true) { + super(text, action, removeMenu, true); + } + } + + class Separator extends MenuItem { + constructor() { + super("", () => { }); + } + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -417,6 +669,12 @@ function makeElement(className, textContent, id) { const element = document.createElement("div"); element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } return element; } @@ -516,13 +774,7 @@ * @param {Element} [closeButton] */ function makeClosable(func, closeButton) { - if (closeButton) { - onClick(closeButton, func); - } document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { - return; - } if (e.key === "Escape") { func(); } @@ -530,102 +782,108 @@ } /** - * @param {StickyNote} stickyNote - * @param {() => void} onSave - * @param {() => void} onDelete + * @param {MenuItem} item + * @param {() => void} removeMenuCallback * @returns {HTMLElement} */ - function renderStickyNote(stickyNote, onSave, onDelete) { - let html = ` -
-
Sticky Note
-
x
-
-
- -
`; - const noteElement = makeElement("birb-window"); - noteElement.classList.add("birb-sticky-note"); - noteElement.innerHTML = html; - - noteElement.style.top = `${stickyNote.top}px`; - noteElement.style.left = `${stickyNote.left}px`; - document.body.appendChild(noteElement); - - makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { - stickyNote.top = top; - stickyNote.left = left; - onSave(); - }); - - const closeButton = noteElement.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - if (confirm("Are you sure you want to delete this sticky note?")) { - onDelete(); - noteElement.remove(); - } - }, closeButton); + function makeMenuItem(item, removeMenuCallback) { + if (item instanceof Separator) { + return makeElement("birb-window-separator"); } - - const textarea = noteElement.querySelector(".birb-sticky-note-input"); - if (textarea && textarea instanceof HTMLTextAreaElement) { - let saveTimeout; - // Save after debounce - textarea.addEventListener("input", () => { - stickyNote.content = textarea.value; - if (saveTimeout) { - clearTimeout(saveTimeout); - } - saveTimeout = setTimeout(() => { - onSave(); - }, 250); - }); - } - - // On window resize - window.addEventListener("resize", () => { - const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; - const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; - noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + let menuItem = makeElement("birb-menu-item", item.text); + onClick(menuItem, () => { + if (item.removeMenu) { + removeMenuCallback(); + } + item.action(); }); - - return noteElement; + return menuItem; } /** - * @param {StickyNote[]} stickyNotes - * @param {() => void} onSave - * @param {(note: StickyNote) => void} onDelete + * Add the menu to the page if it doesn't already exist + * @param {string} menuId + * @param {string} menuExitId + * @param {MenuItem[]} menuItems + * @param {string} title + * @param {boolean} debugMode + * @param {(menu: HTMLElement) => void} updateLocationCallback */ - function drawStickyNotes(stickyNotes, onSave, onDelete) { - // Remove all existing sticky notes - const existingNotes = document.querySelectorAll(".birb-sticky-note"); - existingNotes.forEach(note => note.remove()); - // Render all sticky notes - for (let stickyNote of stickyNotes) { - if (isStickyNoteApplicable(stickyNote)) { - renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); + function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) { + if (document.querySelector("#" + menuId)) { + return; + } + let menu = makeElement("birb-window", undefined, menuId); + let header = makeElement("birb-window-header"); + header.innerHTML = `
${title}
`; + let content = makeElement("birb-window-content"); + const removeCallback = () => removeMenu(menuId, menuExitId); + for (const item of menuItems) { + if (!item.isDebug || debugMode) { + content.appendChild(makeMenuItem(item, removeCallback)); } } + menu.appendChild(header); + menu.appendChild(content); + document.body.appendChild(menu); + makeDraggable(document.querySelector(".birb-window-header")); + + let menuExit = makeElement("birb-window-exit", undefined, menuExitId); + onClick(menuExit, removeCallback); + document.body.appendChild(menuExit); + makeClosable(removeCallback); + + updateLocationCallback(menu); } /** - * @param {StickyNote[]} stickyNotes - * @param {() => void} onSave - * @param {(note: StickyNote) => void} onDelete + * Remove the menu from the page + * @param {string} menuId + * @param {string} menuExitId */ - function createNewStickyNote(stickyNotes, onSave, onDelete) { - const id = Date.now().toString(); - const site = window.location.href; - const stickyNote = new StickyNote(id, site, ""); - const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote)); - element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; - element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`; - stickyNote.top = parseInt(element.style.top, 10); - stickyNote.left = parseInt(element.style.left, 10); - stickyNotes.push(stickyNote); - onSave(); + function removeMenu(menuId, menuExitId) { + const menu = document.querySelector("#" + menuId); + if (menu) { + menu.remove(); + } + const exitMenu = document.querySelector("#" + menuExitId); + if (exitMenu) { + exitMenu.remove(); + } + } + + /** + * @param {string} menuId + * @returns {boolean} Whether the menu element is on the page + */ + function isMenuOpen(menuId) { + return document.querySelector("#" + menuId) !== null; + } + + /** + * @param {string} menuId + * @param {MenuItem[]} menuItems + * @param {boolean} debugMode + * @param {(menu: HTMLElement) => void} updateLocationCallback + */ + function switchMenuItems(menuId, menuItems, debugMode, updateLocationCallback) { + const menu = document.querySelector("#" + menuId); + if (!menu || !(menu instanceof HTMLElement)) { + return; + } + const content = menu.querySelector(".birb-window-content"); + if (!content) { + console.error("Birb: Content not found"); + return; + } + content.innerHTML = ""; + const removeCallback = () => removeMenu(menuId, menuId + "-exit"); + for (const item of menuItems) { + if (!item.isDebug || debugMode) { + content.appendChild(makeMenuItem(item, removeCallback)); + } + } + updateLocationCallback(menu); } // @ts-ignore @@ -1220,37 +1478,6 @@ ]), }; - class MenuItem { - /** - * @param {string} text - * @param {() => void} action - * @param {boolean} [removeMenu] - * @param {boolean} [isDebug] - */ - constructor(text, action, removeMenu = true, isDebug = false) { - this.text = text; - this.action = action; - this.removeMenu = removeMenu; - this.isDebug = isDebug; - } - } - - class DebugMenuItem extends MenuItem { - /** - * @param {string} text - * @param {() => void} action - */ - constructor(text, action, removeMenu = true) { - super(text, action, removeMenu, true); - } - } - - class Separator extends MenuItem { - constructor() { - super("", () => { }); - } - } - const menuItems = [ new MenuItem(`Pet ${birdBirb()}`, pet), new MenuItem("Field Guide", insertFieldGuide), @@ -1269,11 +1496,11 @@ debugMode = false; }), new Separator(), - new MenuItem("Settings", () => switchMenuItems(settingsItems), false), + new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false), ]; const settingsItems = [ - new MenuItem("Go Back", () => switchMenuItems(menuItems), false), + new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false), new Separator(), new MenuItem("Toggle Birb Mode", () => { userSettings.birbMode = !userSettings.birbMode; @@ -1478,7 +1705,7 @@ onClick(document, (e) => { lastActionTimestamp = Date.now(); if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { - removeMenu(); + removeMenu(MENU_ID, MENU_EXIT_ID); } }); @@ -1487,7 +1714,7 @@ // Currently being pet, don't open menu return; } - insertMenu(); + insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation); }); canvas.addEventListener("mouseover", () => { @@ -1534,7 +1761,7 @@ // Won't be restored on fullscreen exit } - if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (currentState === States.IDLE && !frozen && !isMenuOpen(MENU_ID)) { if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { @@ -1623,9 +1850,6 @@ function makeElement(className, textContent, id) { const element = document.createElement("div"); element.classList.add(className); - if (textContent) { - element.textContent = textContent; - } if (id) { element.id = id; } @@ -1764,6 +1988,30 @@ centerElement(modal); } + /** + * @param {HTMLElement} menu + */ + function updateMenuLocation(menu) { + let x = birdX; + let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; + const offset = 20; + if (x < window.innerWidth / 2) { + // Left side + x += offset; + } else { + // Right side + x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; + } + if (y > window.innerHeight / 2) { + // Top side + y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; + } else { + // Bottom side + y += offset; + } + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + } function insertFieldGuide() { if (document.querySelector("#" + FIELD_GUIDE_ID)) { return; @@ -1879,103 +2127,6 @@ save(); } - /** - * Add the menu to the page if it doesn't already exist - */ - function insertMenu() { - if (document.querySelector("#" + MENU_ID)) { - return; - } - let menu = makeElement("birb-window", undefined, MENU_ID); - let header = makeElement("birb-window-header"); - header.innerHTML = `
${birdBirb().toLowerCase()}OS
`; - let content = makeElement("birb-window-content"); - for (const item of menuItems) { - if (!item.isDebug || debugMode) { - content.appendChild(makeMenuItem(item)); - } - } - menu.appendChild(header); - menu.appendChild(content); - document.body.appendChild(menu); - makeDraggable(document.querySelector(".birb-window-header")); - - let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); - onClick(menuExit, () => { - removeMenu(); - }); - document.body.appendChild(menuExit); - makeClosable(removeMenu); - - updateMenuLocation(menu); - } - - /** - * Update the menu's location based on the bird's position - * @param {HTMLElement} menu - */ - function updateMenuLocation(menu) { - let x = birdX; - let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; - const offset = 20; - if (x < window.innerWidth / 2) { - // Left side - x += offset; - } else { - // Right side - x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; - } - if (y > window.innerHeight / 2) { - // Top side - y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; - } else { - // Bottom side - y += offset; - } - menu.style.left = `${x}px`; - menu.style.top = `${y}px`; - } - - /** - * @param {MenuItem[]} menuItems - */ - function switchMenuItems(menuItems) { - const menu = document.querySelector("#" + MENU_ID); - if (!menu || !(menu instanceof HTMLElement)) { - return; - } - const content = menu.querySelector(".birb-window-content"); - if (!content) { - error("Content not found"); - return; - } - content.innerHTML = ""; - for (const item of menuItems) { - if (!item.isDebug || debugMode) { - content.appendChild(makeMenuItem(item)); - } - } - updateMenuLocation(menu); - } - - /** - * @param {MenuItem} item - * @returns {HTMLElement} - */ - function makeMenuItem(item) { - if (item instanceof Separator) { - return makeElement("birb-window-separator"); - } - let menuItem = makeElement("birb-menu-item", item.text); - onClick(menuItem, () => { - if (item.removeMenu) { - removeMenu(); - } - item.action(); - }); - return menuItem; - } - /** * @param {Document|Element} element * @param {(e: Event) => void} action @@ -2001,27 +2152,6 @@ }); } - /** - * Remove the menu from the page - */ - function removeMenu() { - const menu = document.querySelector("#" + MENU_ID); - if (menu) { - menu.remove(); - } - const exitMenu = document.querySelector("#" + MENU_EXIT_ID); - if (exitMenu) { - exitMenu.remove(); - } - } - - /** - * @returns {boolean} Whether the menu element is on the page - */ - function isMenuOpen() { - return document.querySelector("#" + MENU_ID) !== null; - } - /** * @param {HTMLElement|null} element The element to detect drag events on * @param {boolean} [parent] Whether to move the parent element when the child is dragged diff --git a/manifest.json b/manifest.json index 894f595..e11775f 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.76", + "version": "2025.10.26.101", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/birb.js b/src/birb.js index 2484b25..aa3d425 100644 --- a/src/birb.js +++ b/src/birb.js @@ -13,6 +13,7 @@ import Frame from './frame.js'; import Layer from './layer.js'; import Anim from './anim.js'; import { StickyNote, createNewStickyNote, drawStickyNotes } from './stickyNotes.js'; +import { MenuItem, DebugMenuItem, Separator, insertMenu, removeMenu, isMenuOpen, switchMenuItems } from './menu.js'; // @ts-ignore const SHARED_CONFIG = { @@ -271,37 +272,6 @@ Promise.all([ ]), }; - class MenuItem { - /** - * @param {string} text - * @param {() => void} action - * @param {boolean} [removeMenu] - * @param {boolean} [isDebug] - */ - constructor(text, action, removeMenu = true, isDebug = false) { - this.text = text; - this.action = action; - this.removeMenu = removeMenu; - this.isDebug = isDebug; - } - } - - class DebugMenuItem extends MenuItem { - /** - * @param {string} text - * @param {() => void} action - */ - constructor(text, action, removeMenu = true) { - super(text, action, removeMenu, true); - } - } - - class Separator extends MenuItem { - constructor() { - super("", () => { }); - } - } - const menuItems = [ new MenuItem(`Pet ${birdBirb()}`, pet), new MenuItem("Field Guide", insertFieldGuide), @@ -320,11 +290,11 @@ Promise.all([ debugMode = false; }), new Separator(), - new MenuItem("Settings", () => switchMenuItems(settingsItems), false), + new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false), ]; const settingsItems = [ - new MenuItem("Go Back", () => switchMenuItems(menuItems), false), + new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false), new Separator(), new MenuItem("Toggle Birb Mode", () => { userSettings.birbMode = !userSettings.birbMode; @@ -529,7 +499,7 @@ Promise.all([ onClick(document, (e) => { lastActionTimestamp = Date.now(); if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { - removeMenu(); + removeMenu(MENU_ID, MENU_EXIT_ID); } }); @@ -538,7 +508,7 @@ Promise.all([ // Currently being pet, don't open menu return; } - insertMenu(); + insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation); }); canvas.addEventListener("mouseover", () => { @@ -585,7 +555,7 @@ Promise.all([ // Won't be restored on fullscreen exit } - if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (currentState === States.IDLE && !frozen && !isMenuOpen(MENU_ID)) { if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { @@ -835,6 +805,31 @@ Promise.all([ centerElement(modal); } + /** + * @param {HTMLElement} menu + */ + function updateMenuLocation(menu) { + let x = birdX; + let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; + const offset = 20; + if (x < window.innerWidth / 2) { + // Left side + x += offset; + } else { + // Right side + x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; + } + if (y > window.innerHeight / 2) { + // Top side + y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; + } else { + // Bottom side + y += offset; + } + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + }; + function insertFieldGuide() { if (document.querySelector("#" + FIELD_GUIDE_ID)) { return; @@ -954,103 +949,6 @@ Promise.all([ save(); } - /** - * Add the menu to the page if it doesn't already exist - */ - function insertMenu() { - if (document.querySelector("#" + MENU_ID)) { - return; - } - let menu = makeElement("birb-window", undefined, MENU_ID); - let header = makeElement("birb-window-header"); - header.innerHTML = `
${birdBirb().toLowerCase()}OS
`; - let content = makeElement("birb-window-content"); - for (const item of menuItems) { - if (!item.isDebug || debugMode) { - content.appendChild(makeMenuItem(item)); - } - } - menu.appendChild(header); - menu.appendChild(content); - document.body.appendChild(menu); - makeDraggable(document.querySelector(".birb-window-header")); - - let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); - onClick(menuExit, () => { - removeMenu(); - }); - document.body.appendChild(menuExit); - makeClosable(removeMenu); - - updateMenuLocation(menu); - } - - /** - * Update the menu's location based on the bird's position - * @param {HTMLElement} menu - */ - function updateMenuLocation(menu) { - let x = birdX; - let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; - const offset = 20; - if (x < window.innerWidth / 2) { - // Left side - x += offset; - } else { - // Right side - x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; - } - if (y > window.innerHeight / 2) { - // Top side - y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; - } else { - // Bottom side - y += offset; - } - menu.style.left = `${x}px`; - menu.style.top = `${y}px`; - } - - /** - * @param {MenuItem[]} menuItems - */ - function switchMenuItems(menuItems) { - const menu = document.querySelector("#" + MENU_ID); - if (!menu || !(menu instanceof HTMLElement)) { - return; - } - const content = menu.querySelector(".birb-window-content"); - if (!content) { - error("Content not found"); - return; - } - content.innerHTML = ""; - for (const item of menuItems) { - if (!item.isDebug || debugMode) { - content.appendChild(makeMenuItem(item)); - } - } - updateMenuLocation(menu); - } - - /** - * @param {MenuItem} item - * @returns {HTMLElement} - */ - function makeMenuItem(item) { - if (item instanceof Separator) { - return makeElement("birb-window-separator"); - } - let menuItem = makeElement("birb-menu-item", item.text); - onClick(menuItem, () => { - if (item.removeMenu) { - removeMenu(); - } - item.action(); - }); - return menuItem; - } - /** * @param {Document|Element} element * @param {(e: Event) => void} action @@ -1076,27 +974,6 @@ Promise.all([ }); } - /** - * Remove the menu from the page - */ - function removeMenu() { - const menu = document.querySelector("#" + MENU_ID); - if (menu) { - menu.remove(); - } - const exitMenu = document.querySelector("#" + MENU_EXIT_ID); - if (exitMenu) { - exitMenu.remove(); - } - } - - /** - * @returns {boolean} Whether the menu element is on the page - */ - function isMenuOpen() { - return document.querySelector("#" + MENU_ID) !== null; - } - /** * @param {HTMLElement|null} element The element to detect drag events on * @param {boolean} [parent] Whether to move the parent element when the child is dragged diff --git a/src/menu.js b/src/menu.js new file mode 100644 index 0000000..73f4e6f --- /dev/null +++ b/src/menu.js @@ -0,0 +1,263 @@ +export class MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {boolean} [removeMenu] + * @param {boolean} [isDebug] + */ + constructor(text, action, removeMenu = true, isDebug = false) { + this.text = text; + this.action = action; + this.removeMenu = removeMenu; + this.isDebug = isDebug; + } +} + +export class DebugMenuItem extends MenuItem { + /** + * @param {string} text + * @param {() => void} action + */ + constructor(text, action, removeMenu = true) { + super(text, action, removeMenu, true); + } +} + +export class Separator extends MenuItem { + constructor() { + super("", () => { }); + } +} + +/** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ +function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } + return element; +} + +/** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ +function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); +} + +/** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ +function makeDraggable(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + console.error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); +} + +/** + * @param {() => void} func + * @param {Element} [closeButton] + */ +function makeClosable(func, closeButton) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); +} + +/** + * @param {MenuItem} item + * @param {() => void} removeMenuCallback + * @returns {HTMLElement} + */ +function makeMenuItem(item, removeMenuCallback) { + if (item instanceof Separator) { + return makeElement("birb-window-separator"); + } + let menuItem = makeElement("birb-menu-item", item.text); + onClick(menuItem, () => { + if (item.removeMenu) { + removeMenuCallback(); + } + item.action(); + }); + return menuItem; +} + +/** + * Add the menu to the page if it doesn't already exist + * @param {string} menuId + * @param {string} menuExitId + * @param {MenuItem[]} menuItems + * @param {string} title + * @param {boolean} debugMode + * @param {(menu: HTMLElement) => void} updateLocationCallback + */ +export function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) { + if (document.querySelector("#" + menuId)) { + return; + } + let menu = makeElement("birb-window", undefined, menuId); + let header = makeElement("birb-window-header"); + header.innerHTML = `
${title}
`; + let content = makeElement("birb-window-content"); + const removeCallback = () => removeMenu(menuId, menuExitId); + for (const item of menuItems) { + if (!item.isDebug || debugMode) { + content.appendChild(makeMenuItem(item, removeCallback)); + } + } + menu.appendChild(header); + menu.appendChild(content); + document.body.appendChild(menu); + makeDraggable(document.querySelector(".birb-window-header")); + + let menuExit = makeElement("birb-window-exit", undefined, menuExitId); + onClick(menuExit, removeCallback); + document.body.appendChild(menuExit); + makeClosable(removeCallback); + + updateLocationCallback(menu); +} + +/** + * Remove the menu from the page + * @param {string} menuId + * @param {string} menuExitId + */ +export function removeMenu(menuId, menuExitId) { + const menu = document.querySelector("#" + menuId); + if (menu) { + menu.remove(); + } + const exitMenu = document.querySelector("#" + menuExitId); + if (exitMenu) { + exitMenu.remove(); + } +} + +/** + * @param {string} menuId + * @returns {boolean} Whether the menu element is on the page + */ +export function isMenuOpen(menuId) { + return document.querySelector("#" + menuId) !== null; +} + +/** + * @param {string} menuId + * @param {MenuItem[]} menuItems + * @param {boolean} debugMode + * @param {(menu: HTMLElement) => void} updateLocationCallback + */ +export function switchMenuItems(menuId, menuItems, debugMode, updateLocationCallback) { + const menu = document.querySelector("#" + menuId); + if (!menu || !(menu instanceof HTMLElement)) { + return; + } + const content = menu.querySelector(".birb-window-content"); + if (!content) { + console.error("Birb: Content not found"); + return; + } + content.innerHTML = ""; + const removeCallback = () => removeMenu(menuId, menuId + "-exit"); + for (const item of menuItems) { + if (!item.isDebug || debugMode) { + content.appendChild(makeMenuItem(item, removeCallback)); + } + } + updateLocationCallback(menu); +} From 7c146aa57570bb5ef13b134174ab5deecfe34881 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 15:47:51 -0400 Subject: [PATCH 09/16] Move menu constants to menu section --- dist/birb.js | 80 ++++++++++++++++++++++------------------- dist/birb.user.js | 82 ++++++++++++++++++++++-------------------- manifest.json | 2 +- src/Frame.js | 2 +- src/birb.js | 61 +++++++++++++++++-------------- src/frame.js | 2 +- src/menu.js | 43 +++++++++++----------- src/shared.js | 20 +++++++++++ src/sharedConstants.js | 4 --- 9 files changed, 164 insertions(+), 132 deletions(-) create mode 100644 src/shared.js delete mode 100644 src/sharedConstants.js diff --git a/dist/birb.js b/dist/birb.js index fa30024..5ac7341 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -6,6 +6,22 @@ RIGHT: 1, }; + let debug$1 = location.hostname === "127.0.0.1"; + + /** + * @returns {boolean} Whether debug mode is enabled + */ + function isDebug() { + return debug$1; + } + + /** + * @param {boolean} debugMode + */ + function setDebug(debugMode) { + debug$1 = debugMode; + } + /** Indicators for parts of the base bird sprite sheet */ const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", @@ -614,6 +630,9 @@ onSave(); } + const MENU_ID = "birb-menu"; + const MENU_EXIT_ID = "birb-menu-exit"; + class MenuItem { /** * @param {string} text @@ -788,24 +807,21 @@ /** * Add the menu to the page if it doesn't already exist - * @param {string} menuId - * @param {string} menuExitId * @param {MenuItem[]} menuItems * @param {string} title - * @param {boolean} debugMode * @param {(menu: HTMLElement) => void} updateLocationCallback */ - function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) { - if (document.querySelector("#" + menuId)) { + function insertMenu(menuItems, title, updateLocationCallback) { + if (document.querySelector("#" + MENU_ID)) { return; } - let menu = makeElement("birb-window", undefined, menuId); + let menu = makeElement("birb-window", undefined, MENU_ID); let header = makeElement("birb-window-header"); header.innerHTML = `
${title}
`; let content = makeElement("birb-window-content"); - const removeCallback = () => removeMenu(menuId, menuExitId); + const removeCallback = () => removeMenu(); for (const item of menuItems) { - if (!item.isDebug || debugMode) { + if (!item.isDebug || isDebug()) { content.appendChild(makeMenuItem(item, removeCallback)); } } @@ -814,7 +830,7 @@ document.body.appendChild(menu); makeDraggable(document.querySelector(".birb-window-header")); - let menuExit = makeElement("birb-window-exit", undefined, menuExitId); + let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); onClick(menuExit, removeCallback); document.body.appendChild(menuExit); makeClosable(removeCallback); @@ -824,36 +840,31 @@ /** * Remove the menu from the page - * @param {string} menuId - * @param {string} menuExitId */ - function removeMenu(menuId, menuExitId) { - const menu = document.querySelector("#" + menuId); + function removeMenu() { + const menu = document.querySelector("#" + MENU_ID); if (menu) { menu.remove(); } - const exitMenu = document.querySelector("#" + menuExitId); + const exitMenu = document.querySelector("#" + MENU_EXIT_ID); if (exitMenu) { exitMenu.remove(); } } /** - * @param {string} menuId * @returns {boolean} Whether the menu element is on the page */ - function isMenuOpen(menuId) { - return document.querySelector("#" + menuId) !== null; + function isMenuOpen() { + return document.querySelector("#" + MENU_ID) !== null; } /** - * @param {string} menuId * @param {MenuItem[]} menuItems - * @param {boolean} debugMode * @param {(menu: HTMLElement) => void} updateLocationCallback */ - function switchMenuItems(menuId, menuItems, debugMode, updateLocationCallback) { - const menu = document.querySelector("#" + menuId); + function switchMenuItems(menuItems, updateLocationCallback) { + const menu = document.querySelector("#" + MENU_ID); if (!menu || !(menu instanceof HTMLElement)) { return; } @@ -863,9 +874,9 @@ return; } content.innerHTML = ""; - const removeCallback = () => removeMenu(menuId, menuId + "-exit"); + const removeCallback = () => removeMenu(); for (const item of menuItems) { - if (!item.isDebug || debugMode) { + if (!item.isDebug || isDebug()) { content.appendChild(makeMenuItem(item, removeCallback)); } } @@ -892,7 +903,6 @@ const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG }; - let debugMode = location.hostname === "127.0.0.1"; let frozen = false; const BIRB_CSS_SCALE = CONFIG.birbCssScale; @@ -1279,8 +1289,6 @@ 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 MENU_ID = "birb-menu"; - const MENU_EXIT_ID = "birb-menu-exit"; const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; @@ -1290,7 +1298,7 @@ /** 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 AFK_TIME = isDebug() ? 0 : 1000 * 30; const UPDATE_INTERVAL = 1000 / 60; // 60 FPS // Per-frame chances const HOP_CHANCE = 1 / (60 * 3); // 3 seconds @@ -1479,14 +1487,14 @@ } }), new DebugMenuItem("Disable Debug", () => { - debugMode = false; + setDebug(false); }), new Separator(), - new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false), + new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), false), ]; const settingsItems = [ - new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false), + new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), new Separator(), new MenuItem("Toggle Birb Mode", () => { userSettings.birbMode = !userSettings.birbMode; @@ -1691,7 +1699,7 @@ onClick(document, (e) => { lastActionTimestamp = Date.now(); if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { - removeMenu(MENU_ID, MENU_EXIT_ID); + removeMenu(); } }); @@ -1700,10 +1708,8 @@ // Currently being pet, don't open menu return; } - insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation); - }); - - canvas.addEventListener("mouseover", () => { + insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation); + }); canvas.addEventListener("mouseover", () => { lastActionTimestamp = Date.now(); if (currentState === States.IDLE) { petStack.push(Date.now()); @@ -1747,7 +1753,7 @@ // Won't be restored on fullscreen exit } - if (currentState === States.IDLE && !frozen && !isMenuOpen(MENU_ID)) { + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { @@ -2459,7 +2465,7 @@ } function debug() { - if (debugMode) { + if (isDebug()) { console.debug("Birb: ", ...arguments); } } diff --git a/dist/birb.user.js b/dist/birb.user.js index 2800cc6..672d46d 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.101 +// @version 2025.10.26.163 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -20,6 +20,22 @@ RIGHT: 1, }; + let debug$1 = location.hostname === "127.0.0.1"; + + /** + * @returns {boolean} Whether debug mode is enabled + */ + function isDebug() { + return debug$1; + } + + /** + * @param {boolean} debugMode + */ + function setDebug(debugMode) { + debug$1 = debugMode; + } + /** Indicators for parts of the base bird sprite sheet */ const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", @@ -628,6 +644,9 @@ onSave(); } + const MENU_ID = "birb-menu"; + const MENU_EXIT_ID = "birb-menu-exit"; + class MenuItem { /** * @param {string} text @@ -802,24 +821,21 @@ /** * Add the menu to the page if it doesn't already exist - * @param {string} menuId - * @param {string} menuExitId * @param {MenuItem[]} menuItems * @param {string} title - * @param {boolean} debugMode * @param {(menu: HTMLElement) => void} updateLocationCallback */ - function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) { - if (document.querySelector("#" + menuId)) { + function insertMenu(menuItems, title, updateLocationCallback) { + if (document.querySelector("#" + MENU_ID)) { return; } - let menu = makeElement("birb-window", undefined, menuId); + let menu = makeElement("birb-window", undefined, MENU_ID); let header = makeElement("birb-window-header"); header.innerHTML = `
${title}
`; let content = makeElement("birb-window-content"); - const removeCallback = () => removeMenu(menuId, menuExitId); + const removeCallback = () => removeMenu(); for (const item of menuItems) { - if (!item.isDebug || debugMode) { + if (!item.isDebug || isDebug()) { content.appendChild(makeMenuItem(item, removeCallback)); } } @@ -828,7 +844,7 @@ document.body.appendChild(menu); makeDraggable(document.querySelector(".birb-window-header")); - let menuExit = makeElement("birb-window-exit", undefined, menuExitId); + let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); onClick(menuExit, removeCallback); document.body.appendChild(menuExit); makeClosable(removeCallback); @@ -838,36 +854,31 @@ /** * Remove the menu from the page - * @param {string} menuId - * @param {string} menuExitId */ - function removeMenu(menuId, menuExitId) { - const menu = document.querySelector("#" + menuId); + function removeMenu() { + const menu = document.querySelector("#" + MENU_ID); if (menu) { menu.remove(); } - const exitMenu = document.querySelector("#" + menuExitId); + const exitMenu = document.querySelector("#" + MENU_EXIT_ID); if (exitMenu) { exitMenu.remove(); } } /** - * @param {string} menuId * @returns {boolean} Whether the menu element is on the page */ - function isMenuOpen(menuId) { - return document.querySelector("#" + menuId) !== null; + function isMenuOpen() { + return document.querySelector("#" + MENU_ID) !== null; } /** - * @param {string} menuId * @param {MenuItem[]} menuItems - * @param {boolean} debugMode * @param {(menu: HTMLElement) => void} updateLocationCallback */ - function switchMenuItems(menuId, menuItems, debugMode, updateLocationCallback) { - const menu = document.querySelector("#" + menuId); + function switchMenuItems(menuItems, updateLocationCallback) { + const menu = document.querySelector("#" + MENU_ID); if (!menu || !(menu instanceof HTMLElement)) { return; } @@ -877,9 +888,9 @@ return; } content.innerHTML = ""; - const removeCallback = () => removeMenu(menuId, menuId + "-exit"); + const removeCallback = () => removeMenu(); for (const item of menuItems) { - if (!item.isDebug || debugMode) { + if (!item.isDebug || isDebug()) { content.appendChild(makeMenuItem(item, removeCallback)); } } @@ -906,7 +917,6 @@ const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG }; - let debugMode = location.hostname === "127.0.0.1"; let frozen = false; const BIRB_CSS_SCALE = CONFIG.birbCssScale; @@ -1293,8 +1303,6 @@ 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 MENU_ID = "birb-menu"; - const MENU_EXIT_ID = "birb-menu-exit"; const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; @@ -1304,7 +1312,7 @@ /** 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 AFK_TIME = isDebug() ? 0 : 1000 * 30; const UPDATE_INTERVAL = 1000 / 60; // 60 FPS // Per-frame chances const HOP_CHANCE = 1 / (60 * 3); // 3 seconds @@ -1493,14 +1501,14 @@ } }), new DebugMenuItem("Disable Debug", () => { - debugMode = false; + setDebug(false); }), new Separator(), - new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false), + new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), false), ]; const settingsItems = [ - new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false), + new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), new Separator(), new MenuItem("Toggle Birb Mode", () => { userSettings.birbMode = !userSettings.birbMode; @@ -1705,7 +1713,7 @@ onClick(document, (e) => { lastActionTimestamp = Date.now(); if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { - removeMenu(MENU_ID, MENU_EXIT_ID); + removeMenu(); } }); @@ -1714,10 +1722,8 @@ // Currently being pet, don't open menu return; } - insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation); - }); - - canvas.addEventListener("mouseover", () => { + insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation); + }); canvas.addEventListener("mouseover", () => { lastActionTimestamp = Date.now(); if (currentState === States.IDLE) { petStack.push(Date.now()); @@ -1761,7 +1767,7 @@ // Won't be restored on fullscreen exit } - if (currentState === States.IDLE && !frozen && !isMenuOpen(MENU_ID)) { + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { @@ -2473,7 +2479,7 @@ } function debug() { - if (debugMode) { + if (isDebug()) { console.debug("Birb: ", ...arguments); } } diff --git a/manifest.json b/manifest.json index e11775f..53fd3c7 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.101", + "version": "2025.10.26.163", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/Frame.js b/src/Frame.js index f9dd7e0..75afc1c 100644 --- a/src/Frame.js +++ b/src/Frame.js @@ -1,4 +1,4 @@ -import { Directions } from './sharedConstants.js'; +import { Directions } from './shared.js'; import { SPRITE, BirdType } from './sprites.js'; import Layer from './layer.js'; diff --git a/src/birb.js b/src/birb.js index aa3d425..0cb3ba3 100644 --- a/src/birb.js +++ b/src/birb.js @@ -1,19 +1,31 @@ -import { - Directions -} from './sharedConstants.js'; - -import { - SPRITE, - SPRITE_SHEET_COLOR_MAP, - SPECIES, - BirdType -} from './sprites.js'; - import Frame from './frame.js'; import Layer from './layer.js'; import Anim from './anim.js'; -import { StickyNote, createNewStickyNote, drawStickyNotes } from './stickyNotes.js'; -import { MenuItem, DebugMenuItem, Separator, insertMenu, removeMenu, isMenuOpen, switchMenuItems } from './menu.js'; +import { + Directions, + isDebug, + setDebug +} from './shared.js'; +import { + SPRITE, + SPRITE_SHEET_COLOR_MAP, + SPECIES +} from './sprites.js'; +import { + StickyNote, + createNewStickyNote, + drawStickyNotes +} from './stickyNotes.js'; +import { + MenuItem, + DebugMenuItem, + Separator, + insertMenu, + removeMenu, + isMenuOpen, + switchMenuItems, + MENU_EXIT_ID +} from './menu.js'; // @ts-ignore const SHARED_CONFIG = { @@ -35,7 +47,6 @@ const MOBILE_CONFIG = { const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG }; -let debugMode = location.hostname === "127.0.0.1"; let frozen = false; const BIRB_CSS_SCALE = CONFIG.birbCssScale; @@ -79,8 +90,6 @@ const SPRITE_SHEET = "__SPRITE_SHEET__"; const DECORATIONS_SPRITE_SHEET = "__DECORATIONS_SPRITE_SHEET__"; const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__"; -const MENU_ID = "birb-menu"; -const MENU_EXIT_ID = "birb-menu-exit"; const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; @@ -90,7 +99,7 @@ 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 AFK_TIME = isDebug() ? 0 : 1000 * 30; const UPDATE_INTERVAL = 1000 / 60; // 60 FPS // Per-frame chances const HOP_CHANCE = 1 / (60 * 3); // 3 seconds @@ -287,14 +296,14 @@ Promise.all([ } }), new DebugMenuItem("Disable Debug", () => { - debugMode = false; + setDebug(false); }), new Separator(), - new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false), + new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), false), ]; const settingsItems = [ - new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false), + new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), new Separator(), new MenuItem("Toggle Birb Mode", () => { userSettings.birbMode = !userSettings.birbMode; @@ -499,7 +508,7 @@ Promise.all([ onClick(document, (e) => { lastActionTimestamp = Date.now(); if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { - removeMenu(MENU_ID, MENU_EXIT_ID); + removeMenu(); } }); @@ -508,10 +517,8 @@ Promise.all([ // Currently being pet, don't open menu return; } - insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation); - }); - - canvas.addEventListener("mouseover", () => { + insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation); + }); canvas.addEventListener("mouseover", () => { lastActionTimestamp = Date.now(); if (currentState === States.IDLE) { petStack.push(Date.now()); @@ -555,7 +562,7 @@ Promise.all([ // Won't be restored on fullscreen exit } - if (currentState === States.IDLE && !frozen && !isMenuOpen(MENU_ID)) { + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { @@ -1299,7 +1306,7 @@ function log() { } function debug() { - if (debugMode) { + if (isDebug()) { console.debug("Birb: ", ...arguments); } } diff --git a/src/frame.js b/src/frame.js index f9dd7e0..75afc1c 100644 --- a/src/frame.js +++ b/src/frame.js @@ -1,4 +1,4 @@ -import { Directions } from './sharedConstants.js'; +import { Directions } from './shared.js'; import { SPRITE, BirdType } from './sprites.js'; import Layer from './layer.js'; diff --git a/src/menu.js b/src/menu.js index 73f4e6f..bd1e3b8 100644 --- a/src/menu.js +++ b/src/menu.js @@ -1,3 +1,8 @@ +import { isDebug } from './shared.js'; + +export const MENU_ID = "birb-menu"; +export const MENU_EXIT_ID = "birb-menu-exit"; + export class MenuItem { /** * @param {string} text @@ -178,24 +183,21 @@ function makeMenuItem(item, removeMenuCallback) { /** * Add the menu to the page if it doesn't already exist - * @param {string} menuId - * @param {string} menuExitId * @param {MenuItem[]} menuItems * @param {string} title - * @param {boolean} debugMode * @param {(menu: HTMLElement) => void} updateLocationCallback */ -export function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) { - if (document.querySelector("#" + menuId)) { +export function insertMenu(menuItems, title, updateLocationCallback) { + if (document.querySelector("#" + MENU_ID)) { return; } - let menu = makeElement("birb-window", undefined, menuId); + let menu = makeElement("birb-window", undefined, MENU_ID); let header = makeElement("birb-window-header"); header.innerHTML = `
${title}
`; let content = makeElement("birb-window-content"); - const removeCallback = () => removeMenu(menuId, menuExitId); + const removeCallback = () => removeMenu(); for (const item of menuItems) { - if (!item.isDebug || debugMode) { + if (!item.isDebug || isDebug()) { content.appendChild(makeMenuItem(item, removeCallback)); } } @@ -204,7 +206,7 @@ export function insertMenu(menuId, menuExitId, menuItems, title, debugMode, upda document.body.appendChild(menu); makeDraggable(document.querySelector(".birb-window-header")); - let menuExit = makeElement("birb-window-exit", undefined, menuExitId); + let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); onClick(menuExit, removeCallback); document.body.appendChild(menuExit); makeClosable(removeCallback); @@ -214,36 +216,31 @@ export function insertMenu(menuId, menuExitId, menuItems, title, debugMode, upda /** * Remove the menu from the page - * @param {string} menuId - * @param {string} menuExitId */ -export function removeMenu(menuId, menuExitId) { - const menu = document.querySelector("#" + menuId); +export function removeMenu() { + const menu = document.querySelector("#" + MENU_ID); if (menu) { menu.remove(); } - const exitMenu = document.querySelector("#" + menuExitId); + const exitMenu = document.querySelector("#" + MENU_EXIT_ID); if (exitMenu) { exitMenu.remove(); } } /** - * @param {string} menuId * @returns {boolean} Whether the menu element is on the page */ -export function isMenuOpen(menuId) { - return document.querySelector("#" + menuId) !== null; +export function isMenuOpen() { + return document.querySelector("#" + MENU_ID) !== null; } /** - * @param {string} menuId * @param {MenuItem[]} menuItems - * @param {boolean} debugMode * @param {(menu: HTMLElement) => void} updateLocationCallback */ -export function switchMenuItems(menuId, menuItems, debugMode, updateLocationCallback) { - const menu = document.querySelector("#" + menuId); +export function switchMenuItems(menuItems, updateLocationCallback) { + const menu = document.querySelector("#" + MENU_ID); if (!menu || !(menu instanceof HTMLElement)) { return; } @@ -253,9 +250,9 @@ export function switchMenuItems(menuId, menuItems, debugMode, updateLocationCall return; } content.innerHTML = ""; - const removeCallback = () => removeMenu(menuId, menuId + "-exit"); + const removeCallback = () => removeMenu(); for (const item of menuItems) { - if (!item.isDebug || debugMode) { + if (!item.isDebug || isDebug()) { content.appendChild(makeMenuItem(item, removeCallback)); } } diff --git a/src/shared.js b/src/shared.js new file mode 100644 index 0000000..9aa5d2c --- /dev/null +++ b/src/shared.js @@ -0,0 +1,20 @@ +export const Directions = { + LEFT: -1, + RIGHT: 1, +}; + +let debug = location.hostname === "127.0.0.1"; + +/** + * @returns {boolean} Whether debug mode is enabled + */ +export function isDebug() { + return debug; +} + +/** + * @param {boolean} debugMode + */ +export function setDebug(debugMode) { + debug = debugMode; +} \ No newline at end of file diff --git a/src/sharedConstants.js b/src/sharedConstants.js deleted file mode 100644 index 5bd42fa..0000000 --- a/src/sharedConstants.js +++ /dev/null @@ -1,4 +0,0 @@ -export const Directions = { - LEFT: -1, - RIGHT: 1, -}; \ No newline at end of file From 476c75cecb33808ef5c13815daef791d9e2d0013 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 16:05:00 -0400 Subject: [PATCH 10/16] Remove duplicate code --- dist/birb.js | 503 ++++++++++++-------------------------------- dist/birb.user.js | 505 ++++++++++++--------------------------------- manifest.json | 2 +- src/birb.js | 134 +----------- src/menu.js | 136 +----------- src/shared.js | 128 ++++++++++++ src/stickyNotes.js | 134 +----------- 7 files changed, 410 insertions(+), 1132 deletions(-) diff --git a/dist/birb.js b/dist/birb.js index 5ac7341..11ed0f5 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -22,6 +22,134 @@ debug$1 = debugMode; } + /** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ + function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } + return element; + } + + /** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ + function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); + } + + /** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ + function makeDraggable(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + console.error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); + } + + /** + * @param {() => void} func + * @param {Element} [closeButton] + */ + function makeClosable(func, closeButton) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); + } + /** Indicators for parts of the base bird sprite sheet */ const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", @@ -409,128 +537,6 @@ return true; } - /** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ - function makeElement$1(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - return element; - } - - /** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ - function onClick$1(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); - } - - /** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ - function makeDraggable$1(element, parent = true, callback = () => { }) { - if (!element) { - return; - } - - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - console.error("Birb: Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); - } - - /** - * @param {() => void} func - * @param {Element} [closeButton] - */ - function makeClosable$1(func, closeButton) { - if (closeButton) { - onClick$1(closeButton, func); - } - document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { - return; - } - if (e.key === "Escape") { - func(); - } - }); - } - /** * @param {StickyNote} stickyNote * @param {() => void} onSave @@ -546,7 +552,7 @@
`; - const noteElement = makeElement$1("birb-window"); + const noteElement = makeElement("birb-window"); noteElement.classList.add("birb-sticky-note"); noteElement.innerHTML = html; @@ -554,7 +560,7 @@ noteElement.style.left = `${stickyNote.left}px`; document.body.appendChild(noteElement); - makeDraggable$1(noteElement.querySelector(".birb-window-header"), true, (top, left) => { + makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { stickyNote.top = top; stickyNote.left = left; onSave(); @@ -562,7 +568,7 @@ const closeButton = noteElement.querySelector(".birb-window-close"); if (closeButton) { - makeClosable$1(() => { + makeClosable(() => { if (confirm("Are you sure you want to delete this sticky note?")) { onDelete(); noteElement.remove(); @@ -664,128 +670,6 @@ } } - /** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ - function makeElement(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - if (textContent) { - element.textContent = textContent; - } - if (id) { - element.id = id; - } - return element; - } - - /** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ - function onClick(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); - } - - /** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ - function makeDraggable(element, parent = true, callback = () => { }) { - if (!element) { - return; - } - - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - console.error("Birb: Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); - } - - /** - * @param {() => void} func - * @param {Element} [closeButton] - */ - function makeClosable(func, closeButton) { - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - func(); - } - }); - } - /** * @param {MenuItem} item * @param {() => void} removeMenuCallback @@ -1832,22 +1716,6 @@ save(); } - /** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ - function makeElement(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - if (id) { - element.id = id; - } - return element; - } - /** * Create a window element with header and content * @param {string} id @@ -2084,24 +1952,6 @@ centerElement(fieldGuide); } - /** - * @param {() => void} func - * @param {Element} [closeButton] - */ - function makeClosable(func, closeButton) { - if (closeButton) { - onClick(closeButton, func); - } - document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { - return; - } - if (e.key === "Escape") { - func(); - } - }); - } - function removeFieldGuide() { const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); if (fieldGuide) { @@ -2119,97 +1969,6 @@ save(); } - /** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ - function onClick(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); - } - - /** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ - function makeDraggable(element, parent = true, callback = () => { }) { - if (!element) { - return; - } - - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - error("Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); - } - /** * @param {string[][]} array * @param {number} sprite diff --git a/dist/birb.user.js b/dist/birb.user.js index 672d46d..e86804e 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.163 +// @version 2025.10.26.184 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -36,6 +36,134 @@ debug$1 = debugMode; } + /** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ + function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } + return element; + } + + /** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ + function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); + } + + /** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ + function makeDraggable(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + console.error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); + } + + /** + * @param {() => void} func + * @param {Element} [closeButton] + */ + function makeClosable(func, closeButton) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); + } + /** Indicators for parts of the base bird sprite sheet */ const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", @@ -423,128 +551,6 @@ return true; } - /** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ - function makeElement$1(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - return element; - } - - /** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ - function onClick$1(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); - } - - /** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ - function makeDraggable$1(element, parent = true, callback = () => { }) { - if (!element) { - return; - } - - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - console.error("Birb: Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); - } - - /** - * @param {() => void} func - * @param {Element} [closeButton] - */ - function makeClosable$1(func, closeButton) { - if (closeButton) { - onClick$1(closeButton, func); - } - document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { - return; - } - if (e.key === "Escape") { - func(); - } - }); - } - /** * @param {StickyNote} stickyNote * @param {() => void} onSave @@ -560,7 +566,7 @@
`; - const noteElement = makeElement$1("birb-window"); + const noteElement = makeElement("birb-window"); noteElement.classList.add("birb-sticky-note"); noteElement.innerHTML = html; @@ -568,7 +574,7 @@ noteElement.style.left = `${stickyNote.left}px`; document.body.appendChild(noteElement); - makeDraggable$1(noteElement.querySelector(".birb-window-header"), true, (top, left) => { + makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => { stickyNote.top = top; stickyNote.left = left; onSave(); @@ -576,7 +582,7 @@ const closeButton = noteElement.querySelector(".birb-window-close"); if (closeButton) { - makeClosable$1(() => { + makeClosable(() => { if (confirm("Are you sure you want to delete this sticky note?")) { onDelete(); noteElement.remove(); @@ -678,128 +684,6 @@ } } - /** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ - function makeElement(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - if (textContent) { - element.textContent = textContent; - } - if (id) { - element.id = id; - } - return element; - } - - /** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ - function onClick(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); - } - - /** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ - function makeDraggable(element, parent = true, callback = () => { }) { - if (!element) { - return; - } - - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - console.error("Birb: Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); - } - - /** - * @param {() => void} func - * @param {Element} [closeButton] - */ - function makeClosable(func, closeButton) { - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - func(); - } - }); - } - /** * @param {MenuItem} item * @param {() => void} removeMenuCallback @@ -1846,22 +1730,6 @@ save(); } - /** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ - function makeElement(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - if (id) { - element.id = id; - } - return element; - } - /** * Create a window element with header and content * @param {string} id @@ -2098,24 +1966,6 @@ centerElement(fieldGuide); } - /** - * @param {() => void} func - * @param {Element} [closeButton] - */ - function makeClosable(func, closeButton) { - if (closeButton) { - onClick(closeButton, func); - } - document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { - return; - } - if (e.key === "Escape") { - func(); - } - }); - } - function removeFieldGuide() { const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); if (fieldGuide) { @@ -2133,97 +1983,6 @@ save(); } - /** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ - function onClick(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); - } - - /** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ - function makeDraggable(element, parent = true, callback = () => { }) { - if (!element) { - return; - } - - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - error("Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); - } - /** * @param {string[][]} array * @param {number} sprite diff --git a/manifest.json b/manifest.json index 53fd3c7..f09c262 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.163", + "version": "2025.10.26.184", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/birb.js b/src/birb.js index 0cb3ba3..63dcb31 100644 --- a/src/birb.js +++ b/src/birb.js @@ -4,7 +4,11 @@ import Anim from './anim.js'; import { Directions, isDebug, - setDebug + setDebug, + makeElement, + onClick, + makeDraggable, + makeClosable } from './shared.js'; import { SPRITE, @@ -641,25 +645,6 @@ Promise.all([ save(); } - /** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ - function makeElement(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - if (textContent) { - element.textContent = textContent; - } - if (id) { - element.id = id; - } - return element; - } - /** * Create a window element with header and content * @param {string} id @@ -917,24 +902,6 @@ Promise.all([ centerElement(fieldGuide); } - /** - * @param {() => void} func - * @param {Element} [closeButton] - */ - function makeClosable(func, closeButton) { - if (closeButton) { - onClick(closeButton, func); - } - document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { - return; - } - if (e.key === "Escape") { - func(); - } - }); - } - function removeFieldGuide() { const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); if (fieldGuide) { @@ -956,97 +923,6 @@ Promise.all([ save(); } - /** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ - function onClick(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); - } - - /** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ - function makeDraggable(element, parent = true, callback = () => { }) { - if (!element) { - return; - } - - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - error("Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); - } - /** * @param {string[][]} array * @param {number} sprite diff --git a/src/menu.js b/src/menu.js index bd1e3b8..ba4c659 100644 --- a/src/menu.js +++ b/src/menu.js @@ -1,4 +1,10 @@ -import { isDebug } from './shared.js'; +import { + isDebug, + makeElement, + onClick, + makeDraggable, + makeClosable +} from './shared.js'; export const MENU_ID = "birb-menu"; export const MENU_EXIT_ID = "birb-menu-exit"; @@ -34,134 +40,6 @@ export class Separator extends MenuItem { } } -/** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ -function makeElement(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - if (textContent) { - element.textContent = textContent; - } - if (id) { - element.id = id; - } - return element; -} - -/** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ -function onClick(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); -} - -/** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ -function makeDraggable(element, parent = true, callback = () => { }) { - if (!element) { - return; - } - - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - console.error("Birb: Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); -} - -/** - * @param {() => void} func - * @param {Element} [closeButton] - */ -function makeClosable(func, closeButton) { - if (closeButton) { - onClick(closeButton, func); - } - document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { - return; - } - if (e.key === "Escape") { - func(); - } - }); -} - /** * @param {MenuItem} item * @param {() => void} removeMenuCallback diff --git a/src/shared.js b/src/shared.js index 9aa5d2c..e2c64b1 100644 --- a/src/shared.js +++ b/src/shared.js @@ -17,4 +17,132 @@ export function isDebug() { */ export function setDebug(debugMode) { debug = debugMode; +} + +/** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ +export function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } + return element; +} + +/** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ +export function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); +} + +/** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + */ +export function makeDraggable(element, parent = true, callback = () => { }) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + console.error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); +} + +/** + * @param {() => void} func + * @param {Element} [closeButton] + */ +export function makeClosable(func, closeButton) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (e.key === "Escape") { + func(); + } + }); } \ No newline at end of file diff --git a/src/stickyNotes.js b/src/stickyNotes.js index 5027710..ef04e1f 100644 --- a/src/stickyNotes.js +++ b/src/stickyNotes.js @@ -1,3 +1,9 @@ +import { + makeElement, + makeDraggable, + makeClosable +} from './shared.js'; + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -64,134 +70,6 @@ export function isStickyNoteApplicable(stickyNote) { return true; } -/** - * Create an HTML element with the specified parameters - * @param {string} className - * @param {string} [textContent] - * @param {string} [id] - * @returns {HTMLElement} - */ -function makeElement(className, textContent, id) { - const element = document.createElement("div"); - element.classList.add(className); - if (textContent) { - element.textContent = textContent; - } - if (id) { - element.id = id; - } - return element; -} - -/** - * @param {Document|Element} element - * @param {(e: Event) => void} action - */ -function onClick(element, action) { - element.addEventListener("click", (e) => action(e)); - element.addEventListener("touchend", (e) => { - if (e instanceof TouchEvent === false) { - return; - } else if (element instanceof HTMLElement === false) { - return; - } - const touch = e.changedTouches[0]; - const rect = element.getBoundingClientRect(); - if ( - touch.clientX >= rect.left && - touch.clientX <= rect.right && - touch.clientY >= rect.top && - touch.clientY <= rect.bottom - ) { - action(e); - } - }); -} - -/** - * @param {HTMLElement|null} element The element to detect drag events on - * @param {boolean} [parent] Whether to move the parent element when the child is dragged - * @param {(top: number, left: number) => void} [callback] Callback for when element is moved - */ -function makeDraggable(element, parent = true, callback = () => { }) { - if (!element) { - return; - } - - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - let elementToMove = parent ? element.parentElement : element; - - if (!elementToMove) { - console.error("Birb: Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - elementToMove.offsetLeft; - offsetY = e.clientY - elementToMove.offsetTop; - }); - - element.addEventListener("touchstart", (e) => { - isMouseDown = true; - const touch = e.touches[0]; - offsetX = touch.clientX - elementToMove.offsetLeft; - offsetY = touch.clientY - elementToMove.offsetTop; - e.preventDefault(); - }); - - document.addEventListener("mouseup", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("touchend", (e) => { - if (isMouseDown) { - callback(elementToMove.offsetTop, elementToMove.offsetLeft); - e.preventDefault(); - } - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`; - } - }); - - document.addEventListener("touchmove", (e) => { - if (isMouseDown) { - const touch = e.touches[0]; - elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; - elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; - } - }); -} - -/** - * @param {() => void} func - * @param {Element} [closeButton] - */ -function makeClosable(func, closeButton) { - if (closeButton) { - onClick(closeButton, func); - } - document.addEventListener("keydown", (e) => { - if (closeButton && !document.body.contains(closeButton)) { - return; - } - if (e.key === "Escape") { - func(); - } - }); -} - /** * @param {StickyNote} stickyNote * @param {() => void} onSave From b7453d1a942df9e6a3d4b11b9eb45aa843bcd180 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 16:21:52 -0400 Subject: [PATCH 11/16] Remove decoration code --- build.js | 4 ---- dist/birb.js | 14 +------------- dist/birb.user.js | 16 ++-------------- manifest.json | 2 +- src/birb.js | 39 +-------------------------------------- 5 files changed, 5 insertions(+), 70 deletions(-) diff --git a/build.js b/build.js index b954f80..a35435a 100644 --- a/build.js +++ b/build.js @@ -11,10 +11,6 @@ const spriteSheets = [ { key: "__FEATHER_SPRITE_SHEET__", path: "./sprites/feather.png" - }, - { - key: "__DECORATIONS_SPRITE_SHEET__", - path: "./sprites/decorations.png" } ]; diff --git a/dist/birb.js b/dist/birb.js index 11ed0f5..419472f 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -1166,11 +1166,9 @@ const SPRITE_WIDTH = 32; const SPRITE_HEIGHT = 32; - const DECORATIONS_SPRITE_WIDTH = 48; const FEATHER_SPRITE_WIDTH = 32; 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 FIELD_GUIDE_ID = "birb-field-guide"; @@ -1259,12 +1257,10 @@ Promise.all([ loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, decorationPixels, featherPixels]) => { + ]).then(([birbPixels, featherPixels]) => { const SPRITE_SHEET = birbPixels; - const DECORATIONS_SPRITE_SHEET = decorationPixels; const FEATHER_SPRITE_SHEET = featherPixels; const layers = { @@ -1280,10 +1276,6 @@ happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), }; - const decorationLayers = { - mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)), - }; - const featherLayers = { feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), }; @@ -1299,10 +1291,6 @@ heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), }; - ({ - mac: new Frame([decorationLayers.mac]), - }); - const featherFrames = { feather: new Frame([featherLayers.feather]), }; diff --git a/dist/birb.user.js b/dist/birb.user.js index e86804e..4893b2c 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.184 +// @version 2025.10.26.201 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -1180,11 +1180,9 @@ const SPRITE_WIDTH = 32; const SPRITE_HEIGHT = 32; - const DECORATIONS_SPRITE_WIDTH = 48; const FEATHER_SPRITE_WIDTH = 32; 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 FIELD_GUIDE_ID = "birb-field-guide"; @@ -1273,12 +1271,10 @@ Promise.all([ loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, decorationPixels, featherPixels]) => { + ]).then(([birbPixels, featherPixels]) => { const SPRITE_SHEET = birbPixels; - const DECORATIONS_SPRITE_SHEET = decorationPixels; const FEATHER_SPRITE_SHEET = featherPixels; const layers = { @@ -1294,10 +1290,6 @@ happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), }; - const decorationLayers = { - mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)), - }; - const featherLayers = { feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), }; @@ -1313,10 +1305,6 @@ heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), }; - ({ - mac: new Frame([decorationLayers.mac]), - }); - const featherFrames = { feather: new Frame([featherLayers.feather]), }; diff --git a/manifest.json b/manifest.json index f09c262..4c52dd6 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.184", + "version": "2025.10.26.201", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/birb.js b/src/birb.js index 63dcb31..4376bfe 100644 --- a/src/birb.js +++ b/src/birb.js @@ -87,11 +87,9 @@ const DEFAULT_BIRD = "bluebird"; const SPRITE_WIDTH = 32; const SPRITE_HEIGHT = 32; -const DECORATIONS_SPRITE_WIDTH = 48; const FEATHER_SPRITE_WIDTH = 32; const SPRITE_SHEET = "__SPRITE_SHEET__"; -const DECORATIONS_SPRITE_SHEET = "__DECORATIONS_SPRITE_SHEET__"; const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__"; const FIELD_GUIDE_ID = "birb-field-guide"; @@ -180,12 +178,10 @@ log("Loading sprite sheets..."); Promise.all([ loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) -]).then(([birbPixels, decorationPixels, featherPixels]) => { +]).then(([birbPixels, featherPixels]) => { const SPRITE_SHEET = birbPixels; - const DECORATIONS_SPRITE_SHEET = decorationPixels; const FEATHER_SPRITE_SHEET = featherPixels; const layers = { @@ -201,10 +197,6 @@ Promise.all([ happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), }; - const decorationLayers = { - mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)), - }; - const featherLayers = { feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), }; @@ -220,10 +212,6 @@ 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]), }; @@ -269,14 +257,6 @@ Promise.all([ ], false), }; - const DECORATION_ANIMATIONS = { - mac: new Anim([ - decorationFrames.mac, - ], [ - 1000, - ]), - }; - const FEATHER_ANIMATIONS = { feather: new Anim([ featherFrames.feather, @@ -681,23 +661,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, CANVAS_PIXEL_SIZE, Date.now()); - // Add the decoration to the page - document.body.appendChild(decorationCanvas); - makeDraggable(decorationCanvas, false); - } - function activateFeather() { if (document.querySelector("#" + FEATHER_ID)) { return; From 129cd893401d0a1f31b5d01c44b3bb138c675feb Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 16:34:20 -0400 Subject: [PATCH 12/16] Remove configs --- dist/birb.js | 122 ++++++++++++++++++--------------------------- dist/birb.user.js | 124 +++++++++++++++++++--------------------------- manifest.json | 2 +- src/birb.js | 94 ++++++++++------------------------- src/shared.js | 31 ++++++++++-- 5 files changed, 151 insertions(+), 222 deletions(-) diff --git a/dist/birb.js b/dist/birb.js index 419472f..bfe76a4 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -6,20 +6,20 @@ RIGHT: 1, }; - let debug$1 = location.hostname === "127.0.0.1"; + let debugMode = location.hostname === "127.0.0.1"; /** * @returns {boolean} Whether debug mode is enabled */ function isDebug() { - return debug$1; + return debugMode; } /** - * @param {boolean} debugMode + * @param {boolean} value */ - function setDebug(debugMode) { - debug$1 = debugMode; + function setDebug(value) { + debugMode = value; } /** @@ -150,6 +150,27 @@ }); } + /** + * @returns {boolean} Whether the user is on a mobile device + */ + function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + } + + function log() { + console.log("Birb: ", ...arguments); + } + + function debug() { + if (isDebug()) { + console.debug("Birb: ", ...arguments); + } + } + + function error() { + console.error("Birb: ", ...arguments); + } + /** Indicators for parts of the base bird sprite sheet */ const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", @@ -767,41 +788,6 @@ updateLocationCallback(menu); } - // @ts-ignore - const SHARED_CONFIG = { - birbCssScale: 1, - uiCssScale: 1, - canvasPixelSize: 1, - hopSpeed: 0.07, - hopDistance: 45, - }; - - const DESKTOP_CONFIG = { - flySpeed: 0.25 - }; - - const MOBILE_CONFIG = { - uiCssScale: 0.9, - flySpeed: 0.125, - }; - - const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG }; - - 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 DEFAULT_SETTINGS = { - birbMode: false - }; - - /** - * @typedef {typeof DEFAULT_SETTINGS} Settings - */ - /** * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote */ @@ -814,8 +800,21 @@ * @property {SavedStickyNote[]} [stickyNotes] */ - /** @type {Partial} */ - let userSettings = {}; + /** + * @typedef {typeof DEFAULT_SETTINGS} Settings + */ + const DEFAULT_SETTINGS = { + birbMode: false + }; + + + const SPRITE_WIDTH = 32; + const SPRITE_HEIGHT = 32; + const FEATHER_SPRITE_WIDTH = 32; + const BIRB_CSS_SCALE = 1; + const UI_CSS_SCALE = isMobile() ? 0.9 : 1; + const CANVAS_PIXEL_SIZE = 1; + const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; const STYLESHEET = `:root { --birb-border-size: 2px; @@ -1161,22 +1160,16 @@ .birb-sticky-note-input:focus { outline: none; }`; - - const DEFAULT_BIRD = "bluebird"; - - const SPRITE_WIDTH = 32; - const SPRITE_HEIGHT = 32; - const FEATHER_SPRITE_WIDTH = 32; - 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 FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; 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; + const DEFAULT_BIRD = "bluebird"; + const HOP_SPEED = 0.07; + const FLY_SPEED = isMobile() ? 0.125 : 0.25; + const HOP_DISTANCE = 45; /** Speed at which the feather falls per tick */ const FEATHER_FALL_SPEED = 1; /** Time in milliseconds until the user is considered AFK */ @@ -1197,6 +1190,9 @@ /** Time after petting before the menu can be opened */ const PET_MENU_COOLDOWN = 1000; + /** @type {Partial} */ + let userSettings = {}; + /** * Load the sprite sheet and return the pixel-map template * @param {string} dataUri @@ -1387,6 +1383,7 @@ FLYING: "flying", }; + let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; let animStart = Date.now(); @@ -2200,25 +2197,4 @@ error("Error while loading sprite sheets: ", e); }); - /** - * @returns {boolean} Whether the user is on a mobile device - */ - function isMobile() { - return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); - } - - function log() { - console.log("Birb: ", ...arguments); - } - - function debug() { - if (isDebug()) { - console.debug("Birb: ", ...arguments); - } - } - - function error() { - console.error("Birb: ", ...arguments); - } - })(); diff --git a/dist/birb.user.js b/dist/birb.user.js index 4893b2c..b29e5ad 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.201 +// @version 2025.10.26.221 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -20,20 +20,20 @@ RIGHT: 1, }; - let debug$1 = location.hostname === "127.0.0.1"; + let debugMode = location.hostname === "127.0.0.1"; /** * @returns {boolean} Whether debug mode is enabled */ function isDebug() { - return debug$1; + return debugMode; } /** - * @param {boolean} debugMode + * @param {boolean} value */ - function setDebug(debugMode) { - debug$1 = debugMode; + function setDebug(value) { + debugMode = value; } /** @@ -164,6 +164,27 @@ }); } + /** + * @returns {boolean} Whether the user is on a mobile device + */ + function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + } + + function log() { + console.log("Birb: ", ...arguments); + } + + function debug() { + if (isDebug()) { + console.debug("Birb: ", ...arguments); + } + } + + function error() { + console.error("Birb: ", ...arguments); + } + /** Indicators for parts of the base bird sprite sheet */ const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", @@ -781,41 +802,6 @@ updateLocationCallback(menu); } - // @ts-ignore - const SHARED_CONFIG = { - birbCssScale: 1, - uiCssScale: 1, - canvasPixelSize: 1, - hopSpeed: 0.07, - hopDistance: 45, - }; - - const DESKTOP_CONFIG = { - flySpeed: 0.25 - }; - - const MOBILE_CONFIG = { - uiCssScale: 0.9, - flySpeed: 0.125, - }; - - const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG }; - - 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 DEFAULT_SETTINGS = { - birbMode: false - }; - - /** - * @typedef {typeof DEFAULT_SETTINGS} Settings - */ - /** * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote */ @@ -828,8 +814,21 @@ * @property {SavedStickyNote[]} [stickyNotes] */ - /** @type {Partial} */ - let userSettings = {}; + /** + * @typedef {typeof DEFAULT_SETTINGS} Settings + */ + const DEFAULT_SETTINGS = { + birbMode: false + }; + + + const SPRITE_WIDTH = 32; + const SPRITE_HEIGHT = 32; + const FEATHER_SPRITE_WIDTH = 32; + const BIRB_CSS_SCALE = 1; + const UI_CSS_SCALE = isMobile() ? 0.9 : 1; + const CANVAS_PIXEL_SIZE = 1; + const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; const STYLESHEET = `:root { --birb-border-size: 2px; @@ -1175,22 +1174,16 @@ .birb-sticky-note-input:focus { outline: none; }`; - - const DEFAULT_BIRD = "bluebird"; - - const SPRITE_WIDTH = 32; - const SPRITE_HEIGHT = 32; - const FEATHER_SPRITE_WIDTH = 32; - 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 FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; 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; + const DEFAULT_BIRD = "bluebird"; + const HOP_SPEED = 0.07; + const FLY_SPEED = isMobile() ? 0.125 : 0.25; + const HOP_DISTANCE = 45; /** Speed at which the feather falls per tick */ const FEATHER_FALL_SPEED = 1; /** Time in milliseconds until the user is considered AFK */ @@ -1211,6 +1204,9 @@ /** Time after petting before the menu can be opened */ const PET_MENU_COOLDOWN = 1000; + /** @type {Partial} */ + let userSettings = {}; + /** * Load the sprite sheet and return the pixel-map template * @param {string} dataUri @@ -1401,6 +1397,7 @@ FLYING: "flying", }; + let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; let animStart = Date.now(); @@ -2214,25 +2211,4 @@ error("Error while loading sprite sheets: ", e); }); - /** - * @returns {boolean} Whether the user is on a mobile device - */ - function isMobile() { - return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); - } - - function log() { - console.log("Birb: ", ...arguments); - } - - function debug() { - if (isDebug()) { - console.debug("Birb: ", ...arguments); - } - } - - function error() { - console.error("Birb: ", ...arguments); - } - })(); diff --git a/manifest.json b/manifest.json index 4c52dd6..03c371b 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.201", + "version": "2025.10.26.221", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/birb.js b/src/birb.js index 4376bfe..ce5c97d 100644 --- a/src/birb.js +++ b/src/birb.js @@ -8,7 +8,11 @@ import { makeElement, onClick, makeDraggable, - makeClosable + makeClosable, + isMobile, + log, + debug, + error } from './shared.js'; import { SPRITE, @@ -31,40 +35,6 @@ import { MENU_EXIT_ID } from './menu.js'; -// @ts-ignore -const SHARED_CONFIG = { - birbCssScale: 1, - uiCssScale: 1, - canvasPixelSize: 1, - hopSpeed: 0.07, - hopDistance: 45, -}; - -const DESKTOP_CONFIG = { - flySpeed: 0.25 -}; - -const MOBILE_CONFIG = { - uiCssScale: 0.9, - flySpeed: 0.125, -}; - -const CONFIG = { ...SHARED_CONFIG, ...isMobile() ? MOBILE_CONFIG : DESKTOP_CONFIG }; - -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 DEFAULT_SETTINGS = { - birbMode: false -}; - -/** - * @typedef {typeof DEFAULT_SETTINGS} Settings - */ /** * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote @@ -78,26 +48,33 @@ const DEFAULT_SETTINGS = { * @property {SavedStickyNote[]} [stickyNotes] */ -/** @type {Partial} */ -let userSettings = {}; +/** + * @typedef {typeof DEFAULT_SETTINGS} Settings + */ +const DEFAULT_SETTINGS = { + birbMode: false +}; -const STYLESHEET = `___STYLESHEET___`; - -const DEFAULT_BIRD = "bluebird"; const SPRITE_WIDTH = 32; const SPRITE_HEIGHT = 32; const FEATHER_SPRITE_WIDTH = 32; +const BIRB_CSS_SCALE = 1; +const UI_CSS_SCALE = isMobile() ? 0.9 : 1; +const CANVAS_PIXEL_SIZE = 1; +const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; +const STYLESHEET = `___STYLESHEET___`; const SPRITE_SHEET = "__SPRITE_SHEET__"; const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__"; 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; +const DEFAULT_BIRD = "bluebird"; +const HOP_SPEED = 0.07; +const FLY_SPEED = isMobile() ? 0.125 : 0.25; +const HOP_DISTANCE = 45; /** Speed at which the feather falls per tick */ const FEATHER_FALL_SPEED = 1; /** Time in milliseconds until the user is considered AFK */ @@ -118,6 +95,9 @@ const URL_CHECK_INTERVAL = 500; /** Time after petting before the menu can be opened */ const PET_MENU_COOLDOWN = 1000; +/** @type {Partial} */ +let userSettings = {}; + /** * Load the sprite sheet and return the pixel-map template * @param {string} dataUri @@ -308,6 +288,7 @@ Promise.all([ FLYING: "flying", }; + let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; let animStart = Date.now(); @@ -1000,10 +981,6 @@ Promise.all([ return canvas.width * BIRB_CSS_SCALE } - function getCanvasHeight() { - return canvas.height * BIRB_CSS_SCALE - } - function hop() { if (frozen) { return; @@ -1131,25 +1108,4 @@ Promise.all([ draw(); }).catch((e) => { error("Error while loading sprite sheets: ", e); -}); - -/** - * @returns {boolean} Whether the user is on a mobile device - */ -function isMobile() { - return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); -} - -function log() { - console.log("Birb: ", ...arguments); -} - -function debug() { - if (isDebug()) { - console.debug("Birb: ", ...arguments); - } -} - -function error() { - console.error("Birb: ", ...arguments); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/shared.js b/src/shared.js index e2c64b1..5a6f193 100644 --- a/src/shared.js +++ b/src/shared.js @@ -3,20 +3,20 @@ export const Directions = { RIGHT: 1, }; -let debug = location.hostname === "127.0.0.1"; +let debugMode = location.hostname === "127.0.0.1"; /** * @returns {boolean} Whether debug mode is enabled */ export function isDebug() { - return debug; + return debugMode; } /** - * @param {boolean} debugMode + * @param {boolean} value */ -export function setDebug(debugMode) { - debug = debugMode; +export function setDebug(value) { + debugMode = value; } /** @@ -145,4 +145,25 @@ export function makeClosable(func, closeButton) { func(); } }); +} + +/** + * @returns {boolean} Whether the user is on a mobile device + */ +export function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +} + +export function log() { + console.log("Birb: ", ...arguments); +} + +export function debug() { + if (isDebug()) { + console.debug("Birb: ", ...arguments); + } +} + +export function error() { + console.error("Birb: ", ...arguments); } \ No newline at end of file From 0873b4db1f1f01c0a3f440fb8fe6f212ef0db1e9 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 17:00:35 -0400 Subject: [PATCH 13/16] Reorder constants --- dist/birb.js | 37 +++++++++++++++++++++---------------- dist/birb.user.js | 39 ++++++++++++++++++++++----------------- manifest.json | 2 +- src/birb.js | 41 +++++++++++++++++++++-------------------- 4 files changed, 65 insertions(+), 54 deletions(-) diff --git a/dist/birb.js b/dist/birb.js index bfe76a4..31cef6e 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -807,7 +807,7 @@ birbMode: false }; - + // Rendering constants const SPRITE_WIDTH = 32; const SPRITE_HEIGHT = 32; const FEATHER_SPRITE_WIDTH = 32; @@ -816,6 +816,7 @@ const CANVAS_PIXEL_SIZE = 1; const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; + // Build-time assets const STYLESHEET = `:root { --birb-border-size: 2px; --birb-neg-border-size: calc(var(--birb-border-size) * -1); @@ -1163,32 +1164,36 @@ 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 FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; + // Element IDs const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; const DEFAULT_BIRD = "bluebird"; + + // Birb movement const HOP_SPEED = 0.07; const FLY_SPEED = isMobile() ? 0.125 : 0.25; const HOP_DISTANCE = 45; - /** 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 = isDebug() ? 0 : 1000 * 30; + + // Timing constants (in milliseconds) 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 AFK_TIME = isDebug() ? 0 : 1000 * 30; const PET_BOOST_DURATION = 1000 * 60 * 5; + const PET_MENU_COOLDOWN = 1000; + const URL_CHECK_INTERVAL = 500; + + // Random event chances per tick + const HOP_CHANCE = 1 / (60 * 3); // Every 3 seconds + const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds + const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours + + // Feathers + const FEATHER_FALL_SPEED = 1; + const PET_FEATHER_BOOST = 2; + + // Focus element constraints 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; /** @type {Partial} */ let userSettings = {}; diff --git a/dist/birb.user.js b/dist/birb.user.js index b29e5ad..206e0d9 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.221 +// @version 2025.10.26.227 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -821,7 +821,7 @@ birbMode: false }; - + // Rendering constants const SPRITE_WIDTH = 32; const SPRITE_HEIGHT = 32; const FEATHER_SPRITE_WIDTH = 32; @@ -830,6 +830,7 @@ const CANVAS_PIXEL_SIZE = 1; const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; + // Build-time assets const STYLESHEET = `:root { --birb-border-size: 2px; --birb-neg-border-size: calc(var(--birb-border-size) * -1); @@ -1177,32 +1178,36 @@ 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 FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; + // Element IDs const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; const DEFAULT_BIRD = "bluebird"; + + // Birb movement const HOP_SPEED = 0.07; const FLY_SPEED = isMobile() ? 0.125 : 0.25; const HOP_DISTANCE = 45; - /** 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 = isDebug() ? 0 : 1000 * 30; + + // Timing constants (in milliseconds) 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 AFK_TIME = isDebug() ? 0 : 1000 * 30; const PET_BOOST_DURATION = 1000 * 60 * 5; + const PET_MENU_COOLDOWN = 1000; + const URL_CHECK_INTERVAL = 500; + + // Random event chances per tick + const HOP_CHANCE = 1 / (60 * 3); // Every 3 seconds + const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds + const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours + + // Feathers + const FEATHER_FALL_SPEED = 1; + const PET_FEATHER_BOOST = 2; + + // Focus element constraints 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; /** @type {Partial} */ let userSettings = {}; diff --git a/manifest.json b/manifest.json index 03c371b..a370218 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.221", + "version": "2025.10.26.227", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/birb.js b/src/birb.js index ce5c97d..283bfb3 100644 --- a/src/birb.js +++ b/src/birb.js @@ -55,7 +55,7 @@ const DEFAULT_SETTINGS = { birbMode: false }; - +// Rendering constants const SPRITE_WIDTH = 32; const SPRITE_HEIGHT = 32; const FEATHER_SPRITE_WIDTH = 32; @@ -64,36 +64,41 @@ const UI_CSS_SCALE = isMobile() ? 0.9 : 1; const CANVAS_PIXEL_SIZE = 1; const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; +// Build-time assets const STYLESHEET = `___STYLESHEET___`; const SPRITE_SHEET = "__SPRITE_SHEET__"; const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__"; +// Element IDs const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; const DEFAULT_BIRD = "bluebird"; + +// Birb movement const HOP_SPEED = 0.07; const FLY_SPEED = isMobile() ? 0.125 : 0.25; const HOP_DISTANCE = 45; -/** 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 = isDebug() ? 0 : 1000 * 30; + +// Timing constants (in milliseconds) 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 AFK_TIME = isDebug() ? 0 : 1000 * 30; const PET_BOOST_DURATION = 1000 * 60 * 5; +const PET_MENU_COOLDOWN = 1000; +const URL_CHECK_INTERVAL = 500; + +// Random event chances per tick +const HOP_CHANCE = 1 / (60 * 3); // Every 3 seconds +const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds +const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours + +// Feathers +const FEATHER_FALL_SPEED = 1; +const PET_FEATHER_BOOST = 2; + +// Focus element constraints 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; /** @type {Partial} */ let userSettings = {}; @@ -853,10 +858,6 @@ Promise.all([ } } - function isSafari() { - return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - } - /** * @param {string} type */ From e1c88120d32fec39d0eb0564f4881a77b0ea990d Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 17:08:09 -0400 Subject: [PATCH 14/16] Rename birb to application --- build.js | 2 +- dist/birb.user.js | 2 +- manifest.json | 2 +- src/application.js | 1112 +++++++++++++++++++++++++++++++++++++++++++ src/birb.js | 1113 +------------------------------------------- 5 files changed, 1117 insertions(+), 1114 deletions(-) create mode 100644 src/application.js diff --git a/build.js b/build.js index a35435a..f04ad33 100644 --- a/build.js +++ b/build.js @@ -68,7 +68,7 @@ const userScriptHeader = // Bundle with rollup const bundle = await rollup({ - input: 'src/birb.js', + input: 'src/application.js', }); await bundle.write({ diff --git a/dist/birb.user.js b/dist/birb.user.js index 206e0d9..76d2bf5 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.227 +// @version 2025.10.26.240 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js diff --git a/manifest.json b/manifest.json index a370218..aa13af2 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.227", + "version": "2025.10.26.240", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/application.js b/src/application.js new file mode 100644 index 0000000..283bfb3 --- /dev/null +++ b/src/application.js @@ -0,0 +1,1112 @@ +import Frame from './frame.js'; +import Layer from './layer.js'; +import Anim from './anim.js'; +import { + Directions, + isDebug, + setDebug, + makeElement, + onClick, + makeDraggable, + makeClosable, + isMobile, + log, + debug, + error +} from './shared.js'; +import { + SPRITE, + SPRITE_SHEET_COLOR_MAP, + SPECIES +} from './sprites.js'; +import { + StickyNote, + createNewStickyNote, + drawStickyNotes +} from './stickyNotes.js'; +import { + MenuItem, + DebugMenuItem, + Separator, + insertMenu, + removeMenu, + isMenuOpen, + switchMenuItems, + MENU_EXIT_ID +} from './menu.js'; + + +/** + * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote + */ + +/** + * @typedef {Object} BirbSaveData + * @property {string[]} unlockedSpecies + * @property {string} currentSpecies + * @property {Partial} settings + * @property {SavedStickyNote[]} [stickyNotes] + */ + +/** + * @typedef {typeof DEFAULT_SETTINGS} Settings + */ +const DEFAULT_SETTINGS = { + birbMode: false +}; + +// Rendering constants +const SPRITE_WIDTH = 32; +const SPRITE_HEIGHT = 32; +const FEATHER_SPRITE_WIDTH = 32; +const BIRB_CSS_SCALE = 1; +const UI_CSS_SCALE = isMobile() ? 0.9 : 1; +const CANVAS_PIXEL_SIZE = 1; +const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; + +// Build-time assets +const STYLESHEET = `___STYLESHEET___`; +const SPRITE_SHEET = "__SPRITE_SHEET__"; +const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__"; + +// Element IDs +const FIELD_GUIDE_ID = "birb-field-guide"; +const FEATHER_ID = "birb-feather"; + +const DEFAULT_BIRD = "bluebird"; + +// Birb movement +const HOP_SPEED = 0.07; +const FLY_SPEED = isMobile() ? 0.125 : 0.25; +const HOP_DISTANCE = 45; + +// Timing constants (in milliseconds) +const UPDATE_INTERVAL = 1000 / 60; // 60 FPS +const AFK_TIME = isDebug() ? 0 : 1000 * 30; +const PET_BOOST_DURATION = 1000 * 60 * 5; +const PET_MENU_COOLDOWN = 1000; +const URL_CHECK_INTERVAL = 500; + +// Random event chances per tick +const HOP_CHANCE = 1 / (60 * 3); // Every 3 seconds +const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds +const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours + +// Feathers +const FEATHER_FALL_SPEED = 1; +const PET_FEATHER_BOOST = 2; + +// Focus element constraints +const MIN_FOCUS_ELEMENT_WIDTH = 100; +const MIN_FOCUS_ELEMENT_TOP = 80; + +/** @type {Partial} */ +let userSettings = {}; + +/** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ +function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(SPRITE.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(SPRITE.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); +} + +log("Loading sprite sheets..."); + +Promise.all([ + loadSpriteSheetPixels(SPRITE_SHEET), + loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) +]).then(([birbPixels, featherPixels]) => { + + const SPRITE_SHEET = birbPixels; + const FEATHER_SPRITE_SHEET = featherPixels; + + const layers = { + base: new Layer(getLayer(SPRITE_SHEET, 0)), + down: new Layer(getLayer(SPRITE_SHEET, 1)), + heartOne: new Layer(getLayer(SPRITE_SHEET, 2)), + heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), + heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), + tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"), + tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"), + wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)), + wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)), + happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), + }; + + const featherLayers = { + feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), + }; + + const birbFrames = { + base: new Frame([layers.base, layers.tuftBase]), + headDown: new Frame([layers.down, layers.tuftDown]), + wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]), + wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]), + heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]), + heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), + heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]), + heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), + }; + + const featherFrames = { + feather: new Frame([featherLayers.feather]), + }; + + const Animations = { + STILL: new Anim([birbFrames.base], [1000]), + BOB: new Anim([ + birbFrames.base, + birbFrames.headDown + ], [ + 420, + 420 + ]), + FLYING: new Anim([ + birbFrames.base, + birbFrames.wingsUp, + birbFrames.headDown, + birbFrames.wingsDown, + ], [ + 30, + 80, + 30, + 60, + ]), + HEART: new Anim([ + birbFrames.heartOne, + birbFrames.heartTwo, + birbFrames.heartThree, + birbFrames.heartFour, + birbFrames.heartThree, + birbFrames.heartFour, + birbFrames.heartThree, + birbFrames.heartFour, + ], [ + 60, + 80, + 250, + 250, + 250, + 250, + 250, + 250, + ], false), + }; + + const FEATHER_ANIMATIONS = { + feather: new Anim([ + featherFrames.feather, + ], [ + 1000, + ]), + }; + + const menuItems = [ + new MenuItem(`Pet ${birdBirb()}`, pet), + new MenuItem("Field Guide", insertFieldGuide), + new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote)), + new MenuItem(`Hide ${birdBirb()}`, hideBirb), + new DebugMenuItem("Freeze/Unfreeze", () => { + frozen = !frozen; + }), + new DebugMenuItem("Reset Data", resetSaveData), + new DebugMenuItem("Unlock All", () => { + for (let type in SPECIES) { + unlockBird(type); + } + }), + new DebugMenuItem("Disable Debug", () => { + setDebug(false); + }), + new Separator(), + new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), false), + ]; + + const settingsItems = [ + new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), + new Separator(), + new MenuItem("Toggle Birb Mode", () => { + userSettings.birbMode = !userSettings.birbMode; + save(); + insertModal(`${birdBirb()} Mode`, `Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"${userSettings.birbMode ? "\n\nWelcome back to 2012" : ""}`); + }) + ]; + + const styleElement = document.createElement("style"); + const canvas = document.createElement("canvas"); + + /** @type {CanvasRenderingContext2D} */ + const ctx = canvas.getContext("2d"); + + const States = { + IDLE: "idle", + HOP: "hop", + FLYING: "flying", + }; + + let frozen = false; + let stateStart = Date.now(); + let currentState = States.IDLE; + let animStart = Date.now(); + let currentAnimation = Animations.BOB; + let direction = Directions.RIGHT; + let ticks = 0; + // Bird's current position + let birdY = 0; + let birdX = 40; + // Bird's starting position (when flying) + let startX = 0; + let startY = 0; + // Bird's target position (when flying) + let targetX = 0; + let targetY = 0; + /** @type {HTMLElement|null} */ + let focusedElement = null; + let focusedBounds = { left: 0, right: 0, top: 0 }; + let lastActionTimestamp = Date.now(); + /** @type {number[]} */ + let petStack = []; + let currentSpecies = DEFAULT_BIRD; + let unlockedSpecies = [DEFAULT_BIRD]; + let visible = true; + let lastPetTimestamp = 0; + /** @type {StickyNote[]} */ + let stickyNotes = []; + + /** + * @returns {boolean} Whether the script is running in a userscript extension context + */ + function isUserScript() { + // @ts-expect-error + return typeof GM_getValue === "function"; + } + + function isTestEnvironment() { + return window.location.hostname === "127.0.0.1" + || window.location.hostname === "localhost" + || window.location.hostname.startsWith("192.168."); + } + + function load() { + /** @type {Record} */ + let saveData = {}; + + if (isUserScript()) { + log("Loading save data from UserScript storage"); + // @ts-expect-error + saveData = GM_getValue("birbSaveData", {}) ?? {}; + } else if (isTestEnvironment()) { + log("Test environment detected, loading save data from localStorage"); + saveData = JSON.parse(localStorage.getItem("birbSaveData") ?? "{}"); + } else { + log("Not a UserScript"); + } + + debug("Loaded data: " + JSON.stringify(saveData)); + + if (!saveData.settings) { + log("No user settings found in save data, starting fresh"); + } + + userSettings = saveData.settings ?? {}; + unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; + currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; + stickyNotes = []; + + if (saveData.stickyNotes) { + for (let note of saveData.stickyNotes) { + if (note.id) { + stickyNotes.push(new StickyNote(note.id, note.site, note.content, note.top, note.left)); + } + } + } + + log(stickyNotes.length + " sticky notes loaded"); + switchSpecies(currentSpecies); + } + + function save() { + /** @type {BirbSaveData} */ + const saveData = { + unlockedSpecies, + currentSpecies, + settings: userSettings + }; + + if (stickyNotes.length > 0) { + saveData.stickyNotes = stickyNotes.map(note => ({ + id: note.id, + site: note.site, + content: note.content, + top: note.top, + left: note.left + })); + } + + if (isUserScript()) { + log("Saving data to UserScript storage"); + // @ts-expect-error + GM_setValue("birbSaveData", saveData); + } else if (isTestEnvironment()) { + log("Test environment detected, saving data to localStorage"); + localStorage.setItem("birbSaveData", JSON.stringify(saveData)); + } else { + log("Not a UserScript"); + } + } + + function resetSaveData() { + if (isUserScript()) { + log("Resetting save data in UserScript storage"); + // @ts-expect-error + GM_deleteValue("birbSaveData"); + } else if (isTestEnvironment()) { + log("Test environment detected, resetting save data in localStorage"); + localStorage.removeItem("birbSaveData"); + } else { + log("Not a UserScript"); + } + load(); + } + + /** + * Get the user settings merged with default settings + * @returns {Settings} The merged settings + */ + function settings() { + return { ...DEFAULT_SETTINGS, ...userSettings }; + } + + /** + * Bird or birb, you decide + */ + function birdBirb() { + return settings().birbMode ? "Birb" : "Bird"; + } + + function init() { + if (window !== window.top) { + // Skip installation if within an iframe + log("In iframe, skipping Birb script initialization"); + return; + } + log("Sprite sheets loaded successfully, initializing bird..."); + + // Preload font + const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf"; + const fontLink = document.createElement("link"); + fontLink.rel = "stylesheet"; + fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`; + document.head.appendChild(fontLink); + + // Add stylesheet font-face + const fontFace = ` + @font-face { + font-family: 'Monocraft'; + src: url(${MONOCRAFT_SRC}) format('opentype'); + font-weight: normal; + font-style: normal; + } + `; + const fontStyle = document.createElement("style"); + fontStyle.innerHTML = fontFace; + document.head.appendChild(fontStyle); + + load(); + + styleElement.innerHTML = STYLESHEET; + document.head.appendChild(styleElement); + + canvas.id = "birb"; + canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE; + canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; + document.body.appendChild(canvas); + + window.addEventListener("scroll", () => { + lastActionTimestamp = Date.now(); + }); + + onClick(document, (e) => { + lastActionTimestamp = Date.now(); + if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { + removeMenu(); + } + }); + + onClick(canvas, () => { + if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { + // Currently being pet, don't open menu + return; + } + insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation); + }); canvas.addEventListener("mouseover", () => { + lastActionTimestamp = Date.now(); + if (currentState === States.IDLE) { + petStack.push(Date.now()); + if (petStack.length > 10) { + petStack.shift(); + } + const pets = petStack.filter((time) => Date.now() - time < 1000).length; + if (pets >= 3) { + pet(); + // Clear the stack + petStack = []; + } + } + }); + + canvas.addEventListener("touchmove", (e) => { + pet(); + }); + + drawStickyNotes(stickyNotes, save, deleteStickyNote); + + let lastUrl = (window.location.href ?? "").split("?")[0]; + setInterval(() => { + const currentUrl = (window.location.href ?? "").split("?")[0]; + if (currentUrl !== lastUrl) { + log("URL changed, updating sticky notes"); + lastUrl = currentUrl; + drawStickyNotes(stickyNotes, save, deleteStickyNote); + } + }, URL_CHECK_INTERVAL); + + setInterval(update, UPDATE_INTERVAL); + } + + function update() { + ticks++; + + // Hide bird if the browser is fullscreen + if (document.fullscreenElement) { + hideBirb(); + // Won't be restored on fullscreen exit + } + + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { + hop(); + } else if (Date.now() - lastActionTimestamp > AFK_TIME) { + // Idle for a while, do something + if (focusedElement === null) { + // Fly to an element + focusOnElement(); + lastActionTimestamp = Date.now(); + } else if (Math.random() < FOCUS_SWITCH_CHANCE) { + // Fly to another element if idle for a longer while + focusOnElement(); + lastActionTimestamp = Date.now(); + } + } + } else if (currentState === States.HOP) { + if (updateParabolicPath(HOP_SPEED)) { + setState(States.IDLE); + } + } + + // Double the chance of a feather if recently pet + const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1; + if (visible && Math.random() < FEATHER_CHANCE * petMod) { + lastPetTimestamp = 0; + activateFeather(); + } + updateFeather(); + } + + function draw() { + requestAnimationFrame(draw); + + if (!visible) { + return; + } + + updateFocusedElementBounds(); + + // Update the bird's position + if (currentState === States.IDLE) { + if (focusedElement && !isWithinHorizontalBounds()) { + focusOnGround(); + } + birdY = getFocusedY(); + } else if (currentState === States.FLYING) { + // Fly to target location (even if in the air) + if (updateParabolicPath(FLY_SPEED)) { + setState(States.IDLE); + } + } + + const oldTargetY = targetY; + targetY = getFocusedY(); + // Adjust startY to account for scrolling + startY += targetY - oldTargetY; + if (targetY < 0 || targetY > window.innerHeight) { + // Fly to ground if the focused element moves out of bounds + focusOnGround(); + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) { + setAnimation(Animations.STILL); + } + + // Update HTML element position + setX(birdX); + setY(birdY); + } + + /** + * @param {StickyNote} stickyNote + */ + function deleteStickyNote(stickyNote) { + stickyNotes = stickyNotes.filter(note => note.id !== stickyNote.id); + save(); + } + + /** + * Create a window element with header and content + * @param {string} id + * @param {string} title + * @param {string} contentHtml + * @param {() => void} [onClose] + * @returns {HTMLElement} + */ + function createWindow(id, title, contentHtml, onClose) { + const window = makeElement("birb-window", undefined, id); + window.innerHTML = ` +
+
${title}
+
x
+
+
+ ${contentHtml} +
+ `; + + document.body.appendChild(window); + makeDraggable(window.querySelector(".birb-window-header")); + + const closeButton = window.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + if (onClose) { + onClose(); + } + window.remove(); + }, closeButton); + } + + return window; + } + + function activateFeather() { + if (document.querySelector("#" + FEATHER_ID)) { + return; + } + const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); + if (speciesToUnlock.length === 0) { + // No more species to unlock + return; + } + const birdType = speciesToUnlock[Math.floor(Math.random() * speciesToUnlock.length)]; + insertFeather(birdType); + } + + /** + * @param {string} birdType + */ + function insertFeather(birdType) { + let type = SPECIES[birdType]; + const featherCanvas = document.createElement("canvas"); + featherCanvas.id = FEATHER_ID; + featherCanvas.classList.add("birb-decoration"); + featherCanvas.width = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + featherCanvas.height = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + const x = featherCanvas.width * 2 + Math.random() * (window.innerWidth - featherCanvas.width * 4); + featherCanvas.style.marginLeft = `${x}px`; + featherCanvas.style.top = `${-featherCanvas.height}px`; + const featherCtx = featherCanvas.getContext("2d"); + if (!featherCtx) { + return; + } + FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, type); + document.body.appendChild(featherCanvas); + onClick(featherCanvas, () => { + unlockBird(birdType); + removeFeather(); + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + removeFieldGuide(); + insertFieldGuide(); + } + }); + } + + function removeFeather() { + const feather = document.querySelector("#" + FEATHER_ID); + if (feather) { + feather.remove(); + } + } + + /** + * @param {string} birdType + */ + function unlockBird(birdType) { + if (!unlockedSpecies.includes(birdType)) { + unlockedSpecies.push(birdType); + insertModal("New Bird Unlocked!", `You've found a ${SPECIES[birdType].name} feather! Use the Field Guide to switch your bird's species.`); + } + save(); + } + + function updateFeather() { + const feather = document.querySelector("#birb-feather"); + if (!feather || !(feather instanceof HTMLElement)) { + return; + } + const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED; + feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`; + if (y < window.innerHeight - feather.offsetHeight) { + feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; + } + } + + /** + * @param {HTMLElement} element + */ + function centerElement(element) { + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${window.innerHeight / 2 - element.offsetHeight / 2}px`; + } + + /** + * @param {string} title + * @param {string} message + */ + function insertModal(title, message) { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + + const modal = createWindow("birb-modal", title, ` +
+ ${message} +
+ `); + + modal.style.width = "270px"; + centerElement(modal); + } + + /** + * @param {HTMLElement} menu + */ + function updateMenuLocation(menu) { + let x = birdX; + let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; + const offset = 20; + if (x < window.innerWidth / 2) { + // Left side + x += offset; + } else { + // Right side + x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; + } + if (y > window.innerHeight / 2) { + // Top side + y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; + } else { + // Bottom side + y += offset; + } + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + }; + + function insertFieldGuide() { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + let html = ` +
+
Field Guide
+
x
+
+
+
+
+
` + const fieldGuide = makeElement("birb-window", undefined, FIELD_GUIDE_ID); + fieldGuide.innerHTML = html; + document.body.appendChild(fieldGuide); + makeDraggable(fieldGuide.querySelector(".birb-window-header")); + + const closeButton = fieldGuide.querySelector(".birb-window-close"); + if (closeButton) { + makeClosable(() => { + fieldGuide.remove(); + }, closeButton); + } + + const content = fieldGuide.querySelector(".birb-grid-content"); + if (!content) { + return; + } + content.innerHTML = ""; + + const generateDescription = (/** @type {string} */ speciesId) => { + const type = SPECIES[speciesId]; + const unlocked = unlockedSpecies.includes(speciesId); + return "" + type.name + "
" + (!unlocked ? "Not yet unlocked" : type.description); + }; + + const description = fieldGuide.querySelector(".birb-field-guide-description"); + if (!description) { + return; + } + description.innerHTML = generateDescription(currentSpecies); + for (const [id, type] of Object.entries(SPECIES)) { + const unlocked = unlockedSpecies.includes(id); + const speciesElement = makeElement("birb-grid-item"); + if (id === currentSpecies) { + speciesElement.classList.add("birb-grid-item-selected"); + } + const speciesCanvas = document.createElement("canvas"); + speciesCanvas.width = SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + speciesCanvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; + const speciesCtx = speciesCanvas.getContext("2d"); + if (!speciesCtx) { + return; + } + birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); + speciesElement.appendChild(speciesCanvas); + content.appendChild(speciesElement); + if (unlocked) { + onClick(speciesElement, () => { + switchSpecies(id); + document.querySelectorAll(".birb-grid-item").forEach((element) => { + element.classList.remove("birb-grid-item-selected"); + }); + speciesElement.classList.add("birb-grid-item-selected"); + }); + } else { + speciesElement.classList.add("birb-grid-item-locked"); + } + speciesElement.addEventListener("mouseover", () => { + log("mouseover"); + description.innerHTML = generateDescription(id); + }); + speciesElement.addEventListener("mouseout", () => { + description.innerHTML = generateDescription(currentSpecies); + }); + } + centerElement(fieldGuide); + } + + function removeFieldGuide() { + const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); + if (fieldGuide) { + fieldGuide.remove(); + } + } + + /** + * @param {string} type + */ + function switchSpecies(type) { + currentSpecies = type; + // Update CSS variable --birb-highlight to be wing color + document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[SPRITE.THEME_HIGHLIGHT]); + save(); + } + + /** + * @param {string[][]} array + * @param {number} sprite + * @param {number} [width] + * @returns {string[][]} + */ + function getLayer(array, sprite, width = SPRITE_WIDTH) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < width; y++) { + layer.push(array[y].slice(sprite * width, (sprite + 1) * width)); + } + return layer; + } + + /** + * Update the birds location from the start to the target location on a parabolic path + * @param {number} speed The speed of the bird along the path + * @param {number} [intensity] The intensity of the parabolic path + * @returns {boolean} Whether the bird has reached the target location + */ + function updateParabolicPath(speed, intensity = 2.5) { + const dx = targetX - startX; + const dy = targetY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const time = Date.now() - stateStart; + if (distance > Math.max(window.innerWidth, window.innerHeight) / 2) { + speed *= 1.3; + } + const amount = Math.min(1, time / (distance / speed)); + const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity); + birdX = x; + birdY = y; + const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1; + if (complete) { + birdX = targetX; + birdY = targetY; + } else { + direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT; + } + return complete; + } + + function getFocusedElementRandomX() { + return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; + } + + function isWithinHorizontalBounds() { + return birdX >= focusedBounds.left && birdX <= focusedBounds.right; + } + + function getFocusedY() { + return getFullWindowHeight() - focusedBounds.top; + } + + /** + * @returns The render-safe height of the inner browser window + */ + function getSafeWindowHeight() { + // Necessary because iOS 26 Safari is terrible and won't render + // fixed elements behind the address bar + return window.innerHeight; + } + + /** + * @returns The true height of the inner browser window + */ + function getFullWindowHeight() { + return document.documentElement.clientHeight; + } + + function focusOnGround() { + console.log("Focusing on ground"); + focusedElement = null; + focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; + flyTo(Math.random() * window.innerWidth, 0); + } + + function focusOnElement() { + if (frozen) { + return; + } + const elements = document.querySelectorAll("img, video"); + const inWindow = Array.from(elements).filter((img) => { + const rect = img.getBoundingClientRect(); + return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight; + }); + /** @type {HTMLElement[]} */ + // @ts-expect-error + const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); + if (largeElements.length === 0) { + return; + } + const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)]; + focusedElement = randomElement; + log("Focusing on element: ", focusedElement); + updateFocusedElementBounds(); + flyTo(getFocusedElementRandomX(), getFocusedY()); + } + + function updateFocusedElementBounds() { + if (focusedElement === null) { + // Update ground location to bottom of window + focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; + return; + } + const { left, right, top } = focusedElement.getBoundingClientRect(); + focusedBounds = { left, right, top }; + } + + function getCanvasWidth() { + return canvas.width * BIRB_CSS_SCALE + } + + function hop() { + if (frozen) { + return; + } + if (currentState === States.IDLE) { + setState(States.HOP); + setAnimation(Animations.FLYING); + if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { + targetX = birdX - HOP_DISTANCE; + } else { + targetX = birdX + HOP_DISTANCE; + } + targetY = getFocusedY(); + } + } + + function pet() { + if (currentState === States.IDLE && currentAnimation !== Animations.HEART) { + setAnimation(Animations.HEART); + lastPetTimestamp = Date.now(); + } + } + + function hideBirb() { + canvas.style.display = "none"; + visible = false; + } + + /** + * @param {number} x + * @param {number} y + */ + function flyTo(x, y) { + targetX = x; + targetY = y; + setState(States.FLYING); + setAnimation(Animations.FLYING); + } + + /** + * @returns {boolean} Whether the bird should be absolutely positioned + */ + function isAbsolute() { + return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); + } + + /** + * Set the current animation and reset the animation timer + * @param {Anim} animation + */ + function setAnimation(animation) { + currentAnimation = animation; + animStart = Date.now(); + } + + /** + * Set the current state and reset the state timer + * @param {string} state + */ + function setState(state) { + stateStart = Date.now(); + startX = birdX; + startY = birdY; + currentState = state; + if (state === States.IDLE) { + setAnimation(Animations.BOB); + } + if (isAbsolute()) { + canvas.classList.add("birb-absolute"); + } else { + canvas.classList.remove("birb-absolute"); + } + setY(birdY); + } + + /** + * @param {number} x + */ + function setX(x) { + let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2)); + canvas.style.left = `${x + mod}px`; + } + + /** + * @param {number} y + */ + function setY(y) { + let bottom; + if (isAbsolute()) { + // Position is absolute, convert from fixed + bottom = y - window.scrollY; + } else { + // Position is fixed + bottom = y; + } + canvas.style.bottom = `${bottom}px`; + } + + // Helper functions + + /** + * @param {number} startX + * @param {number} startY + * @param {number} endX + * @param {number} endY + * @param {number} amount + * @param {number} [intensity] + * @returns {{x: number, y: number}} + */ + function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) { + const dx = endX - startX; + const dy = endY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx); + const midX = startX + Math.cos(angle) * distance / 2; + const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity; + const t = amount; + const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX; + const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY; + return { x, y }; + } + + // Run the birb + init(); + draw(); +}).catch((e) => { + error("Error while loading sprite sheets: ", e); +}); \ No newline at end of file diff --git a/src/birb.js b/src/birb.js index 283bfb3..b98d3f5 100644 --- a/src/birb.js +++ b/src/birb.js @@ -1,1112 +1,3 @@ -import Frame from './frame.js'; -import Layer from './layer.js'; -import Anim from './anim.js'; -import { - Directions, - isDebug, - setDebug, - makeElement, - onClick, - makeDraggable, - makeClosable, - isMobile, - log, - debug, - error -} from './shared.js'; -import { - SPRITE, - SPRITE_SHEET_COLOR_MAP, - SPECIES -} from './sprites.js'; -import { - StickyNote, - createNewStickyNote, - drawStickyNotes -} from './stickyNotes.js'; -import { - MenuItem, - DebugMenuItem, - Separator, - insertMenu, - removeMenu, - isMenuOpen, - switchMenuItems, - MENU_EXIT_ID -} from './menu.js'; +export class Birb { - -/** - * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote - */ - -/** - * @typedef {Object} BirbSaveData - * @property {string[]} unlockedSpecies - * @property {string} currentSpecies - * @property {Partial} settings - * @property {SavedStickyNote[]} [stickyNotes] - */ - -/** - * @typedef {typeof DEFAULT_SETTINGS} Settings - */ -const DEFAULT_SETTINGS = { - birbMode: false -}; - -// Rendering constants -const SPRITE_WIDTH = 32; -const SPRITE_HEIGHT = 32; -const FEATHER_SPRITE_WIDTH = 32; -const BIRB_CSS_SCALE = 1; -const UI_CSS_SCALE = isMobile() ? 0.9 : 1; -const CANVAS_PIXEL_SIZE = 1; -const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; - -// Build-time assets -const STYLESHEET = `___STYLESHEET___`; -const SPRITE_SHEET = "__SPRITE_SHEET__"; -const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__"; - -// Element IDs -const FIELD_GUIDE_ID = "birb-field-guide"; -const FEATHER_ID = "birb-feather"; - -const DEFAULT_BIRD = "bluebird"; - -// Birb movement -const HOP_SPEED = 0.07; -const FLY_SPEED = isMobile() ? 0.125 : 0.25; -const HOP_DISTANCE = 45; - -// Timing constants (in milliseconds) -const UPDATE_INTERVAL = 1000 / 60; // 60 FPS -const AFK_TIME = isDebug() ? 0 : 1000 * 30; -const PET_BOOST_DURATION = 1000 * 60 * 5; -const PET_MENU_COOLDOWN = 1000; -const URL_CHECK_INTERVAL = 500; - -// Random event chances per tick -const HOP_CHANCE = 1 / (60 * 3); // Every 3 seconds -const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds -const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours - -// Feathers -const FEATHER_FALL_SPEED = 1; -const PET_FEATHER_BOOST = 2; - -// Focus element constraints -const MIN_FOCUS_ELEMENT_WIDTH = 100; -const MIN_FOCUS_ELEMENT_TOP = 80; - -/** @type {Partial} */ -let userSettings = {}; - -/** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} - */ -function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(SPRITE.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(SPRITE.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); -} - -log("Loading sprite sheets..."); - -Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) -]).then(([birbPixels, featherPixels]) => { - - const SPRITE_SHEET = birbPixels; - const FEATHER_SPRITE_SHEET = featherPixels; - - const layers = { - base: new Layer(getLayer(SPRITE_SHEET, 0)), - down: new Layer(getLayer(SPRITE_SHEET, 1)), - heartOne: new Layer(getLayer(SPRITE_SHEET, 2)), - heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), - heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), - tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"), - tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"), - wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)), - wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)), - happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), - }; - - const featherLayers = { - feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), - }; - - const birbFrames = { - base: new Frame([layers.base, layers.tuftBase]), - headDown: new Frame([layers.down, layers.tuftDown]), - wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]), - wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]), - heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]), - heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]), - heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - }; - - const featherFrames = { - feather: new Frame([featherLayers.feather]), - }; - - const Animations = { - STILL: new Anim([birbFrames.base], [1000]), - BOB: new Anim([ - birbFrames.base, - birbFrames.headDown - ], [ - 420, - 420 - ]), - FLYING: new Anim([ - birbFrames.base, - birbFrames.wingsUp, - birbFrames.headDown, - birbFrames.wingsDown, - ], [ - 30, - 80, - 30, - 60, - ]), - HEART: new Anim([ - birbFrames.heartOne, - birbFrames.heartTwo, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - ], [ - 60, - 80, - 250, - 250, - 250, - 250, - 250, - 250, - ], false), - }; - - const FEATHER_ANIMATIONS = { - feather: new Anim([ - featherFrames.feather, - ], [ - 1000, - ]), - }; - - const menuItems = [ - new MenuItem(`Pet ${birdBirb()}`, pet), - new MenuItem("Field Guide", insertFieldGuide), - new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote)), - new MenuItem(`Hide ${birdBirb()}`, hideBirb), - new DebugMenuItem("Freeze/Unfreeze", () => { - frozen = !frozen; - }), - new DebugMenuItem("Reset Data", resetSaveData), - new DebugMenuItem("Unlock All", () => { - for (let type in SPECIES) { - unlockBird(type); - } - }), - new DebugMenuItem("Disable Debug", () => { - setDebug(false); - }), - new Separator(), - new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), false), - ]; - - const settingsItems = [ - new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), - new Separator(), - new MenuItem("Toggle Birb Mode", () => { - userSettings.birbMode = !userSettings.birbMode; - save(); - insertModal(`${birdBirb()} Mode`, `Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"${userSettings.birbMode ? "\n\nWelcome back to 2012" : ""}`); - }) - ]; - - const styleElement = document.createElement("style"); - const canvas = document.createElement("canvas"); - - /** @type {CanvasRenderingContext2D} */ - const ctx = canvas.getContext("2d"); - - const States = { - IDLE: "idle", - HOP: "hop", - FLYING: "flying", - }; - - let frozen = false; - let stateStart = Date.now(); - let currentState = States.IDLE; - let animStart = Date.now(); - let currentAnimation = Animations.BOB; - let direction = Directions.RIGHT; - let ticks = 0; - // Bird's current position - let birdY = 0; - let birdX = 40; - // Bird's starting position (when flying) - let startX = 0; - let startY = 0; - // Bird's target position (when flying) - let targetX = 0; - let targetY = 0; - /** @type {HTMLElement|null} */ - let focusedElement = null; - let focusedBounds = { left: 0, right: 0, top: 0 }; - let lastActionTimestamp = Date.now(); - /** @type {number[]} */ - let petStack = []; - let currentSpecies = DEFAULT_BIRD; - let unlockedSpecies = [DEFAULT_BIRD]; - let visible = true; - let lastPetTimestamp = 0; - /** @type {StickyNote[]} */ - let stickyNotes = []; - - /** - * @returns {boolean} Whether the script is running in a userscript extension context - */ - function isUserScript() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - function isTestEnvironment() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - function load() { - /** @type {Record} */ - let saveData = {}; - - if (isUserScript()) { - log("Loading save data from UserScript storage"); - // @ts-expect-error - saveData = GM_getValue("birbSaveData", {}) ?? {}; - } else if (isTestEnvironment()) { - log("Test environment detected, loading save data from localStorage"); - saveData = JSON.parse(localStorage.getItem("birbSaveData") ?? "{}"); - } else { - log("Not a UserScript"); - } - - debug("Loaded data: " + JSON.stringify(saveData)); - - if (!saveData.settings) { - log("No user settings found in save data, starting fresh"); - } - - userSettings = saveData.settings ?? {}; - unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; - currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; - stickyNotes = []; - - if (saveData.stickyNotes) { - for (let note of saveData.stickyNotes) { - if (note.id) { - stickyNotes.push(new StickyNote(note.id, note.site, note.content, note.top, note.left)); - } - } - } - - log(stickyNotes.length + " sticky notes loaded"); - switchSpecies(currentSpecies); - } - - function save() { - /** @type {BirbSaveData} */ - const saveData = { - unlockedSpecies, - currentSpecies, - settings: userSettings - }; - - if (stickyNotes.length > 0) { - saveData.stickyNotes = stickyNotes.map(note => ({ - id: note.id, - site: note.site, - content: note.content, - top: note.top, - left: note.left - })); - } - - if (isUserScript()) { - log("Saving data to UserScript storage"); - // @ts-expect-error - GM_setValue("birbSaveData", saveData); - } else if (isTestEnvironment()) { - log("Test environment detected, saving data to localStorage"); - localStorage.setItem("birbSaveData", JSON.stringify(saveData)); - } else { - log("Not a UserScript"); - } - } - - function resetSaveData() { - if (isUserScript()) { - log("Resetting save data in UserScript storage"); - // @ts-expect-error - GM_deleteValue("birbSaveData"); - } else if (isTestEnvironment()) { - log("Test environment detected, resetting save data in localStorage"); - localStorage.removeItem("birbSaveData"); - } else { - log("Not a UserScript"); - } - load(); - } - - /** - * Get the user settings merged with default settings - * @returns {Settings} The merged settings - */ - function settings() { - return { ...DEFAULT_SETTINGS, ...userSettings }; - } - - /** - * Bird or birb, you decide - */ - function birdBirb() { - return settings().birbMode ? "Birb" : "Bird"; - } - - function init() { - if (window !== window.top) { - // Skip installation if within an iframe - log("In iframe, skipping Birb script initialization"); - return; - } - log("Sprite sheets loaded successfully, initializing bird..."); - - // Preload font - const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf"; - const fontLink = document.createElement("link"); - fontLink.rel = "stylesheet"; - fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`; - document.head.appendChild(fontLink); - - // Add stylesheet font-face - const fontFace = ` - @font-face { - font-family: 'Monocraft'; - src: url(${MONOCRAFT_SRC}) format('opentype'); - font-weight: normal; - font-style: normal; - } - `; - const fontStyle = document.createElement("style"); - fontStyle.innerHTML = fontFace; - document.head.appendChild(fontStyle); - - load(); - - styleElement.innerHTML = STYLESHEET; - document.head.appendChild(styleElement); - - canvas.id = "birb"; - canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE; - canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; - document.body.appendChild(canvas); - - window.addEventListener("scroll", () => { - lastActionTimestamp = Date.now(); - }); - - onClick(document, (e) => { - lastActionTimestamp = Date.now(); - if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { - removeMenu(); - } - }); - - onClick(canvas, () => { - if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { - // Currently being pet, don't open menu - return; - } - insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation); - }); canvas.addEventListener("mouseover", () => { - lastActionTimestamp = Date.now(); - if (currentState === States.IDLE) { - petStack.push(Date.now()); - if (petStack.length > 10) { - petStack.shift(); - } - const pets = petStack.filter((time) => Date.now() - time < 1000).length; - if (pets >= 3) { - pet(); - // Clear the stack - petStack = []; - } - } - }); - - canvas.addEventListener("touchmove", (e) => { - pet(); - }); - - drawStickyNotes(stickyNotes, save, deleteStickyNote); - - let lastUrl = (window.location.href ?? "").split("?")[0]; - setInterval(() => { - const currentUrl = (window.location.href ?? "").split("?")[0]; - if (currentUrl !== lastUrl) { - log("URL changed, updating sticky notes"); - lastUrl = currentUrl; - drawStickyNotes(stickyNotes, save, deleteStickyNote); - } - }, URL_CHECK_INTERVAL); - - setInterval(update, UPDATE_INTERVAL); - } - - function update() { - ticks++; - - // Hide bird if the browser is fullscreen - if (document.fullscreenElement) { - hideBirb(); - // Won't be restored on fullscreen exit - } - - if (currentState === States.IDLE && !frozen && !isMenuOpen()) { - if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { - hop(); - } else if (Date.now() - lastActionTimestamp > AFK_TIME) { - // Idle for a while, do something - if (focusedElement === null) { - // Fly to an element - focusOnElement(); - lastActionTimestamp = Date.now(); - } else if (Math.random() < FOCUS_SWITCH_CHANCE) { - // Fly to another element if idle for a longer while - focusOnElement(); - lastActionTimestamp = Date.now(); - } - } - } else if (currentState === States.HOP) { - if (updateParabolicPath(HOP_SPEED)) { - setState(States.IDLE); - } - } - - // Double the chance of a feather if recently pet - const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1; - if (visible && Math.random() < FEATHER_CHANCE * petMod) { - lastPetTimestamp = 0; - activateFeather(); - } - updateFeather(); - } - - function draw() { - requestAnimationFrame(draw); - - if (!visible) { - return; - } - - updateFocusedElementBounds(); - - // Update the bird's position - if (currentState === States.IDLE) { - if (focusedElement && !isWithinHorizontalBounds()) { - focusOnGround(); - } - birdY = getFocusedY(); - } else if (currentState === States.FLYING) { - // Fly to target location (even if in the air) - if (updateParabolicPath(FLY_SPEED)) { - setState(States.IDLE); - } - } - - const oldTargetY = targetY; - targetY = getFocusedY(); - // Adjust startY to account for scrolling - startY += targetY - oldTargetY; - if (targetY < 0 || targetY > window.innerHeight) { - // Fly to ground if the focused element moves out of bounds - focusOnGround(); - } - - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) { - setAnimation(Animations.STILL); - } - - // Update HTML element position - setX(birdX); - setY(birdY); - } - - /** - * @param {StickyNote} stickyNote - */ - function deleteStickyNote(stickyNote) { - stickyNotes = stickyNotes.filter(note => note.id !== stickyNote.id); - save(); - } - - /** - * Create a window element with header and content - * @param {string} id - * @param {string} title - * @param {string} contentHtml - * @param {() => void} [onClose] - * @returns {HTMLElement} - */ - function createWindow(id, title, contentHtml, onClose) { - const window = makeElement("birb-window", undefined, id); - window.innerHTML = ` -
-
${title}
-
x
-
-
- ${contentHtml} -
- `; - - document.body.appendChild(window); - makeDraggable(window.querySelector(".birb-window-header")); - - const closeButton = window.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - if (onClose) { - onClose(); - } - window.remove(); - }, closeButton); - } - - return window; - } - - function activateFeather() { - if (document.querySelector("#" + FEATHER_ID)) { - return; - } - const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); - if (speciesToUnlock.length === 0) { - // No more species to unlock - return; - } - const birdType = speciesToUnlock[Math.floor(Math.random() * speciesToUnlock.length)]; - insertFeather(birdType); - } - - /** - * @param {string} birdType - */ - function insertFeather(birdType) { - let type = SPECIES[birdType]; - const featherCanvas = document.createElement("canvas"); - featherCanvas.id = FEATHER_ID; - featherCanvas.classList.add("birb-decoration"); - featherCanvas.width = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - featherCanvas.height = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - const x = featherCanvas.width * 2 + Math.random() * (window.innerWidth - featherCanvas.width * 4); - featherCanvas.style.marginLeft = `${x}px`; - featherCanvas.style.top = `${-featherCanvas.height}px`; - const featherCtx = featherCanvas.getContext("2d"); - if (!featherCtx) { - return; - } - FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, type); - document.body.appendChild(featherCanvas); - onClick(featherCanvas, () => { - unlockBird(birdType); - removeFeather(); - if (document.querySelector("#" + FIELD_GUIDE_ID)) { - removeFieldGuide(); - insertFieldGuide(); - } - }); - } - - function removeFeather() { - const feather = document.querySelector("#" + FEATHER_ID); - if (feather) { - feather.remove(); - } - } - - /** - * @param {string} birdType - */ - function unlockBird(birdType) { - if (!unlockedSpecies.includes(birdType)) { - unlockedSpecies.push(birdType); - insertModal("New Bird Unlocked!", `You've found a ${SPECIES[birdType].name} feather! Use the Field Guide to switch your bird's species.`); - } - save(); - } - - function updateFeather() { - const feather = document.querySelector("#birb-feather"); - if (!feather || !(feather instanceof HTMLElement)) { - return; - } - const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED; - feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`; - if (y < window.innerHeight - feather.offsetHeight) { - feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; - } - } - - /** - * @param {HTMLElement} element - */ - function centerElement(element) { - element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; - element.style.top = `${window.innerHeight / 2 - element.offsetHeight / 2}px`; - } - - /** - * @param {string} title - * @param {string} message - */ - function insertModal(title, message) { - if (document.querySelector("#" + FIELD_GUIDE_ID)) { - return; - } - - const modal = createWindow("birb-modal", title, ` -
- ${message} -
- `); - - modal.style.width = "270px"; - centerElement(modal); - } - - /** - * @param {HTMLElement} menu - */ - function updateMenuLocation(menu) { - let x = birdX; - let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; - const offset = 20; - if (x < window.innerWidth / 2) { - // Left side - x += offset; - } else { - // Right side - x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; - } - if (y > window.innerHeight / 2) { - // Top side - y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; - } else { - // Bottom side - y += offset; - } - menu.style.left = `${x}px`; - menu.style.top = `${y}px`; - }; - - function insertFieldGuide() { - if (document.querySelector("#" + FIELD_GUIDE_ID)) { - return; - } - let html = ` -
-
Field Guide
-
x
-
-
-
-
-
` - const fieldGuide = makeElement("birb-window", undefined, FIELD_GUIDE_ID); - fieldGuide.innerHTML = html; - document.body.appendChild(fieldGuide); - makeDraggable(fieldGuide.querySelector(".birb-window-header")); - - const closeButton = fieldGuide.querySelector(".birb-window-close"); - if (closeButton) { - makeClosable(() => { - fieldGuide.remove(); - }, closeButton); - } - - const content = fieldGuide.querySelector(".birb-grid-content"); - if (!content) { - return; - } - content.innerHTML = ""; - - const generateDescription = (/** @type {string} */ speciesId) => { - const type = SPECIES[speciesId]; - const unlocked = unlockedSpecies.includes(speciesId); - return "" + type.name + "
" + (!unlocked ? "Not yet unlocked" : type.description); - }; - - const description = fieldGuide.querySelector(".birb-field-guide-description"); - if (!description) { - return; - } - description.innerHTML = generateDescription(currentSpecies); - for (const [id, type] of Object.entries(SPECIES)) { - const unlocked = unlockedSpecies.includes(id); - const speciesElement = makeElement("birb-grid-item"); - if (id === currentSpecies) { - speciesElement.classList.add("birb-grid-item-selected"); - } - const speciesCanvas = document.createElement("canvas"); - speciesCanvas.width = SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - speciesCanvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; - const speciesCtx = speciesCanvas.getContext("2d"); - if (!speciesCtx) { - return; - } - birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); - speciesElement.appendChild(speciesCanvas); - content.appendChild(speciesElement); - if (unlocked) { - onClick(speciesElement, () => { - switchSpecies(id); - document.querySelectorAll(".birb-grid-item").forEach((element) => { - element.classList.remove("birb-grid-item-selected"); - }); - speciesElement.classList.add("birb-grid-item-selected"); - }); - } else { - speciesElement.classList.add("birb-grid-item-locked"); - } - speciesElement.addEventListener("mouseover", () => { - log("mouseover"); - description.innerHTML = generateDescription(id); - }); - speciesElement.addEventListener("mouseout", () => { - description.innerHTML = generateDescription(currentSpecies); - }); - } - centerElement(fieldGuide); - } - - function removeFieldGuide() { - const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); - if (fieldGuide) { - fieldGuide.remove(); - } - } - - /** - * @param {string} type - */ - function switchSpecies(type) { - currentSpecies = type; - // Update CSS variable --birb-highlight to be wing color - document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[SPRITE.THEME_HIGHLIGHT]); - save(); - } - - /** - * @param {string[][]} array - * @param {number} sprite - * @param {number} [width] - * @returns {string[][]} - */ - function getLayer(array, sprite, width = SPRITE_WIDTH) { - // From an array of a horizontal sprite sheet, get the layer for a specific sprite - const layer = []; - for (let y = 0; y < width; y++) { - layer.push(array[y].slice(sprite * width, (sprite + 1) * width)); - } - return layer; - } - - /** - * Update the birds location from the start to the target location on a parabolic path - * @param {number} speed The speed of the bird along the path - * @param {number} [intensity] The intensity of the parabolic path - * @returns {boolean} Whether the bird has reached the target location - */ - function updateParabolicPath(speed, intensity = 2.5) { - const dx = targetX - startX; - const dy = targetY - startY; - const distance = Math.sqrt(dx * dx + dy * dy); - const time = Date.now() - stateStart; - if (distance > Math.max(window.innerWidth, window.innerHeight) / 2) { - speed *= 1.3; - } - const amount = Math.min(1, time / (distance / speed)); - const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity); - birdX = x; - birdY = y; - const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1; - if (complete) { - birdX = targetX; - birdY = targetY; - } else { - direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT; - } - return complete; - } - - function getFocusedElementRandomX() { - return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; - } - - function isWithinHorizontalBounds() { - return birdX >= focusedBounds.left && birdX <= focusedBounds.right; - } - - function getFocusedY() { - return getFullWindowHeight() - focusedBounds.top; - } - - /** - * @returns The render-safe height of the inner browser window - */ - function getSafeWindowHeight() { - // Necessary because iOS 26 Safari is terrible and won't render - // fixed elements behind the address bar - return window.innerHeight; - } - - /** - * @returns The true height of the inner browser window - */ - function getFullWindowHeight() { - return document.documentElement.clientHeight; - } - - function focusOnGround() { - console.log("Focusing on ground"); - focusedElement = null; - focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; - flyTo(Math.random() * window.innerWidth, 0); - } - - function focusOnElement() { - if (frozen) { - return; - } - const elements = document.querySelectorAll("img, video"); - const inWindow = Array.from(elements).filter((img) => { - const rect = img.getBoundingClientRect(); - return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight; - }); - /** @type {HTMLElement[]} */ - // @ts-expect-error - const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - if (largeElements.length === 0) { - return; - } - const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)]; - focusedElement = randomElement; - log("Focusing on element: ", focusedElement); - updateFocusedElementBounds(); - flyTo(getFocusedElementRandomX(), getFocusedY()); - } - - function updateFocusedElementBounds() { - if (focusedElement === null) { - // Update ground location to bottom of window - focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; - return; - } - const { left, right, top } = focusedElement.getBoundingClientRect(); - focusedBounds = { left, right, top }; - } - - function getCanvasWidth() { - return canvas.width * BIRB_CSS_SCALE - } - - function hop() { - if (frozen) { - return; - } - if (currentState === States.IDLE) { - setState(States.HOP); - setAnimation(Animations.FLYING); - if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { - targetX = birdX - HOP_DISTANCE; - } else { - targetX = birdX + HOP_DISTANCE; - } - targetY = getFocusedY(); - } - } - - function pet() { - if (currentState === States.IDLE && currentAnimation !== Animations.HEART) { - setAnimation(Animations.HEART); - lastPetTimestamp = Date.now(); - } - } - - function hideBirb() { - canvas.style.display = "none"; - visible = false; - } - - /** - * @param {number} x - * @param {number} y - */ - function flyTo(x, y) { - targetX = x; - targetY = y; - setState(States.FLYING); - setAnimation(Animations.FLYING); - } - - /** - * @returns {boolean} Whether the bird should be absolutely positioned - */ - function isAbsolute() { - return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); - } - - /** - * Set the current animation and reset the animation timer - * @param {Anim} animation - */ - function setAnimation(animation) { - currentAnimation = animation; - animStart = Date.now(); - } - - /** - * Set the current state and reset the state timer - * @param {string} state - */ - function setState(state) { - stateStart = Date.now(); - startX = birdX; - startY = birdY; - currentState = state; - if (state === States.IDLE) { - setAnimation(Animations.BOB); - } - if (isAbsolute()) { - canvas.classList.add("birb-absolute"); - } else { - canvas.classList.remove("birb-absolute"); - } - setY(birdY); - } - - /** - * @param {number} x - */ - function setX(x) { - let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2)); - canvas.style.left = `${x + mod}px`; - } - - /** - * @param {number} y - */ - function setY(y) { - let bottom; - if (isAbsolute()) { - // Position is absolute, convert from fixed - bottom = y - window.scrollY; - } else { - // Position is fixed - bottom = y; - } - canvas.style.bottom = `${bottom}px`; - } - - // Helper functions - - /** - * @param {number} startX - * @param {number} startY - * @param {number} endX - * @param {number} endY - * @param {number} amount - * @param {number} [intensity] - * @returns {{x: number, y: number}} - */ - function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) { - const dx = endX - startX; - const dy = endY - startY; - const distance = Math.sqrt(dx * dx + dy * dy); - const angle = Math.atan2(dy, dx); - const midX = startX + Math.cos(angle) * distance / 2; - const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity; - const t = amount; - const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX; - const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY; - return { x, y }; - } - - // Run the birb - init(); - draw(); -}).catch((e) => { - error("Error while loading sprite sheets: ", e); -}); \ No newline at end of file +} \ No newline at end of file From fe497a40201948240c9fb70b44cb7d7e7b89e1cc Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 17:56:49 -0400 Subject: [PATCH 15/16] Move birb rendering to separate file --- dist/birb.js | 437 ++++++++++++++++++++++++++++++-------------- dist/birb.user.js | 439 +++++++++++++++++++++++++++++++-------------- manifest.json | 2 +- src/application.js | 154 ++++------------ src/birb.js | 282 ++++++++++++++++++++++++++++- 5 files changed, 930 insertions(+), 384 deletions(-) diff --git a/dist/birb.js b/dist/birb.js index 31cef6e..5ed00e0 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -492,6 +492,284 @@ } } + /** + * @typedef {keyof typeof Animations} AnimationType + */ + + const Animations = /** @type {const} */ ({ + STILL: "STILL", + BOB: "BOB", + FLYING: "FLYING", + HEART: "HEART" + }); + + class Birb { + animStart = Date.now(); + x = 0; + y = 0; + direction = Directions.RIGHT; + isAbsolutePositioned = false; + visible = true; + /** @type {AnimationType} */ + currentAnimation = Animations.STILL; + + /** + * @param {number} birbCssScale + * @param {number} canvasPixelSize + * @param {string[][]} spriteSheet The loaded sprite sheet pixel data + * @param {number} spriteWidth + * @param {number} spriteHeight + */ + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + this.birbCssScale = birbCssScale; + this.canvasPixelSize = canvasPixelSize; + this.windowPixelSize = canvasPixelSize * birbCssScale; + this.spriteWidth = spriteWidth; + this.spriteHeight = spriteHeight; + + // Build layers from sprite sheet + this.layers = { + base: new Layer(this.getLayer(spriteSheet, 0)), + down: new Layer(this.getLayer(spriteSheet, 1)), + heartOne: new Layer(this.getLayer(spriteSheet, 2)), + heartTwo: new Layer(this.getLayer(spriteSheet, 3)), + heartThree: new Layer(this.getLayer(spriteSheet, 4)), + tuftBase: new Layer(this.getLayer(spriteSheet, 5), "tuft"), + tuftDown: new Layer(this.getLayer(spriteSheet, 6), "tuft"), + wingsUp: new Layer(this.getLayer(spriteSheet, 7)), + wingsDown: new Layer(this.getLayer(spriteSheet, 8)), + happyEye: new Layer(this.getLayer(spriteSheet, 9)), + }; + + // Build frames from layers + this.frames = { + base: new Frame([this.layers.base, this.layers.tuftBase]), + headDown: new Frame([this.layers.down, this.layers.tuftDown]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), + heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), + heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), + heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), + }; + + // Build animations from frames + this.animations = { + [Animations.STILL]: new Anim([this.frames.base], [1000]), + [Animations.BOB]: new Anim([ + this.frames.base, + this.frames.headDown + ], [ + 420, + 420 + ]), + [Animations.FLYING]: new Anim([ + this.frames.base, + this.frames.wingsUp, + this.frames.headDown, + this.frames.wingsDown, + ], [ + 30, + 80, + 30, + 60, + ]), + [Animations.HEART]: new Anim([ + this.frames.heartOne, + this.frames.heartTwo, + this.frames.heartThree, + this.frames.heartFour, + this.frames.heartThree, + this.frames.heartFour, + this.frames.heartThree, + this.frames.heartFour, + ], [ + 60, + 80, + 250, + 250, + 250, + 250, + 250, + 250, + ], false), + }; + + // Create canvas element + this.canvas = document.createElement("canvas"); + this.canvas.id = "birb"; + this.canvas.width = this.frames.base.getPixels()[0].length * canvasPixelSize; + this.canvas.height = spriteHeight * canvasPixelSize; + + this.ctx = this.canvas.getContext("2d"); + + // Append to document + document.body.appendChild(this.canvas); + } + + /** + * Draw the current animation frame + * @param {BirdType} species The species color data + * @returns {boolean} Whether the animation has completed (for non-looping animations) + */ + draw(species) { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + const anim = this.animations[this.currentAnimation]; + return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); + } + + /** + * Get a layer from the sprite sheet array + * @param {string[][]} array + * @param {number} sprite + * @returns {string[][]} + */ + getLayer(array, sprite) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < this.spriteWidth; y++) { + layer.push(array[y].slice(sprite * this.spriteWidth, (sprite + 1) * this.spriteWidth)); + } + return layer; + } + + /** + * @returns {AnimationType} The current animation key + */ + getCurrentAnimation() { + return this.currentAnimation; + } + + /** + * Set the current animation by name and reset the animation timer + * @param {AnimationType} animationName + */ + setAnimation(animationName) { + this.currentAnimation = animationName; + this.animStart = Date.now(); + } + + /** + * Get the frames object + * @returns {Record} + */ + getFrames() { + return this.frames; + } + + /** + * Get the canvas element + * @returns {HTMLCanvasElement} + */ + getElement() { + return this.canvas; + } + + /** + * Get the canvas width in CSS pixels + * @returns {number} + */ + getElementWidth() { + return this.canvas.width * this.birbCssScale; + } + + /** + * Get the canvas height in CSS pixels + * @returns {number} + */ + getElementHeight() { + return this.canvas.height * this.birbCssScale; + } + + getElementTop() { + const rect = this.canvas.getBoundingClientRect(); + return rect.top; + } + + /** + * Set the X position + * @param {number} x + */ + setX(x) { + this.x = x; + let mod = this.getElementWidth() / -2 - (this.windowPixelSize * (this.direction === Directions.RIGHT ? 2 : -2)); + this.canvas.style.left = `${x + mod}px`; + } + + /** + * Set the Y position + * @param {number} y + */ + setY(y) { + this.y = y; + let bottom; + if (this.isAbsolutePositioned) { + // Position is absolute, convert from fixed + bottom = y - window.scrollY; + } else { + // Position is fixed + bottom = y; + } + this.canvas.style.bottom = `${bottom}px`; + } + + /** + * Get the current X position + * @returns {number} + */ + getX() { + return this.x; + } + + /** + * Get the current Y position + * @returns {number} + */ + getY() { + return this.y; + } + + /** + * Set the direction the bird is facing + * @param {number} direction + */ + setDirection(direction) { + this.direction = direction; + } + + /** + * Set whether the element should be absolutely positioned + * @param {boolean} absolute + */ + setAbsolutePositioned(absolute) { + this.isAbsolutePositioned = absolute; + if (absolute) { + this.canvas.classList.add("birb-absolute"); + } else { + this.canvas.classList.remove("birb-absolute"); + } + // Update Y position to apply the new positioning mode + this.setY(this.y); + } + + /** + * Set visibility of the bird + * @param {boolean} visible + */ + setVisible(visible) { + this.visible = visible; + this.canvas.style.display = visible ? "" : "none"; + } + + /** + * Get visibility of the bird + * @returns {boolean} + */ + isVisible() { + return this.visible; + } + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -1264,79 +1542,14 @@ const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; - const layers = { - base: new Layer(getLayer(SPRITE_SHEET, 0)), - down: new Layer(getLayer(SPRITE_SHEET, 1)), - heartOne: new Layer(getLayer(SPRITE_SHEET, 2)), - heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), - heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), - tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"), - tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"), - wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)), - wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)), - happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), - }; - const featherLayers = { feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), }; - const birbFrames = { - base: new Frame([layers.base, layers.tuftBase]), - headDown: new Frame([layers.down, layers.tuftDown]), - wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]), - wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]), - heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]), - heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]), - heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - }; - const featherFrames = { feather: new Frame([featherLayers.feather]), }; - const Animations = { - STILL: new Anim([birbFrames.base], [1000]), - BOB: new Anim([ - birbFrames.base, - birbFrames.headDown - ], [ - 420, - 420 - ]), - FLYING: new Anim([ - birbFrames.base, - birbFrames.wingsUp, - birbFrames.headDown, - birbFrames.wingsDown, - ], [ - 30, - 80, - 30, - 60, - ]), - HEART: new Anim([ - birbFrames.heartOne, - birbFrames.heartTwo, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - ], [ - 60, - 80, - 250, - 250, - 250, - 250, - 250, - 250, - ], false), - }; - const FEATHER_ANIMATIONS = { feather: new Anim([ featherFrames.feather, @@ -1377,10 +1590,9 @@ ]; const styleElement = document.createElement("style"); - const canvas = document.createElement("canvas"); - /** @type {CanvasRenderingContext2D} */ - const ctx = canvas.getContext("2d"); + /** @type {Birb} */ + let birb; const States = { IDLE: "idle", @@ -1391,9 +1603,6 @@ let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; - let animStart = Date.now(); - let currentAnimation = Animations.BOB; - let direction = Directions.RIGHT; let ticks = 0; // Bird's current position let birdY = 0; @@ -1561,10 +1770,8 @@ styleElement.innerHTML = STYLESHEET; document.head.appendChild(styleElement); - canvas.id = "birb"; - canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE; - canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; - document.body.appendChild(canvas); + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb.setAnimation(Animations.BOB); window.addEventListener("scroll", () => { lastActionTimestamp = Date.now(); @@ -1577,13 +1784,17 @@ } }); - onClick(canvas, () => { - if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { + const birbElement = birb.getElement(); + + onClick(birbElement, () => { + if (birb.getCurrentAnimation() === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { // Currently being pet, don't open menu return; } insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation); - }); canvas.addEventListener("mouseover", () => { + }); + + birbElement.addEventListener("mouseover", () => { lastActionTimestamp = Date.now(); if (currentState === States.IDLE) { petStack.push(Date.now()); @@ -1599,7 +1810,7 @@ } }); - canvas.addEventListener("touchmove", (e) => { + birbElement.addEventListener("touchmove", (e) => { pet(); }); @@ -1628,7 +1839,7 @@ } if (currentState === States.IDLE && !frozen && !isMenuOpen()) { - if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { + if (Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { // Idle for a while, do something @@ -1660,7 +1871,7 @@ function draw() { requestAnimationFrame(draw); - if (!visible) { + if (!birb.isVisible()) { return; } @@ -1688,14 +1899,13 @@ focusOnGround(); } - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) { - setAnimation(Animations.STILL); + if (birb.draw(SPECIES[currentSpecies])) { + birb.setAnimation(Animations.STILL); } // Update HTML element position - setX(birdX); - setY(birdY); + birb.setX(birdX); + birb.setY(birdY); } /** @@ -1843,7 +2053,7 @@ */ function updateMenuLocation(menu) { let x = birdX; - let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; + let y = birb.getElementTop() + birb.getElementHeight() / 2 + WINDOW_PIXEL_SIZE * 10; const offset = 20; if (x < window.innerWidth / 2) { // Left side @@ -1917,7 +2127,7 @@ if (!speciesCtx) { return; } - birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); + birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); speciesElement.appendChild(speciesCanvas); content.appendChild(speciesElement); if (unlocked) { @@ -1997,7 +2207,7 @@ birdX = targetX; birdY = targetY; } else { - direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT; + birb.setDirection(targetX > birdX ? Directions.RIGHT : Directions.LEFT); } return complete; } @@ -2069,17 +2279,13 @@ focusedBounds = { left, right, top }; } - function getCanvasWidth() { - return canvas.width * BIRB_CSS_SCALE - } - function hop() { if (frozen) { return; } if (currentState === States.IDLE) { setState(States.HOP); - setAnimation(Animations.FLYING); + birb.setAnimation(Animations.FLYING); if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { targetX = birdX - HOP_DISTANCE; } else { @@ -2090,14 +2296,14 @@ } function pet() { - if (currentState === States.IDLE && currentAnimation !== Animations.HEART) { - setAnimation(Animations.HEART); + if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + birb.setAnimation(Animations.HEART); lastPetTimestamp = Date.now(); } } function hideBirb() { - canvas.style.display = "none"; + birb.setVisible(false); visible = false; } @@ -2109,7 +2315,7 @@ targetX = x; targetY = y; setState(States.FLYING); - setAnimation(Animations.FLYING); + birb.setAnimation(Animations.FLYING); } /** @@ -2119,15 +2325,6 @@ return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); } - /** - * Set the current animation and reset the animation timer - * @param {Anim} animation - */ - function setAnimation(animation) { - currentAnimation = animation; - animStart = Date.now(); - } - /** * Set the current state and reset the state timer * @param {string} state @@ -2138,37 +2335,17 @@ startY = birdY; currentState = state; if (state === States.IDLE) { - setAnimation(Animations.BOB); - } - if (isAbsolute()) { - canvas.classList.add("birb-absolute"); - } else { - canvas.classList.remove("birb-absolute"); + birb.setAnimation(Animations.BOB); } + birb.setAbsolutePositioned(isAbsolute()); setY(birdY); } - /** - * @param {number} x - */ - function setX(x) { - let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2)); - canvas.style.left = `${x + mod}px`; - } - /** * @param {number} y */ function setY(y) { - let bottom; - if (isAbsolute()) { - // Position is absolute, convert from fixed - bottom = y - window.scrollY; - } else { - // Position is fixed - bottom = y; - } - canvas.style.bottom = `${bottom}px`; + birb.setY(y); } // Helper functions diff --git a/dist/birb.user.js b/dist/birb.user.js index 76d2bf5..1b07866 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.240 +// @version 2025.10.26.388 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -506,6 +506,284 @@ } } + /** + * @typedef {keyof typeof Animations} AnimationType + */ + + const Animations = /** @type {const} */ ({ + STILL: "STILL", + BOB: "BOB", + FLYING: "FLYING", + HEART: "HEART" + }); + + class Birb { + animStart = Date.now(); + x = 0; + y = 0; + direction = Directions.RIGHT; + isAbsolutePositioned = false; + visible = true; + /** @type {AnimationType} */ + currentAnimation = Animations.STILL; + + /** + * @param {number} birbCssScale + * @param {number} canvasPixelSize + * @param {string[][]} spriteSheet The loaded sprite sheet pixel data + * @param {number} spriteWidth + * @param {number} spriteHeight + */ + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + this.birbCssScale = birbCssScale; + this.canvasPixelSize = canvasPixelSize; + this.windowPixelSize = canvasPixelSize * birbCssScale; + this.spriteWidth = spriteWidth; + this.spriteHeight = spriteHeight; + + // Build layers from sprite sheet + this.layers = { + base: new Layer(this.getLayer(spriteSheet, 0)), + down: new Layer(this.getLayer(spriteSheet, 1)), + heartOne: new Layer(this.getLayer(spriteSheet, 2)), + heartTwo: new Layer(this.getLayer(spriteSheet, 3)), + heartThree: new Layer(this.getLayer(spriteSheet, 4)), + tuftBase: new Layer(this.getLayer(spriteSheet, 5), "tuft"), + tuftDown: new Layer(this.getLayer(spriteSheet, 6), "tuft"), + wingsUp: new Layer(this.getLayer(spriteSheet, 7)), + wingsDown: new Layer(this.getLayer(spriteSheet, 8)), + happyEye: new Layer(this.getLayer(spriteSheet, 9)), + }; + + // Build frames from layers + this.frames = { + base: new Frame([this.layers.base, this.layers.tuftBase]), + headDown: new Frame([this.layers.down, this.layers.tuftDown]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), + heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), + heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), + heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), + }; + + // Build animations from frames + this.animations = { + [Animations.STILL]: new Anim([this.frames.base], [1000]), + [Animations.BOB]: new Anim([ + this.frames.base, + this.frames.headDown + ], [ + 420, + 420 + ]), + [Animations.FLYING]: new Anim([ + this.frames.base, + this.frames.wingsUp, + this.frames.headDown, + this.frames.wingsDown, + ], [ + 30, + 80, + 30, + 60, + ]), + [Animations.HEART]: new Anim([ + this.frames.heartOne, + this.frames.heartTwo, + this.frames.heartThree, + this.frames.heartFour, + this.frames.heartThree, + this.frames.heartFour, + this.frames.heartThree, + this.frames.heartFour, + ], [ + 60, + 80, + 250, + 250, + 250, + 250, + 250, + 250, + ], false), + }; + + // Create canvas element + this.canvas = document.createElement("canvas"); + this.canvas.id = "birb"; + this.canvas.width = this.frames.base.getPixels()[0].length * canvasPixelSize; + this.canvas.height = spriteHeight * canvasPixelSize; + + this.ctx = this.canvas.getContext("2d"); + + // Append to document + document.body.appendChild(this.canvas); + } + + /** + * Draw the current animation frame + * @param {BirdType} species The species color data + * @returns {boolean} Whether the animation has completed (for non-looping animations) + */ + draw(species) { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + const anim = this.animations[this.currentAnimation]; + return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); + } + + /** + * Get a layer from the sprite sheet array + * @param {string[][]} array + * @param {number} sprite + * @returns {string[][]} + */ + getLayer(array, sprite) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < this.spriteWidth; y++) { + layer.push(array[y].slice(sprite * this.spriteWidth, (sprite + 1) * this.spriteWidth)); + } + return layer; + } + + /** + * @returns {AnimationType} The current animation key + */ + getCurrentAnimation() { + return this.currentAnimation; + } + + /** + * Set the current animation by name and reset the animation timer + * @param {AnimationType} animationName + */ + setAnimation(animationName) { + this.currentAnimation = animationName; + this.animStart = Date.now(); + } + + /** + * Get the frames object + * @returns {Record} + */ + getFrames() { + return this.frames; + } + + /** + * Get the canvas element + * @returns {HTMLCanvasElement} + */ + getElement() { + return this.canvas; + } + + /** + * Get the canvas width in CSS pixels + * @returns {number} + */ + getElementWidth() { + return this.canvas.width * this.birbCssScale; + } + + /** + * Get the canvas height in CSS pixels + * @returns {number} + */ + getElementHeight() { + return this.canvas.height * this.birbCssScale; + } + + getElementTop() { + const rect = this.canvas.getBoundingClientRect(); + return rect.top; + } + + /** + * Set the X position + * @param {number} x + */ + setX(x) { + this.x = x; + let mod = this.getElementWidth() / -2 - (this.windowPixelSize * (this.direction === Directions.RIGHT ? 2 : -2)); + this.canvas.style.left = `${x + mod}px`; + } + + /** + * Set the Y position + * @param {number} y + */ + setY(y) { + this.y = y; + let bottom; + if (this.isAbsolutePositioned) { + // Position is absolute, convert from fixed + bottom = y - window.scrollY; + } else { + // Position is fixed + bottom = y; + } + this.canvas.style.bottom = `${bottom}px`; + } + + /** + * Get the current X position + * @returns {number} + */ + getX() { + return this.x; + } + + /** + * Get the current Y position + * @returns {number} + */ + getY() { + return this.y; + } + + /** + * Set the direction the bird is facing + * @param {number} direction + */ + setDirection(direction) { + this.direction = direction; + } + + /** + * Set whether the element should be absolutely positioned + * @param {boolean} absolute + */ + setAbsolutePositioned(absolute) { + this.isAbsolutePositioned = absolute; + if (absolute) { + this.canvas.classList.add("birb-absolute"); + } else { + this.canvas.classList.remove("birb-absolute"); + } + // Update Y position to apply the new positioning mode + this.setY(this.y); + } + + /** + * Set visibility of the bird + * @param {boolean} visible + */ + setVisible(visible) { + this.visible = visible; + this.canvas.style.display = visible ? "" : "none"; + } + + /** + * Get visibility of the bird + * @returns {boolean} + */ + isVisible() { + return this.visible; + } + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -1278,79 +1556,14 @@ const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; - const layers = { - base: new Layer(getLayer(SPRITE_SHEET, 0)), - down: new Layer(getLayer(SPRITE_SHEET, 1)), - heartOne: new Layer(getLayer(SPRITE_SHEET, 2)), - heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), - heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), - tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"), - tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"), - wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)), - wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)), - happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), - }; - const featherLayers = { feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), }; - const birbFrames = { - base: new Frame([layers.base, layers.tuftBase]), - headDown: new Frame([layers.down, layers.tuftDown]), - wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]), - wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]), - heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]), - heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]), - heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - }; - const featherFrames = { feather: new Frame([featherLayers.feather]), }; - const Animations = { - STILL: new Anim([birbFrames.base], [1000]), - BOB: new Anim([ - birbFrames.base, - birbFrames.headDown - ], [ - 420, - 420 - ]), - FLYING: new Anim([ - birbFrames.base, - birbFrames.wingsUp, - birbFrames.headDown, - birbFrames.wingsDown, - ], [ - 30, - 80, - 30, - 60, - ]), - HEART: new Anim([ - birbFrames.heartOne, - birbFrames.heartTwo, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - ], [ - 60, - 80, - 250, - 250, - 250, - 250, - 250, - 250, - ], false), - }; - const FEATHER_ANIMATIONS = { feather: new Anim([ featherFrames.feather, @@ -1391,10 +1604,9 @@ ]; const styleElement = document.createElement("style"); - const canvas = document.createElement("canvas"); - /** @type {CanvasRenderingContext2D} */ - const ctx = canvas.getContext("2d"); + /** @type {Birb} */ + let birb; const States = { IDLE: "idle", @@ -1405,9 +1617,6 @@ let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; - let animStart = Date.now(); - let currentAnimation = Animations.BOB; - let direction = Directions.RIGHT; let ticks = 0; // Bird's current position let birdY = 0; @@ -1575,10 +1784,8 @@ styleElement.innerHTML = STYLESHEET; document.head.appendChild(styleElement); - canvas.id = "birb"; - canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE; - canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; - document.body.appendChild(canvas); + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb.setAnimation(Animations.BOB); window.addEventListener("scroll", () => { lastActionTimestamp = Date.now(); @@ -1591,13 +1798,17 @@ } }); - onClick(canvas, () => { - if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { + const birbElement = birb.getElement(); + + onClick(birbElement, () => { + if (birb.getCurrentAnimation() === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { // Currently being pet, don't open menu return; } insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation); - }); canvas.addEventListener("mouseover", () => { + }); + + birbElement.addEventListener("mouseover", () => { lastActionTimestamp = Date.now(); if (currentState === States.IDLE) { petStack.push(Date.now()); @@ -1613,7 +1824,7 @@ } }); - canvas.addEventListener("touchmove", (e) => { + birbElement.addEventListener("touchmove", (e) => { pet(); }); @@ -1642,7 +1853,7 @@ } if (currentState === States.IDLE && !frozen && !isMenuOpen()) { - if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { + if (Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { // Idle for a while, do something @@ -1674,7 +1885,7 @@ function draw() { requestAnimationFrame(draw); - if (!visible) { + if (!birb.isVisible()) { return; } @@ -1702,14 +1913,13 @@ focusOnGround(); } - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) { - setAnimation(Animations.STILL); + if (birb.draw(SPECIES[currentSpecies])) { + birb.setAnimation(Animations.STILL); } // Update HTML element position - setX(birdX); - setY(birdY); + birb.setX(birdX); + birb.setY(birdY); } /** @@ -1857,7 +2067,7 @@ */ function updateMenuLocation(menu) { let x = birdX; - let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; + let y = birb.getElementTop() + birb.getElementHeight() / 2 + WINDOW_PIXEL_SIZE * 10; const offset = 20; if (x < window.innerWidth / 2) { // Left side @@ -1931,7 +2141,7 @@ if (!speciesCtx) { return; } - birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); + birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); speciesElement.appendChild(speciesCanvas); content.appendChild(speciesElement); if (unlocked) { @@ -2011,7 +2221,7 @@ birdX = targetX; birdY = targetY; } else { - direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT; + birb.setDirection(targetX > birdX ? Directions.RIGHT : Directions.LEFT); } return complete; } @@ -2083,17 +2293,13 @@ focusedBounds = { left, right, top }; } - function getCanvasWidth() { - return canvas.width * BIRB_CSS_SCALE - } - function hop() { if (frozen) { return; } if (currentState === States.IDLE) { setState(States.HOP); - setAnimation(Animations.FLYING); + birb.setAnimation(Animations.FLYING); if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { targetX = birdX - HOP_DISTANCE; } else { @@ -2104,14 +2310,14 @@ } function pet() { - if (currentState === States.IDLE && currentAnimation !== Animations.HEART) { - setAnimation(Animations.HEART); + if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + birb.setAnimation(Animations.HEART); lastPetTimestamp = Date.now(); } } function hideBirb() { - canvas.style.display = "none"; + birb.setVisible(false); visible = false; } @@ -2123,7 +2329,7 @@ targetX = x; targetY = y; setState(States.FLYING); - setAnimation(Animations.FLYING); + birb.setAnimation(Animations.FLYING); } /** @@ -2133,15 +2339,6 @@ return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); } - /** - * Set the current animation and reset the animation timer - * @param {Anim} animation - */ - function setAnimation(animation) { - currentAnimation = animation; - animStart = Date.now(); - } - /** * Set the current state and reset the state timer * @param {string} state @@ -2152,37 +2349,17 @@ startY = birdY; currentState = state; if (state === States.IDLE) { - setAnimation(Animations.BOB); - } - if (isAbsolute()) { - canvas.classList.add("birb-absolute"); - } else { - canvas.classList.remove("birb-absolute"); + birb.setAnimation(Animations.BOB); } + birb.setAbsolutePositioned(isAbsolute()); setY(birdY); } - /** - * @param {number} x - */ - function setX(x) { - let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2)); - canvas.style.left = `${x + mod}px`; - } - /** * @param {number} y */ function setY(y) { - let bottom; - if (isAbsolute()) { - // Position is absolute, convert from fixed - bottom = y - window.scrollY; - } else { - // Position is fixed - bottom = y; - } - canvas.style.bottom = `${bottom}px`; + birb.setY(y); } // Helper functions diff --git a/manifest.json b/manifest.json index aa13af2..4b21fbd 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.240", + "version": "2025.10.26.388", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/application.js b/src/application.js index 283bfb3..0017a41 100644 --- a/src/application.js +++ b/src/application.js @@ -1,6 +1,8 @@ import Frame from './frame.js'; import Layer from './layer.js'; import Anim from './anim.js'; +import { Birb, Animations } from './birb.js'; + import { Directions, isDebug, @@ -169,79 +171,14 @@ Promise.all([ const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; - const layers = { - base: new Layer(getLayer(SPRITE_SHEET, 0)), - down: new Layer(getLayer(SPRITE_SHEET, 1)), - heartOne: new Layer(getLayer(SPRITE_SHEET, 2)), - heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), - heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), - tuftBase: new Layer(getLayer(SPRITE_SHEET, 5), "tuft"), - tuftDown: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"), - wingsUp: new Layer(getLayer(SPRITE_SHEET, 7)), - wingsDown: new Layer(getLayer(SPRITE_SHEET, 8)), - happyEye: new Layer(getLayer(SPRITE_SHEET, 9)), - }; - const featherLayers = { feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), }; - const birbFrames = { - base: new Frame([layers.base, layers.tuftBase]), - headDown: new Frame([layers.down, layers.tuftDown]), - wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]), - wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]), - heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]), - heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]), - heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), - }; - const featherFrames = { feather: new Frame([featherLayers.feather]), }; - const Animations = { - STILL: new Anim([birbFrames.base], [1000]), - BOB: new Anim([ - birbFrames.base, - birbFrames.headDown - ], [ - 420, - 420 - ]), - FLYING: new Anim([ - birbFrames.base, - birbFrames.wingsUp, - birbFrames.headDown, - birbFrames.wingsDown, - ], [ - 30, - 80, - 30, - 60, - ]), - HEART: new Anim([ - birbFrames.heartOne, - birbFrames.heartTwo, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - ], [ - 60, - 80, - 250, - 250, - 250, - 250, - 250, - 250, - ], false), - }; - const FEATHER_ANIMATIONS = { feather: new Anim([ featherFrames.feather, @@ -282,10 +219,9 @@ Promise.all([ ]; const styleElement = document.createElement("style"); - const canvas = document.createElement("canvas"); - /** @type {CanvasRenderingContext2D} */ - const ctx = canvas.getContext("2d"); + /** @type {Birb} */ + let birb; const States = { IDLE: "idle", @@ -296,9 +232,6 @@ Promise.all([ let frozen = false; let stateStart = Date.now(); let currentState = States.IDLE; - let animStart = Date.now(); - let currentAnimation = Animations.BOB; - let direction = Directions.RIGHT; let ticks = 0; // Bird's current position let birdY = 0; @@ -466,10 +399,8 @@ Promise.all([ styleElement.innerHTML = STYLESHEET; document.head.appendChild(styleElement); - canvas.id = "birb"; - canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE; - canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; - document.body.appendChild(canvas); + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb.setAnimation(Animations.BOB); window.addEventListener("scroll", () => { lastActionTimestamp = Date.now(); @@ -482,13 +413,17 @@ Promise.all([ } }); - onClick(canvas, () => { - if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { + const birbElement = birb.getElement(); + + onClick(birbElement, () => { + if (birb.getCurrentAnimation() === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { // Currently being pet, don't open menu return; } insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation); - }); canvas.addEventListener("mouseover", () => { + }); + + birbElement.addEventListener("mouseover", () => { lastActionTimestamp = Date.now(); if (currentState === States.IDLE) { petStack.push(Date.now()); @@ -504,7 +439,7 @@ Promise.all([ } }); - canvas.addEventListener("touchmove", (e) => { + birbElement.addEventListener("touchmove", (e) => { pet(); }); @@ -533,7 +468,7 @@ Promise.all([ } if (currentState === States.IDLE && !frozen && !isMenuOpen()) { - if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) { + if (Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== Animations.HEART) { hop(); } else if (Date.now() - lastActionTimestamp > AFK_TIME) { // Idle for a while, do something @@ -565,7 +500,7 @@ Promise.all([ function draw() { requestAnimationFrame(draw); - if (!visible) { + if (!birb.isVisible()) { return; } @@ -593,14 +528,13 @@ Promise.all([ focusOnGround(); } - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart, CANVAS_PIXEL_SIZE, SPECIES[currentSpecies])) { - setAnimation(Animations.STILL); + if (birb.draw(SPECIES[currentSpecies])) { + birb.setAnimation(Animations.STILL); } // Update HTML element position - setX(birdX); - setY(birdY); + birb.setX(birdX); + birb.setY(birdY); } /** @@ -751,7 +685,7 @@ Promise.all([ */ function updateMenuLocation(menu) { let x = birdX; - let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; + let y = birb.getElementTop() + birb.getElementHeight() / 2 + WINDOW_PIXEL_SIZE * 10; const offset = 20; if (x < window.innerWidth / 2) { // Left side @@ -826,7 +760,7 @@ Promise.all([ if (!speciesCtx) { return; } - birbFrames.base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); + birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); speciesElement.appendChild(speciesCanvas); content.appendChild(speciesElement); if (unlocked) { @@ -906,7 +840,7 @@ Promise.all([ birdX = targetX; birdY = targetY; } else { - direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT; + birb.setDirection(targetX > birdX ? Directions.RIGHT : Directions.LEFT); } return complete; } @@ -979,7 +913,7 @@ Promise.all([ } function getCanvasWidth() { - return canvas.width * BIRB_CSS_SCALE + return birb.getElementWidth(); } function hop() { @@ -988,7 +922,7 @@ Promise.all([ } if (currentState === States.IDLE) { setState(States.HOP); - setAnimation(Animations.FLYING); + birb.setAnimation(Animations.FLYING); if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { targetX = birdX - HOP_DISTANCE; } else { @@ -999,14 +933,14 @@ Promise.all([ } function pet() { - if (currentState === States.IDLE && currentAnimation !== Animations.HEART) { - setAnimation(Animations.HEART); + if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + birb.setAnimation(Animations.HEART); lastPetTimestamp = Date.now(); } } function hideBirb() { - canvas.style.display = "none"; + birb.setVisible(false); visible = false; } @@ -1018,7 +952,7 @@ Promise.all([ targetX = x; targetY = y; setState(States.FLYING); - setAnimation(Animations.FLYING); + birb.setAnimation(Animations.FLYING); } /** @@ -1028,15 +962,6 @@ Promise.all([ return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); } - /** - * Set the current animation and reset the animation timer - * @param {Anim} animation - */ - function setAnimation(animation) { - currentAnimation = animation; - animStart = Date.now(); - } - /** * Set the current state and reset the state timer * @param {string} state @@ -1047,13 +972,9 @@ Promise.all([ startY = birdY; currentState = state; if (state === States.IDLE) { - setAnimation(Animations.BOB); - } - if (isAbsolute()) { - canvas.classList.add("birb-absolute"); - } else { - canvas.classList.remove("birb-absolute"); + birb.setAnimation(Animations.BOB); } + birb.setAbsolutePositioned(isAbsolute()); setY(birdY); } @@ -1061,23 +982,14 @@ Promise.all([ * @param {number} x */ function setX(x) { - let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2)); - canvas.style.left = `${x + mod}px`; + birb.setX(x); } /** * @param {number} y */ function setY(y) { - let bottom; - if (isAbsolute()) { - // Position is absolute, convert from fixed - bottom = y - window.scrollY; - } else { - // Position is fixed - bottom = y; - } - canvas.style.bottom = `${bottom}px`; + birb.setY(y); } // Helper functions diff --git a/src/birb.js b/src/birb.js index b98d3f5..41959d3 100644 --- a/src/birb.js +++ b/src/birb.js @@ -1,3 +1,283 @@ -export class Birb { +import { Directions } from './shared.js'; +import Layer from './layer.js'; +import Frame from './frame.js'; +import Anim from './anim.js'; +import { BirdType } from './sprites.js'; +/** + * @typedef {keyof typeof Animations} AnimationType + */ + +export const Animations = /** @type {const} */ ({ + STILL: "STILL", + BOB: "BOB", + FLYING: "FLYING", + HEART: "HEART" +}); + +export class Birb { + animStart = Date.now(); + x = 0; + y = 0; + direction = Directions.RIGHT; + isAbsolutePositioned = false; + visible = true; + /** @type {AnimationType} */ + currentAnimation = Animations.STILL; + + /** + * @param {number} birbCssScale + * @param {number} canvasPixelSize + * @param {string[][]} spriteSheet The loaded sprite sheet pixel data + * @param {number} spriteWidth + * @param {number} spriteHeight + */ + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + this.birbCssScale = birbCssScale; + this.canvasPixelSize = canvasPixelSize; + this.windowPixelSize = canvasPixelSize * birbCssScale; + this.spriteWidth = spriteWidth; + this.spriteHeight = spriteHeight; + + // Build layers from sprite sheet + this.layers = { + base: new Layer(this.getLayer(spriteSheet, 0)), + down: new Layer(this.getLayer(spriteSheet, 1)), + heartOne: new Layer(this.getLayer(spriteSheet, 2)), + heartTwo: new Layer(this.getLayer(spriteSheet, 3)), + heartThree: new Layer(this.getLayer(spriteSheet, 4)), + tuftBase: new Layer(this.getLayer(spriteSheet, 5), "tuft"), + tuftDown: new Layer(this.getLayer(spriteSheet, 6), "tuft"), + wingsUp: new Layer(this.getLayer(spriteSheet, 7)), + wingsDown: new Layer(this.getLayer(spriteSheet, 8)), + happyEye: new Layer(this.getLayer(spriteSheet, 9)), + }; + + // Build frames from layers + this.frames = { + base: new Frame([this.layers.base, this.layers.tuftBase]), + headDown: new Frame([this.layers.down, this.layers.tuftDown]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), + heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), + heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), + heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), + }; + + // Build animations from frames + this.animations = { + [Animations.STILL]: new Anim([this.frames.base], [1000]), + [Animations.BOB]: new Anim([ + this.frames.base, + this.frames.headDown + ], [ + 420, + 420 + ]), + [Animations.FLYING]: new Anim([ + this.frames.base, + this.frames.wingsUp, + this.frames.headDown, + this.frames.wingsDown, + ], [ + 30, + 80, + 30, + 60, + ]), + [Animations.HEART]: new Anim([ + this.frames.heartOne, + this.frames.heartTwo, + this.frames.heartThree, + this.frames.heartFour, + this.frames.heartThree, + this.frames.heartFour, + this.frames.heartThree, + this.frames.heartFour, + ], [ + 60, + 80, + 250, + 250, + 250, + 250, + 250, + 250, + ], false), + }; + + // Create canvas element + this.canvas = document.createElement("canvas"); + this.canvas.id = "birb"; + this.canvas.width = this.frames.base.getPixels()[0].length * canvasPixelSize; + this.canvas.height = spriteHeight * canvasPixelSize; + + this.ctx = this.canvas.getContext("2d"); + + // Append to document + document.body.appendChild(this.canvas); + } + + /** + * Draw the current animation frame + * @param {BirdType} species The species color data + * @returns {boolean} Whether the animation has completed (for non-looping animations) + */ + draw(species) { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + const anim = this.animations[this.currentAnimation]; + return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); + } + + /** + * Get a layer from the sprite sheet array + * @param {string[][]} array + * @param {number} sprite + * @returns {string[][]} + */ + getLayer(array, sprite) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < this.spriteWidth; y++) { + layer.push(array[y].slice(sprite * this.spriteWidth, (sprite + 1) * this.spriteWidth)); + } + return layer; + } + + /** + * @returns {AnimationType} The current animation key + */ + getCurrentAnimation() { + return this.currentAnimation; + } + + /** + * Set the current animation by name and reset the animation timer + * @param {AnimationType} animationName + */ + setAnimation(animationName) { + this.currentAnimation = animationName; + this.animStart = Date.now(); + } + + /** + * Get the frames object + * @returns {Record} + */ + getFrames() { + return this.frames; + } + + /** + * Get the canvas element + * @returns {HTMLCanvasElement} + */ + getElement() { + return this.canvas; + } + + /** + * Get the canvas width in CSS pixels + * @returns {number} + */ + getElementWidth() { + return this.canvas.width * this.birbCssScale; + } + + /** + * Get the canvas height in CSS pixels + * @returns {number} + */ + getElementHeight() { + return this.canvas.height * this.birbCssScale; + } + + getElementTop() { + const rect = this.canvas.getBoundingClientRect(); + return rect.top; + } + + /** + * Set the X position + * @param {number} x + */ + setX(x) { + this.x = x; + let mod = this.getElementWidth() / -2 - (this.windowPixelSize * (this.direction === Directions.RIGHT ? 2 : -2)); + this.canvas.style.left = `${x + mod}px`; + } + + /** + * Set the Y position + * @param {number} y + */ + setY(y) { + this.y = y; + let bottom; + if (this.isAbsolutePositioned) { + // Position is absolute, convert from fixed + bottom = y - window.scrollY; + } else { + // Position is fixed + bottom = y; + } + this.canvas.style.bottom = `${bottom}px`; + } + + /** + * Get the current X position + * @returns {number} + */ + getX() { + return this.x; + } + + /** + * Get the current Y position + * @returns {number} + */ + getY() { + return this.y; + } + + /** + * Set the direction the bird is facing + * @param {number} direction + */ + setDirection(direction) { + this.direction = direction; + } + + /** + * Set whether the element should be absolutely positioned + * @param {boolean} absolute + */ + setAbsolutePositioned(absolute) { + this.isAbsolutePositioned = absolute; + if (absolute) { + this.canvas.classList.add("birb-absolute"); + } else { + this.canvas.classList.remove("birb-absolute"); + } + // Update Y position to apply the new positioning mode + this.setY(this.y); + } + + /** + * Set visibility of the bird + * @param {boolean} visible + */ + setVisible(visible) { + this.visible = visible; + this.canvas.style.display = visible ? "" : "none"; + } + + /** + * Get visibility of the bird + * @returns {boolean} + */ + isVisible() { + return this.visible; + } } \ No newline at end of file From 938b139298b1640de957beac16fbdc9946282500 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 26 Oct 2025 18:03:15 -0400 Subject: [PATCH 16/16] Remove duplicate function --- README.md | 22 +------------- dist/birb.js | 71 ++++++++++++++++++-------------------------- dist/birb.user.js | 73 ++++++++++++++++++---------------------------- manifest.json | 2 +- src/application.js | 19 ++---------- src/birb.js | 37 +++++++---------------- src/menu.js | 5 ++-- src/shared.js | 18 +++++++++++- 8 files changed, 92 insertions(+), 155 deletions(-) diff --git a/README.md b/README.md index 391b87b..b73fe0d 100644 --- a/README.md +++ b/README.md @@ -5,24 +5,4 @@ This project is still being worked on, but if you wish to help me beta test it, 1. Install [Tampermonkey](https://www.tampermonkey.net/) on your web browser 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. \ No newline at end of file +4. Now any websites you visit will have a little bird hopping around! \ No newline at end of file diff --git a/dist/birb.js b/dist/birb.js index 5ed00e0..934f7a6 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -82,7 +82,7 @@ let elementToMove = parent ? element.parentElement : element; if (!elementToMove) { - console.error("Birb: Parent element not found"); + error("Birb: Parent element not found"); return; } @@ -171,6 +171,22 @@ console.error("Birb: ", ...arguments); } + /** + * Get a layer from a sprite sheet array + * @param {string[][]} spriteSheet The sprite sheet pixel array + * @param {number} spriteIndex The sprite index + * @param {number} width The width of each sprite + * @returns {string[][]} + */ + function getLayer(spriteSheet, spriteIndex, width) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < width; y++) { + layer.push(spriteSheet[y].slice(spriteIndex * width, (spriteIndex + 1) * width)); + } + return layer; + } + /** Indicators for parts of the base bird sprite sheet */ const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", @@ -529,16 +545,16 @@ // Build layers from sprite sheet this.layers = { - base: new Layer(this.getLayer(spriteSheet, 0)), - down: new Layer(this.getLayer(spriteSheet, 1)), - heartOne: new Layer(this.getLayer(spriteSheet, 2)), - heartTwo: new Layer(this.getLayer(spriteSheet, 3)), - heartThree: new Layer(this.getLayer(spriteSheet, 4)), - tuftBase: new Layer(this.getLayer(spriteSheet, 5), "tuft"), - tuftDown: new Layer(this.getLayer(spriteSheet, 6), "tuft"), - wingsUp: new Layer(this.getLayer(spriteSheet, 7)), - wingsDown: new Layer(this.getLayer(spriteSheet, 8)), - happyEye: new Layer(this.getLayer(spriteSheet, 9)), + base: new Layer(getLayer(spriteSheet, 0, this.spriteWidth)), + down: new Layer(getLayer(spriteSheet, 1, this.spriteWidth)), + heartOne: new Layer(getLayer(spriteSheet, 2, this.spriteWidth)), + heartTwo: new Layer(getLayer(spriteSheet, 3, this.spriteWidth)), + heartThree: new Layer(getLayer(spriteSheet, 4, this.spriteWidth)), + tuftBase: new Layer(getLayer(spriteSheet, 5, this.spriteWidth), "tuft"), + tuftDown: new Layer(getLayer(spriteSheet, 6, this.spriteWidth), "tuft"), + wingsUp: new Layer(getLayer(spriteSheet, 7, this.spriteWidth)), + wingsDown: new Layer(getLayer(spriteSheet, 8, this.spriteWidth)), + happyEye: new Layer(getLayer(spriteSheet, 9, this.spriteWidth)), }; // Build frames from layers @@ -618,21 +634,6 @@ return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); } - /** - * Get a layer from the sprite sheet array - * @param {string[][]} array - * @param {number} sprite - * @returns {string[][]} - */ - getLayer(array, sprite) { - // From an array of a horizontal sprite sheet, get the layer for a specific sprite - const layer = []; - for (let y = 0; y < this.spriteWidth; y++) { - layer.push(array[y].slice(sprite * this.spriteWidth, (sprite + 1) * this.spriteWidth)); - } - return layer; - } - /** * @returns {AnimationType} The current animation key */ @@ -1053,7 +1054,7 @@ } const content = menu.querySelector(".birb-window-content"); if (!content) { - console.error("Birb: Content not found"); + error("Birb: Content not found"); return; } content.innerHTML = ""; @@ -2169,21 +2170,6 @@ save(); } - /** - * @param {string[][]} array - * @param {number} sprite - * @param {number} [width] - * @returns {string[][]} - */ - function getLayer(array, sprite, width = SPRITE_WIDTH) { - // From an array of a horizontal sprite sheet, get the layer for a specific sprite - const layer = []; - for (let y = 0; y < width; y++) { - layer.push(array[y].slice(sprite * width, (sprite + 1) * width)); - } - return layer; - } - /** * Update the birds location from the start to the target location on a parabolic path * @param {number} speed The speed of the bird along the path @@ -2241,7 +2227,6 @@ } function focusOnGround() { - console.log("Focusing on ground"); focusedElement = null; focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; flyTo(Math.random() * window.innerWidth, 0); diff --git a/dist/birb.user.js b/dist/birb.user.js index 1b07866..55dd927 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.10.26.388 +// @version 2025.10.26.402 // @description birb // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js @@ -96,7 +96,7 @@ let elementToMove = parent ? element.parentElement : element; if (!elementToMove) { - console.error("Birb: Parent element not found"); + error("Birb: Parent element not found"); return; } @@ -185,6 +185,22 @@ console.error("Birb: ", ...arguments); } + /** + * Get a layer from a sprite sheet array + * @param {string[][]} spriteSheet The sprite sheet pixel array + * @param {number} spriteIndex The sprite index + * @param {number} width The width of each sprite + * @returns {string[][]} + */ + function getLayer(spriteSheet, spriteIndex, width) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < width; y++) { + layer.push(spriteSheet[y].slice(spriteIndex * width, (spriteIndex + 1) * width)); + } + return layer; + } + /** Indicators for parts of the base bird sprite sheet */ const SPRITE = { THEME_HIGHLIGHT: "theme-highlight", @@ -543,16 +559,16 @@ // Build layers from sprite sheet this.layers = { - base: new Layer(this.getLayer(spriteSheet, 0)), - down: new Layer(this.getLayer(spriteSheet, 1)), - heartOne: new Layer(this.getLayer(spriteSheet, 2)), - heartTwo: new Layer(this.getLayer(spriteSheet, 3)), - heartThree: new Layer(this.getLayer(spriteSheet, 4)), - tuftBase: new Layer(this.getLayer(spriteSheet, 5), "tuft"), - tuftDown: new Layer(this.getLayer(spriteSheet, 6), "tuft"), - wingsUp: new Layer(this.getLayer(spriteSheet, 7)), - wingsDown: new Layer(this.getLayer(spriteSheet, 8)), - happyEye: new Layer(this.getLayer(spriteSheet, 9)), + base: new Layer(getLayer(spriteSheet, 0, this.spriteWidth)), + down: new Layer(getLayer(spriteSheet, 1, this.spriteWidth)), + heartOne: new Layer(getLayer(spriteSheet, 2, this.spriteWidth)), + heartTwo: new Layer(getLayer(spriteSheet, 3, this.spriteWidth)), + heartThree: new Layer(getLayer(spriteSheet, 4, this.spriteWidth)), + tuftBase: new Layer(getLayer(spriteSheet, 5, this.spriteWidth), "tuft"), + tuftDown: new Layer(getLayer(spriteSheet, 6, this.spriteWidth), "tuft"), + wingsUp: new Layer(getLayer(spriteSheet, 7, this.spriteWidth)), + wingsDown: new Layer(getLayer(spriteSheet, 8, this.spriteWidth)), + happyEye: new Layer(getLayer(spriteSheet, 9, this.spriteWidth)), }; // Build frames from layers @@ -632,21 +648,6 @@ return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); } - /** - * Get a layer from the sprite sheet array - * @param {string[][]} array - * @param {number} sprite - * @returns {string[][]} - */ - getLayer(array, sprite) { - // From an array of a horizontal sprite sheet, get the layer for a specific sprite - const layer = []; - for (let y = 0; y < this.spriteWidth; y++) { - layer.push(array[y].slice(sprite * this.spriteWidth, (sprite + 1) * this.spriteWidth)); - } - return layer; - } - /** * @returns {AnimationType} The current animation key */ @@ -1067,7 +1068,7 @@ } const content = menu.querySelector(".birb-window-content"); if (!content) { - console.error("Birb: Content not found"); + error("Birb: Content not found"); return; } content.innerHTML = ""; @@ -2183,21 +2184,6 @@ save(); } - /** - * @param {string[][]} array - * @param {number} sprite - * @param {number} [width] - * @returns {string[][]} - */ - function getLayer(array, sprite, width = SPRITE_WIDTH) { - // From an array of a horizontal sprite sheet, get the layer for a specific sprite - const layer = []; - for (let y = 0; y < width; y++) { - layer.push(array[y].slice(sprite * width, (sprite + 1) * width)); - } - return layer; - } - /** * Update the birds location from the start to the target location on a parabolic path * @param {number} speed The speed of the bird along the path @@ -2255,7 +2241,6 @@ } function focusOnGround() { - console.log("Focusing on ground"); focusedElement = null; focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; flyTo(Math.random() * window.innerWidth, 0); diff --git a/manifest.json b/manifest.json index 4b21fbd..7aaf858 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a bird, in your browser. What more could you want?", - "version": "2025.10.26.388", + "version": "2025.10.26.402", "homepage_url": "https://idreesinc.com", "content_scripts": [ { diff --git a/src/application.js b/src/application.js index 0017a41..ff23bc0 100644 --- a/src/application.js +++ b/src/application.js @@ -14,7 +14,8 @@ import { isMobile, log, debug, - error + error, + getLayer } from './shared.js'; import { SPRITE, @@ -802,21 +803,6 @@ Promise.all([ save(); } - /** - * @param {string[][]} array - * @param {number} sprite - * @param {number} [width] - * @returns {string[][]} - */ - function getLayer(array, sprite, width = SPRITE_WIDTH) { - // From an array of a horizontal sprite sheet, get the layer for a specific sprite - const layer = []; - for (let y = 0; y < width; y++) { - layer.push(array[y].slice(sprite * width, (sprite + 1) * width)); - } - return layer; - } - /** * Update the birds location from the start to the target location on a parabolic path * @param {number} speed The speed of the bird along the path @@ -874,7 +860,6 @@ Promise.all([ } function focusOnGround() { - console.log("Focusing on ground"); focusedElement = null; focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; flyTo(Math.random() * window.innerWidth, 0); diff --git a/src/birb.js b/src/birb.js index 41959d3..96c8adf 100644 --- a/src/birb.js +++ b/src/birb.js @@ -1,4 +1,4 @@ -import { Directions } from './shared.js'; +import { Directions, getLayer } from './shared.js'; import Layer from './layer.js'; import Frame from './frame.js'; import Anim from './anim.js'; @@ -41,16 +41,16 @@ export class Birb { // Build layers from sprite sheet this.layers = { - base: new Layer(this.getLayer(spriteSheet, 0)), - down: new Layer(this.getLayer(spriteSheet, 1)), - heartOne: new Layer(this.getLayer(spriteSheet, 2)), - heartTwo: new Layer(this.getLayer(spriteSheet, 3)), - heartThree: new Layer(this.getLayer(spriteSheet, 4)), - tuftBase: new Layer(this.getLayer(spriteSheet, 5), "tuft"), - tuftDown: new Layer(this.getLayer(spriteSheet, 6), "tuft"), - wingsUp: new Layer(this.getLayer(spriteSheet, 7)), - wingsDown: new Layer(this.getLayer(spriteSheet, 8)), - happyEye: new Layer(this.getLayer(spriteSheet, 9)), + base: new Layer(getLayer(spriteSheet, 0, this.spriteWidth)), + down: new Layer(getLayer(spriteSheet, 1, this.spriteWidth)), + heartOne: new Layer(getLayer(spriteSheet, 2, this.spriteWidth)), + heartTwo: new Layer(getLayer(spriteSheet, 3, this.spriteWidth)), + heartThree: new Layer(getLayer(spriteSheet, 4, this.spriteWidth)), + tuftBase: new Layer(getLayer(spriteSheet, 5, this.spriteWidth), "tuft"), + tuftDown: new Layer(getLayer(spriteSheet, 6, this.spriteWidth), "tuft"), + wingsUp: new Layer(getLayer(spriteSheet, 7, this.spriteWidth)), + wingsDown: new Layer(getLayer(spriteSheet, 8, this.spriteWidth)), + happyEye: new Layer(getLayer(spriteSheet, 9, this.spriteWidth)), }; // Build frames from layers @@ -130,21 +130,6 @@ export class Birb { return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); } - /** - * Get a layer from the sprite sheet array - * @param {string[][]} array - * @param {number} sprite - * @returns {string[][]} - */ - getLayer(array, sprite) { - // From an array of a horizontal sprite sheet, get the layer for a specific sprite - const layer = []; - for (let y = 0; y < this.spriteWidth; y++) { - layer.push(array[y].slice(sprite * this.spriteWidth, (sprite + 1) * this.spriteWidth)); - } - return layer; - } - /** * @returns {AnimationType} The current animation key */ diff --git a/src/menu.js b/src/menu.js index ba4c659..8e14717 100644 --- a/src/menu.js +++ b/src/menu.js @@ -3,7 +3,8 @@ import { makeElement, onClick, makeDraggable, - makeClosable + makeClosable, + error } from './shared.js'; export const MENU_ID = "birb-menu"; @@ -124,7 +125,7 @@ export function switchMenuItems(menuItems, updateLocationCallback) { } const content = menu.querySelector(".birb-window-content"); if (!content) { - console.error("Birb: Content not found"); + error("Birb: Content not found"); return; } content.innerHTML = ""; diff --git a/src/shared.js b/src/shared.js index 5a6f193..30fe9aa 100644 --- a/src/shared.js +++ b/src/shared.js @@ -79,7 +79,7 @@ export function makeDraggable(element, parent = true, callback = () => { }) { let elementToMove = parent ? element.parentElement : element; if (!elementToMove) { - console.error("Birb: Parent element not found"); + error("Birb: Parent element not found"); return; } @@ -166,4 +166,20 @@ export function debug() { export function error() { console.error("Birb: ", ...arguments); +} + +/** + * Get a layer from a sprite sheet array + * @param {string[][]} spriteSheet The sprite sheet pixel array + * @param {number} spriteIndex The sprite index + * @param {number} width The width of each sprite + * @returns {string[][]} + */ +export function getLayer(spriteSheet, spriteIndex, width) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < width; y++) { + layer.push(spriteSheet[y].slice(spriteIndex * width, (spriteIndex + 1) * width)); + } + return layer; } \ No newline at end of file