From 1c70c0cc137a8dc16395d40e19e840229878a1a1 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 2 Nov 2025 14:56:37 -0500 Subject: [PATCH] Build into separate folders --- README.md | 2 +- build.js | 28 +- dist/birb.js | 2 +- dist/extension/birb.js | 2659 +++++++++++++++++ .../images/icons/transparent/1024x1024x1.png | Bin 0 -> 6957 bytes .../images/icons/transparent/1024x768x1.png | Bin 0 -> 5578 bytes .../images/icons/transparent/128x128x1.png | Bin 0 -> 1152 bytes .../images/icons/transparent/128x128x2.png | Bin 0 -> 1587 bytes .../images/icons/transparent/16x16x1.png | Bin 0 -> 635 bytes .../images/icons/transparent/16x16x2.png | Bin 0 -> 829 bytes .../images/icons/transparent/256x256x1.png | Bin 0 -> 1587 bytes .../images/icons/transparent/256x256x2.png | Bin 0 -> 2956 bytes .../images/icons/transparent/27x20x2.png | Bin 0 -> 848 bytes .../images/icons/transparent/27x20x3.png | Bin 0 -> 944 bytes .../images/icons/transparent/29x29x2.png | Bin 0 -> 914 bytes .../images/icons/transparent/29x29x3.png | Bin 0 -> 1018 bytes .../images/icons/transparent/32x24x2.png | Bin 0 -> 881 bytes .../images/icons/transparent/32x24x3.png | Bin 0 -> 953 bytes .../images/icons/transparent/32x32x1.png | Bin 0 -> 829 bytes .../images/icons/transparent/32x32x2.png | Bin 0 -> 936 bytes .../images/icons/transparent/48x48x1.png | Bin 0 -> 856 bytes .../images/icons/transparent/512x512x1.png | Bin 0 -> 2956 bytes .../images/icons/transparent/512x512x2.png | Bin 0 -> 6957 bytes .../images/icons/transparent/60x45x2.png | Bin 0 -> 1014 bytes .../images/icons/transparent/60x45x3.png | Bin 0 -> 1190 bytes .../images/icons/transparent/67x50x2.png | Bin 0 -> 1059 bytes .../images/icons/transparent/74x55x2.png | Bin 0 -> 1116 bytes .../images/icons/transparent/96x96x1.png | Bin 0 -> 1018 bytes .../icons/transparent/icon-transparent.png | Bin 0 -> 10462 bytes dist/extension/manifest.json | 41 + dist/{ => userscript}/birb.user.js | 8 +- manifest.json | 4 +- 32 files changed, 2729 insertions(+), 15 deletions(-) create mode 100644 dist/extension/birb.js create mode 100644 dist/extension/images/icons/transparent/1024x1024x1.png create mode 100644 dist/extension/images/icons/transparent/1024x768x1.png create mode 100644 dist/extension/images/icons/transparent/128x128x1.png create mode 100644 dist/extension/images/icons/transparent/128x128x2.png create mode 100644 dist/extension/images/icons/transparent/16x16x1.png create mode 100644 dist/extension/images/icons/transparent/16x16x2.png create mode 100644 dist/extension/images/icons/transparent/256x256x1.png create mode 100644 dist/extension/images/icons/transparent/256x256x2.png create mode 100644 dist/extension/images/icons/transparent/27x20x2.png create mode 100644 dist/extension/images/icons/transparent/27x20x3.png create mode 100644 dist/extension/images/icons/transparent/29x29x2.png create mode 100644 dist/extension/images/icons/transparent/29x29x3.png create mode 100644 dist/extension/images/icons/transparent/32x24x2.png create mode 100644 dist/extension/images/icons/transparent/32x24x3.png create mode 100644 dist/extension/images/icons/transparent/32x32x1.png create mode 100644 dist/extension/images/icons/transparent/32x32x2.png create mode 100644 dist/extension/images/icons/transparent/48x48x1.png create mode 100644 dist/extension/images/icons/transparent/512x512x1.png create mode 100644 dist/extension/images/icons/transparent/512x512x2.png create mode 100644 dist/extension/images/icons/transparent/60x45x2.png create mode 100644 dist/extension/images/icons/transparent/60x45x3.png create mode 100644 dist/extension/images/icons/transparent/67x50x2.png create mode 100644 dist/extension/images/icons/transparent/74x55x2.png create mode 100644 dist/extension/images/icons/transparent/96x96x1.png create mode 100644 dist/extension/images/icons/transparent/icon-transparent.png create mode 100644 dist/extension/manifest.json rename dist/{ => userscript}/birb.user.js (99%) diff --git a/README.md b/README.md index b73fe0d..8925215 100644 --- a/README.md +++ b/README.md @@ -4,5 +4,5 @@ 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) +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/userscript/birb.user.js](https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js) 4. Now any websites you visit will have a little bird hopping around! \ No newline at end of file diff --git a/build.js b/build.js index 060294a..546e4f3 100644 --- a/build.js +++ b/build.js @@ -1,7 +1,7 @@ // @ts-check import { rollup } from 'rollup'; -import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs'; +import { readFileSync, writeFileSync, mkdirSync, unlinkSync, cpSync } from 'fs'; const spriteSheets = [ { @@ -56,8 +56,8 @@ const userScriptHeader = // @version ${version} // @description birb // @author Idrees -// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js -// @updateURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js +// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js +// @updateURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js // @match *://*/* // @grant GM_setValue // @grant GM_getValue @@ -80,6 +80,9 @@ await bundle.close(); let birbJs = readFileSync('dist/birb.bundled.js', 'utf8'); +// Delete bundled file +unlinkSync('./dist/birb.bundled.js'); + // Replace version placeholder birbJs = birbJs.replaceAll('__VERSION__', version); @@ -96,9 +99,20 @@ 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 +mkdirSync('./dist/userscript', { recursive: true }); const userScript = userScriptHeader + birbJs; -writeFileSync('./dist/birb.user.js', userScript); \ No newline at end of file +writeFileSync('./dist/userscript/birb.user.js', userScript); + +// Build browser extension +mkdirSync('./dist/extension', { recursive: true }); +// Copy birb.js +writeFileSync('./dist/extension/birb.js', birbJs); +// Copy manifest.json +const manifestContent = readFileSync('./manifest.json', 'utf8'); +writeFileSync('./dist/extension/manifest.json', manifestContent); +// Copy icons folder +mkdirSync('./dist/extension/images/icons', { recursive: true }); +cpSync('./images/icons/transparent', './dist/extension/images/icons/transparent', { recursive: true }); + +console.log(`Build completed: version ${version}`); \ No newline at end of file diff --git a/dist/birb.js b/dist/birb.js index fcd8f75..f254f97 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -1867,7 +1867,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.2.44", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.2.44"); }, false), + new MenuItem("2025.11.2.60", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.2.60"); }, false), ]; const styleElement = document.createElement("style"); diff --git a/dist/extension/birb.js b/dist/extension/birb.js new file mode 100644 index 0000000..f254f97 --- /dev/null +++ b/dist/extension/birb.js @@ -0,0 +1,2659 @@ +(function () { + 'use strict'; + + const Directions = { + LEFT: -1, + RIGHT: 1, + }; + + let debugMode = location.hostname === "127.0.0.1"; + + /** + * @returns {boolean} Whether debug mode is enabled + */ + function isDebug() { + return debugMode; + } + + /** + * @param {boolean} value + */ + function setDebug(value) { + debugMode = value; + } + + /** + * 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) { + 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(); + } + }); + } + + /** + * @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); + } + + /** + * 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; + } + + /** + * The height of the inner browser window + * Will be the same as getFixedWindowHeight() on most browsers + * On iOS, it will vary to be the height excluding the current address bar size (potentially greater than fixed height) + */ + function getWindowHeight() { + // Necessary because iOS 26 Safari is terrible and won't render + // fixed/sticky elements behind the address bar + return window.innerHeight; + } + + /** + * The fixed height of the inner browser window + * Will be the same as getWindowHeight() on most browsers + * On iOS, it will always be the height of the window when the address bar is fully expanded + * @returns The true height of the inner browser window + */ + function getFixedWindowHeight() { + return document.documentElement.clientHeight; + } + + /** Indicators for parts of the base bird sprite sheet */ + 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 { + /** + * @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} */ + 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", + }), + }; + + 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(Sprite.TRANSPARENT)); + } + // Combine layers + for (let i = 1; i < layers.length; i++) { + if (layers[i].tag === "default" || layers[i].tag === tag) { + let layerPixels = layers[i].pixels; + let topMargin = maxHeight - layerPixels.length; + for (let y = 0; y < layerPixels.length; y++) { + for (let x = 0; x < layerPixels[y].length; x++) { + this.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; + } + } + } + } + this.#pixelsByTag[tag] = this.pixels.map(row => row.slice()); + } + } + + /** + * @param {string} [tag] + * @returns {string[][]} + */ + getPixels(tag = "default") { + return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"]; + } + + /** + * @param {CanvasRenderingContext2D} ctx + * @param {BirdType} [species] + * @param {number} direction + * @param {number} canvasPixelSize + */ + draw(ctx, direction, canvasPixelSize, species) { + // Clear the canvas before drawing the new frame + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + const pixels = this.getPixels(species?.tags[0]); + for (let y = 0; y < pixels.length; y++) { + const row = pixels[y]; + for (let x = 0; x < pixels[y].length; x++) { + const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1]; + ctx.fillStyle = species?.colors[cell] ?? cell; + ctx.fillRect(x * canvasPixelSize, y * canvasPixelSize, canvasPixelSize, canvasPixelSize); + } } } + } + + class Anim { + /** + * @param {Frame[]} frames + * @param {number[]} durations + * @param {boolean} loop + */ + constructor(frames, durations, loop = true) { + this.frames = frames; + this.durations = durations; + this.loop = loop; + this.lastFrameIndex = -1; + this.lastDirection = null; + this.lastTimeStart = null; + } + + getAnimationDuration() { + return this.durations.reduce((a, b) => a + b, 0); + } + + /** + * Get the current frame index based on elapsed time + * @param {number} time The elapsed time since animation start + * @returns {number} The index of the current frame + */ + getCurrentFrameIndex(time) { + let totalDuration = 0; + for (let i = 0; i < this.durations.length; i++) { + totalDuration += this.durations[i]; + if (time < totalDuration) { + return i; + } + } + return this.frames.length - 1; + } + + /** + * Clear the cached frame state + */ + #clearCache() { + this.lastFrameIndex = -1; + this.lastDirection = null; + } + + /** + * Check if the frame needs to be redrawn + * @param {number} frameIndex The current frame index + * @param {number} direction The current direction + * @returns {boolean} Whether the frame needs to be redrawn + */ + #shouldRedraw(frameIndex, direction) { + return frameIndex !== this.lastFrameIndex || direction !== this.lastDirection; + } + + /** + * @param {CanvasRenderingContext2D} ctx + * @param {number} direction + * @param {number} timeStart The start time of the animation in milliseconds + * @param {number} canvasPixelSize The size of a canvas pixel in pixels + * @param {BirdType} [species] The species to use for the animation + * @returns {boolean} Whether the animation is complete + */ + draw(ctx, direction, timeStart, canvasPixelSize, species) { + // Reset cache if animation was restarted + if (this.lastTimeStart !== timeStart) { + this.#clearCache(); + this.lastTimeStart = timeStart; + } + + let time = Date.now() - timeStart; + const duration = this.getAnimationDuration(); + + if (this.loop) { + time %= duration; + } + + const currentFrameIndex = this.getCurrentFrameIndex(time); + + if (this.#shouldRedraw(currentFrameIndex, direction)) { + this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, species); + this.lastFrameIndex = currentFrameIndex; + this.lastDirection = direction; + } + + // Return whether animation is complete (for non-looping animations) + return !this.loop && time >= duration; + } + } + + /** + * @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(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 + 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) { + const anim = this.animations[this.currentAnimation]; + return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); + } + + /** + * @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 + // Account for address bar shrinkage on iOS + bottom = y - window.scrollY - (getWindowHeight() - getFixedWindowHeight()); + } 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; + } + } + + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {boolean} Whether this context is applicable + */ + isContextActive() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + } + + class LocalContext extends Context { + + /** + * @override + * @returns {boolean} + */ + isContextActive() { + return window.location.hostname === "127.0.0.1" + || window.location.hostname === "localhost" + || window.location.hostname.startsWith("192.168."); + } + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + class UserScriptContext extends Context { + + /** + * @override + * @returns {boolean} + */ + isContextActive() { + // @ts-expect-error + return typeof GM_getValue === "function"; + } + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from UserScript storage"); + /** @type {BirbSaveData|{}} */ + let saveData = {}; + // @ts-expect-error + saveData = GM_getValue(SAVE_KEY, {}) ?? {}; + return saveData; + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to UserScript storage"); + // @ts-expect-error + GM_setValue(SAVE_KEY, saveData); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in UserScript storage"); + // @ts-expect-error + GM_deleteValue(SAVE_KEY); + } + } + + class BrowserExtensionContext extends Context { + + /** + * @override + * @returns {boolean} + */ + isContextActive() { + // @ts-expect-error + return typeof chrome !== "undefined"; + } + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from browser extension storage"); + return new Promise((resolve) => { + // @ts-expect-error + chrome.storage.sync.get([SAVE_KEY], (result) => { + resolve(result[SAVE_KEY] ?? {}); + }); + }); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to browser extension storage"); + // @ts-expect-error + chrome.storage.sync.set({ [SAVE_KEY]: saveData }, function () { + // @ts-expect-error + if (chrome.runtime.lastError) { + // @ts-expect-error + console.error(chrome.runtime.lastError); + } else { + console.log("Settings saved successfully"); + } + }); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in browser extension storage"); + // @ts-expect-error + chrome.storage.sync.clear(); + } + } + + const CONTEXTS = [ + new UserScriptContext(), + new BrowserExtensionContext(), + new LocalContext() + ]; + + function getContext() { + for (const context of CONTEXTS) { + if (context.isContextActive()) { + return context; + } + } + error("No applicable context found, defaulting to LocalContext"); + return CONTEXTS[0]; + } + + /** + * @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; + } + + /** + * @param {StickyNote} stickyNote + * @param {() => void} onSave + * @param {() => void} onDelete + * @returns {HTMLElement} + */ + function renderStickyNote(stickyNote, onSave, onDelete) { + const noteElement = makeElement("birb-window"); + noteElement.classList.add("birb-sticky-note"); + + // Create header + const header = makeElement("birb-window-header"); + const titleDiv = makeElement("birb-window-title", "Sticky Note"); + const closeButton = makeElement("birb-window-close", "x"); + header.appendChild(titleDiv); + header.appendChild(closeButton); + + // Create content + const content = makeElement("birb-window-content"); + const textarea = document.createElement("textarea"); + textarea.className = "birb-sticky-note-input"; + textarea.style.width = "150px"; + textarea.placeholder = "Write your notes here and they'll stick to the page!"; + textarea.value = stickyNote.content; + content.appendChild(textarea); + + noteElement.appendChild(header); + noteElement.appendChild(content); + + noteElement.style.top = `${stickyNote.top}px`; + noteElement.style.left = `${stickyNote.left}px`; + document.body.appendChild(noteElement); + + makeDraggable(header, true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + onSave(); + }); + + if (closeButton) { + makeClosable(() => { + if (stickyNote.content.trim() === "" || confirm("Are you sure you want to delete this sticky note?")) { + onDelete(); + noteElement.remove(); + } + }, closeButton); + } + + 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(); + } + + const MENU_ID = "birb-menu"; + const MENU_EXIT_ID = "birb-menu-exit"; + + 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("", () => { }); + } + } + + /** + * @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 {MenuItem[]} menuItems + * @param {string} title + * @param {(menu: HTMLElement) => void} updateLocationCallback + */ + function insertMenu(menuItems, title, updateLocationCallback) { + if (document.querySelector("#" + MENU_ID)) { + return; + } + let menu = makeElement("birb-window", undefined, MENU_ID); + let header = makeElement("birb-window-header"); + const titleDiv = makeElement("birb-window-title", title); + header.appendChild(titleDiv); + let content = makeElement("birb-window-content"); + const removeCallback = () => removeMenu(); + for (const item of menuItems) { + if (!item.isDebug || isDebug()) { + 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, MENU_EXIT_ID); + onClick(menuExit, removeCallback); + document.body.appendChild(menuExit); + makeClosable(removeCallback); + + updateLocationCallback(menu); + } + + /** + * 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 {MenuItem[]} menuItems + * @param {(menu: HTMLElement) => void} updateLocationCallback + */ + function switchMenuItems(menuItems, updateLocationCallback) { + const menu = document.querySelector("#" + MENU_ID); + if (!menu || !(menu instanceof HTMLElement)) { + return; + } + const content = menu.querySelector(".birb-window-content"); + if (!content) { + error("Birb: Content not found"); + return; + } + while (content.firstChild) { + content.removeChild(content.firstChild); + } + const removeCallback = () => removeMenu(); + for (const item of menuItems) { + if (!item.isDebug || isDebug()) { + content.appendChild(makeMenuItem(item, removeCallback)); + } + } + updateLocationCallback(menu); + } + + /** + * @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 = `: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); + --birb-neg-double-border-size: calc(var(--birb-neg-border-size) * 2); + --birb-highlight: #ffa3cb; + --birb-border-color: var(--birb-highlight); + --birb-background-color: #ffecda; + --birb-mix-color: color-mix(in srgb, var(--birb-highlight) 50%, var(--birb-background-color)); + --birb-scale: ${BIRB_CSS_SCALE}; + --birb-ui-scale: ${UI_CSS_SCALE}; +} + +#birb { + image-rendering: pixelated; + position: fixed; + bottom: 0; + transform: scale(var(--birb-scale)) !important; + transform-origin: bottom; + z-index: 2147483638 !important; + cursor: pointer; +} + +.birb-absolute { + position: absolute !important; +} + +.birb-decoration { + image-rendering: pixelated; + position: fixed; + bottom: 0; + transform: scale(var(--birb-scale)) !important; + transform-origin: bottom; + z-index: 2147483630 !important; +} + +.birb-window { + font-family: "Monocraft", monospace !important; + line-height: initial !important; + color: #000000 !important; + z-index: 2147483639 !important; + position: fixed; + background-color: var(--birb-background-color); + box-shadow: + var(--birb-border-size) 0 var(--birb-border-color), + var(--birb-neg-border-size) 0 var(--birb-border-color), + 0 var(--birb-neg-border-size) var(--birb-border-color), + 0 var(--birb-border-size) var(--birb-border-color), + var(--birb-double-border-size) 0 var(--birb-border-color), + var(--birb-neg-double-border-size) 0 var(--birb-border-color), + 0 var(--birb-neg-double-border-size) var(--birb-border-color), + 0 var(--birb-double-border-size) var(--birb-border-color), + 0 0 0 var(--birb-border-size) var(--birb-border-color), + 0 0 0 var(--birb-double-border-size) white, + var(--birb-double-border-size) 0 0 var(--birb-border-size) white, + var(--birb-neg-double-border-size) 0 0 var(--birb-border-size) white, + 0 var(--birb-neg-double-border-size) 0 var(--birb-border-size) white, + 0 var(--birb-double-border-size) 0 var(--birb-border-size) white; + box-sizing: border-box; + display: flex; + flex-direction: column; + transform: scale(var(--birb-ui-scale)) !important; + animation: pop-in 0.08s; + transition-timing-function: ease-in; +} + +#birb-menu { + transition-duration: 0.2s; + transition-timing-function: ease-out; + min-width: 140px; + z-index: 2147483639 !important; +} + +#birb-menu-exit { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2147483637 !important; +} + +@keyframes pop-in { + 0% { + opacity: 1; + transform: scale(0.1); + } + + 100% { + opacity: 1; + transform: scale(var(--birb-ui-scale)); + } +} + +.birb-window-header { + box-sizing: border-box; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + padding: 7px; + padding-top: 3px; + padding-bottom: 3px; + padding-left: 30px; + padding-right: 30px; + background-color: var(--birb-highlight); + box-shadow: + var(--birb-border-size) 0 var(--birb-highlight), + var(--birb-neg-border-size) 0 var(--birb-highlight), + 0 var(--birb-neg-border-size) var(--birb-highlight), + var(--birb-neg-border-size) var(--birb-border-size) var(--birb-border-color), + var(--birb-border-size) var(--birb-border-size) var(--birb-border-color); + color: var(--birb-border-color) !important; + font-size: 16px; +} + +.birb-window-title { + text-align: center; + flex-grow: 1; + user-select: none; + color: var(--birb-background-color); +} + +.birb-window-close { + position: absolute; + top: 1px; + right: 0; + color: var(--birb-background-color); + user-select: none; + cursor: pointer; + padding-left: 5px; + padding-right: 5px; +} + +.birb-window-close:hover { + transform: scale(1.1); +} + +.birb-window-content { + box-sizing: border-box; + background-color: var(--birb-background-color); + margin-top: var(--birb-border-size); + flex-grow: 1; + box-shadow: + var(--birb-border-size) 0 var(--birb-background-color), + var(--birb-neg-border-size) 0 var(--birb-background-color), + 0 var(--birb-border-size) var(--birb-background-color), + 0 var(--birb-neg-border-size) var(--birb-border-color), + 0 var(--birb-border-size) var(--birb-border-color); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: calc(var(--birb-double-border-size)); + padding-bottom: var(--birb-border-size); +} + +.birb-pico-8-content { + background: #111111; + box-shadow: none; + display: flex; + justify-content: center; + overflow: hidden; + border: none; +} + +.birb-pico-8-content iframe { + width: 300px; + margin-left: -15px; + margin-right: -30px; + margin-top: -10px; + margin-bottom: -23px; + border: none; + aspect-ratio: 1; +} + +.birb-music-player-content { + background: var(--birb-background-color); + box-shadow: + var(--birb-border-size) 0 var(--birb-background-color), + var(--birb-neg-border-size) 0 var(--birb-background-color), + 0 var(--birb-border-size) var(--birb-background-color), + 0 var(--birb-neg-border-size) var(--birb-border-color), + 0 var(--birb-border-size) var(--birb-border-color); + display: flex; + justify-content: center; + overflow: hidden; + padding: 10px; +} + +.birb-menu-item { + width: calc(100% - var(--birb-double-border-size)); + font-size: 14px; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 10px; + padding-right: 10px; + box-sizing: border-box; + opacity: 0.7 !important; + user-select: none; + display: flex; + justify-content: space-between; + cursor: pointer; + color: black !important; +} + +.birb-menu-item:hover { + opacity: 1 !important; + background: var(--birb-highlight) !important; + color: white !important; + box-shadow: + var(--birb-border-size) 0 var(--birb-highlight), + var(--birb-neg-border-size) 0 var(--birb-highlight), + 0 var(--birb-neg-border-size) var(--birb-highlight), + 0 var(--birb-border-size) var(--birb-highlight); +} + +.birb-menu-item-arrow { + display: inline-block; +} + +.birb-window-separator { + width: 100%; + height: var(--birb-border-size); + background-color: var(--birb-border-color); + box-sizing: border-box; + margin-top: var(--birb-double-border-size); + margin-bottom: var(--birb-double-border-size); + opacity: 0.4; +} + +#birb-field-guide { + width: 322px !important; +} + +.birb-grid-content { + display: grid; + grid-template-rows: repeat(3, auto); + grid-auto-flow: column; + gap: 10px; + padding-top: 8px; + padding-bottom: 8px; + padding-left: 10px; + padding-right: 10px; + box-sizing: border-box; + justify-content: center; + align-items: center; +} + +.birb-grid-item { + width: 64px; + height: 64px; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.birb-grid-item:hover { + border-color: var(--birb-highlight); +} + +.birb-grid-item canvas { + image-rendering: pixelated; + transform: scale(2) !important; + padding-bottom: var(--birb-border-size); +} + +.birb-grid-item, .birb-field-guide-description, .birb-message-content { + border: var(--birb-border-size) solid rgb(255, 207, 144); + box-shadow: 0 0 0 var(--birb-border-size) white; + background: rgba(255, 221, 177, 0.5); +} + +.birb-grid-item-locked { + cursor: auto; + filter: grayscale(100%) sepia(30%); +} + +.birb-grid-item-locked canvas { + filter: contrast(90%); +} + +.birb-grid-item-selected { + border: var(--birb-border-size) solid var(--birb-highlight); + background: var(--birb-mix-color); +} + +.birb-field-guide-description { + max-width: calc(100% - 20px); + margin-left: 10px; + margin-right: 10px; + margin-top: 5px; + padding: 8px; + padding-top: 4px; + padding-bottom: 4px; + margin-bottom: 10px; + font-size: 14px; + box-sizing: border-box; + color: rgb(124, 108, 75); +} + +#birb-feather { + cursor: pointer; +} + +.birb-message-content { + box-sizing: border-box; + margin: 2px; + width: 100%; + padding: 10px; + font-size: 14px; + color: rgb(124, 108, 75); +} + +.birb-sticky-note { + position: absolute; + box-sizing: border-box; +} + +.birb-sticky-note > .birb-window-content { + padding: 0; +} + +.birb-sticky-note-input { + width: 100%; + height: 100%; + padding: 10px !important; + resize: both !important; + min-width: 175px !important; + min-height: 135px !important; + box-sizing: border-box !important; + font-family: "Monocraft", monospace !important; + font-size: 14px !important; + color: black !important; + background-color: transparent !important; + border: none !important; +} + +.birb-sticky-note-input:focus { + outline: none; +}`; + 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.175 : 0.25; + const HOP_DISTANCE = 35; + + // Timing constants (in milliseconds) + const UPDATE_INTERVAL = 1000 / 60; // 60 FPS + const AFK_TIME = isDebug() ? 0 : 1000 * 5; + const PET_BOOST_DURATION = 1000 * 60 * 5; + const PET_MENU_COOLDOWN = 1000; + const URL_CHECK_INTERVAL = 500; + const HOP_DELAY = 500; + + // Random event chances per tick + const HOP_CHANCE = 1 / (60 * 2.5); // Every 2.5 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 featherLayers = { + feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), + }; + + const featherFrames = { + feather: new Frame([featherLayers.feather]), + }; + + 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()}`, () => birb.setVisible(false)), + new DebugMenuItem("Freeze/Unfreeze", () => { + frozen = !frozen; + }), + new DebugMenuItem("Reset Data", resetSaveData), + new DebugMenuItem("Unlock All", () => { + for (let type in SPECIES) { + unlockBird(type); + } + }), + new DebugMenuItem("Add Feather", () => { + activateFeather(); + }), + 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(); + const message = makeElement("birb-message-content"); + message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`)); + if (userSettings.birbMode) { + message.appendChild(document.createElement("br")); + message.appendChild(document.createElement("br")); + message.appendChild(document.createTextNode("Welcome back to 2012")); + } + insertModal(`${birdBirb()} Mode`, message); + }), + new Separator(), + new MenuItem("2025.11.2.60", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.2.60"); }, false), + ]; + + const styleElement = document.createElement("style"); + + /** @type {Birb} */ + let birb; + + const States = { + IDLE: "idle", + HOP: "hop", + FLYING: "flying", + }; + + let frozen = false; + let stateStart = Date.now(); + let currentState = States.IDLE; + 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 = []; + + async function load() { + /** @type {BirbSaveData|Object} */ + let saveData = await getContext().getSaveData(); + + debug("Loaded data: " + JSON.stringify(saveData)); + + if (!('settings' in saveData)) { + 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 + })); + } + + getContext().putSaveData(saveData); + } + + function resetSaveData() { + getContext().resetSaveData(); + 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() { + log("Sprite sheets loaded successfully, initializing bird..."); + + if (window !== window.top) { + // Skip installation if within an iframe + log("In iframe, skipping Birb script initialization"); + return; + } + + // 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; + } + `; + + try { + const fontStyle = document.createElement("style"); + fontStyle.textContent = fontFace; + document.head.appendChild(fontStyle); + } catch (e) { + error("Failed to load font: " + e); + } + + load().then(onLoad); + } + + function onLoad() { + styleElement.textContent = STYLESHEET; + document.head.appendChild(styleElement); + + 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(); + }); + + onClick(document, (e) => { + lastActionTimestamp = Date.now(); + if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { + removeMenu(); + } + }); + + 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); + }); + + birbElement.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 = []; + } + } + }); + + birbElement.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); + + focusOnElement(true); + } + + function update() { + ticks++; + + // Hide bird if the browser is fullscreen + if (document.fullscreenElement) { + birb.setVisible(false); + // Won't be restored on fullscreen exit + } + + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (Date.now() - stateStart > HOP_DELAY && Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== 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 (birb.isVisible() && Math.random() < FEATHER_CHANCE * petMod) { + lastPetTimestamp = 0; + activateFeather(); + } + updateFeather(); + } + + function draw() { + requestAnimationFrame(draw); + + if (!birb || !birb.isVisible()) { + return; + } + + updateFocusedElementBounds(); + + // Update the bird's position + if (currentState === States.IDLE) { + if (focusedElement && !isWithinHorizontalBounds()) { + flySomewhere(); + } + birdY = getFocusedY(); + } else if (currentState === States.FLYING) { + // Fly to target location (even if in the air) + if (updateParabolicPath(FLY_SPEED, 2)) { + setState(States.IDLE); + } + } + + const oldTargetY = targetY; + targetY = getFocusedY(); + // Adjust startY to account for scrolling + startY += targetY - oldTargetY; + if (targetY < 0 || targetY > getWindowHeight()) { + // Fly to another element or the ground if the focused element moves out of bounds + flySomewhere(); + } + + if (birb.draw(SPECIES[currentSpecies])) { + birb.setAnimation(Animations.STILL); + } + + // Clamp startY, birdY, targetY to a bit above the top of the window + const maxY = getWindowHeight() * 1.5; + startY = Math.min(startY, maxY); + birdY = Math.min(birdY, maxY); + targetY = Math.min(targetY, maxY); + + // Update HTML element position + birb.setX(birdX); + birb.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 {HTMLElement} contentElement + * @param {() => void} [onClose] + * @returns {HTMLElement} + */ + function createWindow(id, title, contentElement, onClose) { + const window = makeElement("birb-window", undefined, id); + + const header = makeElement("birb-window-header"); + const titleElement = makeElement("birb-window-title"); + titleElement.textContent = title; + const closeButton = makeElement("birb-window-close"); + closeButton.textContent = "x"; + + header.appendChild(titleElement); + header.appendChild(closeButton); + + const contentWrapper = makeElement("birb-window-content"); + contentWrapper.appendChild(contentElement); + + window.appendChild(header); + window.appendChild(contentWrapper); + + document.body.appendChild(window); + makeDraggable(header); + + makeClosable(() => { + 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); + const message = makeElement("birb-message-content"); + message.appendChild(document.createTextNode("You've found a ")); + const bold = document.createElement("b"); + bold.textContent = SPECIES[birdType].name; + message.appendChild(bold); + message.appendChild(document.createTextNode(" feather! Use the Field Guide to switch your bird's species.")); + insertModal("New Bird Unlocked!", message); + } + 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, getWindowHeight() - feather.offsetHeight)}px`; + if (y < getWindowHeight() - 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 = `${getWindowHeight() / 2 - element.offsetHeight / 2}px`; + } + + /** + * @param {string} title + * @param {HTMLElement} content + */ + function insertModal(title, content) { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + + const modal = createWindow("birb-modal", title, content); + + modal.style.width = "270px"; + centerElement(modal); + } + + /** + * @param {HTMLElement} menu + */ + function updateMenuLocation(menu) { + let x = birdX; + let y = birb.getElementTop() + birb.getElementHeight() / 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 > getWindowHeight() / 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; + } + + const contentContainer = document.createElement("div"); + const content = makeElement("birb-grid-content"); + const description = makeElement("birb-field-guide-description"); + contentContainer.appendChild(content); + contentContainer.appendChild(description); + + const fieldGuide = createWindow( + FIELD_GUIDE_ID, + "Field Guide", + contentContainer + ); + + const generateDescription = (/** @type {string} */ speciesId) => { + const type = SPECIES[speciesId]; + const unlocked = unlockedSpecies.includes(speciesId); + + const boldName = document.createElement("b"); + boldName.textContent = type.name; + + const spacer = document.createElement("div"); + spacer.style.height = "0.3em"; + + const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description); + + const fragment = document.createDocumentFragment(); + fragment.appendChild(boldName); + fragment.appendChild(spacer); + fragment.appendChild(descText); + + return fragment; + }; + + description.appendChild(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; + } + birb.getFrames().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", () => { + description.textContent = ""; + description.appendChild(generateDescription(id)); + }); + speciesElement.addEventListener("mouseout", () => { + description.textContent = ""; + description.appendChild(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(); + } + + /** + * 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, getWindowHeight()) / 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 { + birb.setDirection(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 getWindowHeight() - focusedBounds.top; + } + + /** + * Fly to either an element or the ground + */ + function flySomewhere() { + // On mobile, always prefer to focus on an element + // If not mobile, 50% chance to focus on ground + // if ((!isMobile() && coinFlip()) || !focusOnElement()) { + // focusOnGround(); + // } + if (!focusOnElement()) { + focusOnGround(); + } + } + + function focusOnGround() { + focusedElement = null; + updateFocusedElementBounds(); + flyTo(Math.random() * window.innerWidth, 0); + } + + /** + * Focus on an element within the viewport + * @param {boolean} [teleport] Whether to teleport to the element instead of flying + * @returns Whether an element to focus on was found + */ + function focusOnElement(teleport = false) { + if (frozen) { + return false; + } + const elements = document.querySelectorAll("img, video, .birb-sticky-note"); + 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 <= getWindowHeight(); + }); + const visible = Array.from(inWindow).filter((img) => { + const style = window.getComputedStyle(img); + if (style.display === "none" || style.visibility === "hidden" || (style.opacity && parseFloat(style.opacity) < 0.25)) { + return false; + } + return true; + }); + /** @type {HTMLElement[]} */ + // @ts-expect-error + const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); + // Ensure the bird doesn't land on fixed or sticky elements + const nonFixedElements = largeElements.filter((el) => { + const style = window.getComputedStyle(el); + return style.position !== "fixed" && style.position !== "sticky"; + }); + if (nonFixedElements.length === 0) { + return false; + } + const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; + focusedElement = randomElement; + log("Focusing on element: ", focusedElement); + updateFocusedElementBounds(); + if (teleport) { + teleportTo(getFocusedElementRandomX(), getFocusedY()); + } else { + flyTo(getFocusedElementRandomX(), getFocusedY()); + } + return randomElement !== null; + } + + /** + * @param {number} x + * @param {number} y + */ + function teleportTo(x, y) { + birdX = x; + birdY = y; + setState(States.IDLE); + } + + function updateFocusedElementBounds() { + if (focusedElement === null) { + // Update ground location to bottom of window + focusedBounds = { left: 0, right: window.innerWidth, top: getWindowHeight() }; + return; + } + let { left, right, top } = focusedElement.getBoundingClientRect(); + if (focusedElement.classList.contains("birb-sticky-note")) { + top -= 4.5 * UI_CSS_SCALE; + if (focusedBounds.left !== left) { + // Sticky note has moved + const oldWidth = focusedBounds.right - focusedBounds.left; + const newWidth = right - left; + if (oldWidth === newWidth) { + // Move bird along with note + if (currentState === States.IDLE) { + birdX += left - focusedBounds.left; + } else if (currentState === States.HOP) { + startX += left - focusedBounds.left; + startY += top - focusedBounds.top; + targetX += left - focusedBounds.left; + targetY += top - focusedBounds.top; + } + } + } + } + focusedBounds = { left, right, top }; + } + + function hop() { + if (frozen) { + return; + } + if (currentState === States.IDLE) { + setState(States.HOP); + birb.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 && birb.getCurrentAnimation() !== Animations.HEART) { + birb.setAnimation(Animations.HEART); + lastPetTimestamp = Date.now(); + } + } + + /** + * @param {number} x + * @param {number} y + */ + function flyTo(x, y) { + targetX = x; + targetY = y; + setState(States.FLYING); + birb.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 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) { + birb.setAnimation(Animations.BOB); + } + birb.setAbsolutePositioned(isAbsolute()); + birb.setY(birdY); + } + + // 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); + }); + +})(); diff --git a/dist/extension/images/icons/transparent/1024x1024x1.png b/dist/extension/images/icons/transparent/1024x1024x1.png new file mode 100644 index 0000000000000000000000000000000000000000..e915b5cd219eb2c53a49ed03797670972db7e307 GIT binary patch literal 6957 zcmeHK`BPI@7`*{x6$EK55SG*#pelk0R;Yvog917NRhFPa!Ki49jS|77Do^&7AgE&z zMS>OFf?xm#NFc-|33Z8EqoImC&=`pXaEWX&^xZh^=yd!C2=9mQzIk`vdvnh{-#O?0 z8W|p9Y3^VS0I&=T4PFU=5q4_?OpUQ2!)ZoKO+i$d29$M$wHrqJCCU|O+ zk@b<%IM(4x-vIGL;+{RN%LhFM7#RnGE+x(vxKF=b$4#=KTSw4Wr%A@H*~I)F-NWed z&hiY^w5JK>3ni|3Q_AhV>q>X^m~yDfq^i8Qjyw5h@}HhRy^3GVh?BPn{yWMgQk;eE5 z0=d;ZO;m|x$3cy6&N`c&q*L_khLlF~)?2Cz^+oqh2+QNE=tveNu_3tQrSpLE3ze$N z=Y)122scPQ%6&t00Kk5ISa49(&N9{Cr1J?&Kwh^`s)Ysp>wE&$*rP9{4tGK5hBrNz z{l#L3uZm}8<|KYuXUutEDU-z+dAMd8d8|uX_=iQP=WtGvWGrAW8p3DXW|jC`VcR`< zO(HOffSw_sEqc|aT>!v%DMKQ~BMU@!e75Wgo^1kgsAcKUz!|{7$P`v<0<2^NRxt4= zXlekE5*;>@jZ&{ni!3RB{76x%TqS=kTIv-Y4#6TLu-;G1R&B_O$A9;E}4hTQ9Xm zqY^YDW1NDQja_SqIP^$v(UAhiqP8+M43?F1G(ed)!Fx()mpXE3=FR9;*b5Q zG&}QV#D!E{zI7RIEjuD+8iG&Nci2RngU}VbQ$#Z5blH`VAyy3rPveue1R{?5>|s}e z*XW8!wMWu&)A?3q4V=@1y>DVa0^jCBN5DQmZ*IpU8pBs5$6icXPR zN$OcF7Wt#|A6ncwbAXAiy-O@|B_cx4c zx>e9MwWD$&r=T&g1#8%F;i9zTw(wOwJNWz`IsugkP`7`*lNBO(lh+s<0sIPg@HM@% z9Pzlk@I(S=^3(T|5gD=t!J-)ulzzzkz9Q!$wqhC`(<_hHk`j^=Z}2R)g>xh8w*Y+y zE3w)c2pkxa=G|92n4H@*HLupikvH5|P#Y8B=hQYXZ?57y)6U@MqzERB5#tp>ckX^* zM`O}#A!>#%_YA4CXeO+^ucVmVH{2L#yE|9i^@~zzCYU*y_+cyJFtG=MxSRe(@l0G2lwWkdmBj4z|9FZYmkf$0gE{f5Niy#;>;e)Ux;lVYF H6Ept-PDebB literal 0 HcmV?d00001 diff --git a/dist/extension/images/icons/transparent/1024x768x1.png b/dist/extension/images/icons/transparent/1024x768x1.png new file mode 100644 index 0000000000000000000000000000000000000000..c2dad996227e2ca8af23127af8da5eef7b0f3a13 GIT binary patch literal 5578 zcmeH~{a4ag9LFz6iWH@t%~h7NJk66Ujp`(BBQdOLor%jrZDEuxYf+eFDv^+-Qj%1z zWsAg)s3l7w?4OP&Iy-0U9}vGkeD3|8`_p~h_xp3- z_e5mGmuS>H6aWAn9u~S00EB(&1dz`5K`#Fg4FK%;9wIR^oJhn(Cee58p?wQLs^G{O zd{_fvzH(bs@kYM|eVAfmmSJberpB+uk;hS&$-7%`cRvi+{qU@)3WqA6<`V27p6Rca ze^!Oc(7JGIw4xUa7g+-qS8@lnRo=z{{6Gh6jrh(=8GZhPgLhS;vpf86`8mhAhtAlJ za65M)R+uMR`9#o4iK54fe=uCkJ-`qPZqo?2zBcvXzPz>;}6 zqyGB6FYA@A^Qz+0V)(k;X}=YQ9I|NHrkZbL zxFFppnP&;MoPBH_I7%seR@rZiZ{IHj)&%W(ejWfHz`{dAHl+&spJ2=4)`BeEilph&=^t}3q8TEI>=)-skBc;nX!b_r zs#wBFq({bP)x5?nfzD z?J0HvKyX{YDDMD2VVg`fCzsaBm=F%rh$}R>VW3LjKo!8?P(>i2I{6otk!RT(-lZV@ z);b-n$>gf?vW#x)Fu}tZP7nY%)GqPN+)jog%V(%tJ!2Ah2l_g# zQGCC{njYhKrH^e939XtQn1<^Kv6%cS!9d%5zdsZ)cmO(fs)M5GxQkd8eCw0(Qm?2h z%n_E_wP|<3DfrRCHfjCg2p3|jK*7*jINgU6#5i0+dc#QNaZQM9S zztb*#6x$;+3_XRoJB;K0+iN&dref~>AiV(%=*zQwv=0aHX;LP9I^~w_SA(l!VT)HE zqvXxiq_p@VzF!~&%fXt|{~!~``!Lki%!W1|mP`U3)vvmh2G>Nt3afQkqco3sUn>Bn zbq)M;IS$|9-@>cfg4gnz<}~}SPJ7KVAAeMRq?c(%fYfWoU<=LuKRQ$)B$IH(l!Dd- zv5Nv-J#qgzR!YD?)*^7A3SeCt6%?fsSH;uJNSZD$yCvF=r~PJmv|VnaO0;LfE4+eS6Yh~TSP-k*M?hn`*z6R$APX8!^A?_imjOxwxB=A!Qf>7^%5 zmF$ntYd#n}OIQ&HmJIc#q~tEJjCGZYeSu=H!|UxWHeRcfw&b~hVRrs~ZbL7sEN*pu z4fE$eYhMkT?d{FSm~I`GItj1Rn@zCP1N+~H{k`WgxfY<9kr)-vliII=358Kg243dZ zvsfu+1oaF%{cv($XC1#Hn#743{OZ~BDNEY*;s!m1s!sg<64L0AsiUt446dMb@OII* zTnf$_K<|4AQz)nW2$>6E_ajihjr>K}5nTC%AK?;}3qL*RknW1SR(dyNZGoewen?Z) zQ|~ktGp=i|;8R$Gt$|`+L9WeqcX<7g&zm9w$Qq8LqnhZziUa93n6F+)%5UV|fOHnV LHX^iq4K?F0vXJoJ literal 0 HcmV?d00001 diff --git a/dist/extension/images/icons/transparent/128x128x1.png b/dist/extension/images/icons/transparent/128x128x1.png new file mode 100644 index 0000000000000000000000000000000000000000..0fd3b1de398eb3ceace7213ad4f12982738aa527 GIT binary patch literal 1152 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU~I{Bb`J1#c2)=|%1_J8No8QD zm{U8^*5j~)%+dJZr9oPD@Kd0KxI#1GY)6B|892+BEB`$4`ym_HxdB@{b%Yye! zD2{x(=V;_=vxw*O{U>@p$@) zr4v>&x%IJLS>Raq=Ry64-O-yZEFY@1PICC)om=0-&=|Ym?v}UjlW*tEXZV-?!0Y|5 zY~6%slfR^ecgr(A%zf}^*J{n`15f!3{)9~Dy*crZNo$f|h5h3EQ_RlW9SY9AUAwve zGP{Cz?9%*(X6J-0E;nDxy_b9O_U(P+x&P`I{FsVn?D}jg#lXO{-P6S}q+-t7yBoWN z9c7MxjPy)Qz*6I47WVME^Lz5kf6rR=}|-hJOc6W`7C z@zbBxIj!wxY%pdhU}HEZ6cPDN>dU6j*WZWn#4+kczq)yETqP$ga@awls_^_>MDrdm|moV7D6V}%;aa3J@^ssVebwibc z``1?btIAK-h8us5;ec=UE|XPdQ=)JG*dD&VeWDC&b{Wt?3@@W(NXCMhA-u%SVNLX%%3r3{|7V+Td$_2w{B7O0;dI z>@TQd^RF_j&*cnz?4o{rs};Xn_wC!YgWKxm-?hf++5afnp!+S*9 zq`%Q~u1230g#;bYi0!MQk3SPzcFX&+pYqXlOJtUPTfg^gHa8+%c3ho)K&UhRDv$Da z`LhUpK&!**?HoF92^MnO{{Jrjm(r$-Yro>fH7!#WCba;Oc-Du7zP4R}Vh9q@QtV(vlq(T7CnUwS%ka0ei>p2TNWkss7W~+|u#!!4CQN?`+@QV{}@5%4BoC%!QKECzeb~ z(%Y}a7ANS!lgJ`4b!O!J$xBRZj$gN}soCrOLFavOeV^mj1fv7$LgzQ_N)*f&@XwDb zJ@@;FVA!7JvJtIZ;w&->H5uxa0>Go`Z(mu%=z^$=LNppk3Uh6xKo)sMQgeVZ)zZ^h&3 zCzeiF&E(d{dS!uQ*`EjXA9hD?wy=Dt);h`Ie|K(u4?|<@g1cMZzE8fLH=p5O`U9`` zzp`}`o=yIe7Tzt-_%Qduqg|^ts}DToGx!rSo%iO%KPIh7f))0Q^G`85Z+9p-`*!W- z{>$tN-my#b8=9RHwz%AUE%#pT#oM>{jpzQWWAI}tnz8G%trP@+^GCI3-j`7)Q_Z>gKn4EQ8?8hLWh8sN# zd+=lJp6~KD@qK0+?;5%8yJ>Ly?(cNdN7qkrG8|&Tf&AQK4xLRovUE=M`rqsBS7dAM zF4WX8V8-Dgphq{XR{mH2Yvvo<+p-V8^~Z|qUQf+ zb@Bgy%J1#l_oLeASb6@v-9M`9zTIA&dOm$`ynRgHvvcnK;rq)@&!6J$*w`(^z#)tS-7e95aZB^jw38niN{0cR%J+em_+waMAKYk7?dhl51)3{w ze%SY}?(5}O*ZX$7xhx`aqZfx=poFXWL1pqo@oVecUQd4bYfm}1I*!l)hSKb`jU^$~ z@g~0xo5!!KdwV?IrskyjuX_9X3j)P6GaqiBCd|;ni5tbf{mW1q{cP!CJ?Xt)a>|dS zRGfaEANgk9_rKHLox`6bd>Z*PPVXr_;=0|qwr(ySYh>U3pS$`0(}nJPr+n?e8(0Z1 ae@V;KZfUGKnqUbk7d&14T-G@yGywohIB}~0 literal 0 HcmV?d00001 diff --git a/dist/extension/images/icons/transparent/16x16x1.png b/dist/extension/images/icons/transparent/16x16x1.png new file mode 100644 index 0000000000000000000000000000000000000000..479ed9af97f7528dc973e5226821115167e8b1d9 GIT binary patch literal 635 zcmV->0)+jEP)4Tx0C=2zkv&MmKpe$iQ>9WW9qb^bLx$>PK~%(1t5Adrp;l;yWphgA|?JWDYS_3;J6>}?mh0_0Ya^GTzbXb^5kw4O1Q3;(S)Y@nG(5-GJ$!t?RNS*tc!=brq9p@P1$%ypVEq_Bu3ND!f*feNaykf2p3#YBeolOF!4PDEj~Z|9=!sjEr!7 zEW~I8sZXB$ADcKM6G11UX@-fRxZppU6Jd(NyPq?Jbv?st0TZ$YWQ?rw^ZVyG4PZg$ z80Fmr(<(B&D2i5VU$rEnSskk#Q#2iR=DW+`50;7A`RdH|H{ VR2dYl*>(T`002ovPDHLkV1j)fC{F+Y literal 0 HcmV?d00001 diff --git a/dist/extension/images/icons/transparent/16x16x2.png b/dist/extension/images/icons/transparent/16x16x2.png new file mode 100644 index 0000000000000000000000000000000000000000..ab5323713d1bded8a37b8fe6bb5a35b8fdd2c643 GIT binary patch literal 829 zcmV-D1H$}?P)4Tx0C=2zkv&MmKpe$iQ>9WW9qb^bLx$>PK~%(1t5Adrp;l;yWphgA|?JWDYS_3;J6>}?mh0_0Ya^GTzbXb^5kw4O1Q3;(S)Y@nG(5-GJ$!t?RNS*tc!=brq9p@P1$%ypVEq_Bu3ND!f*feNaykf2p3#YBeolOF!4NnRBECy*@ei_0un~{EjIAw>SwH>fX!?$pBPu7CsIO=Kn5aJ zC-GLtj6|D+(r$-Yro>fH7!#WCba;Oc-Du7zP4R}Vh9q@QtV(vlq(T7CnUwS%ka0ei>p2TNWkss7W~+|u#!!4CQN?`+@QV{}@5%4BoC%!QKECzeb~ z(%Y}a7ANS!lgJ`4b!O!J$xBRZj$gN}soCrOLFavOeV^mj1fv7$LgzQ_N)*f&@XwDb zJ@@;FVA!7JvJtIZ;w&->H5uxa0>Go`Z(mu%=z^$=LNppk3Uh6xKo)sMQgeVZ)zZ^h&3 zCzeiF&E(d{dS!uQ*`EjXA9hD?wy=Dt);h`Ie|K(u4?|<@g1cMZzE8fLH=p5O`U9`` zzp`}`o=yIe7Tzt-_%Qduqg|^ts}DToGx!rSo%iO%KPIh7f))0Q^G`85Z+9p-`*!W- z{>$tN-my#b8=9RHwz%AUE%#pT#oM>{jpzQWWAI}tnz8G%trP@+^GCI3-j`7)Q_Z>gKn4EQ8?8hLWh8sN# zd+=lJp6~KD@qK0+?;5%8yJ>Ly?(cNdN7qkrG8|&Tf&AQK4xLRovUE=M`rqsBS7dAM zF4WX8V8-Dgphq{XR{mH2Yvvo<+p-V8^~Z|qUQf+ zb@Bgy%J1#l_oLeASb6@v-9M`9zTIA&dOm$`ynRgHvvcnK;rq)@&!6J$*w`(^z#)tS-7e95aZB^jw38niN{0cR%J+em_+waMAKYk7?dhl51)3{w ze%SY}?(5}O*ZX$7xhx`aqZfx=poFXWL1pqo@oVecUQd4bYfm}1I*!l)hSKb`jU^$~ z@g~0xo5!!KdwV?IrskyjuX_9X3j)P6GaqiBCd|;ni5tbf{mW1q{cP!CJ?Xt)a>|dS zRGfaEANgk9_rKHLox`6bd>Z*PPVXr_;=0|qwr(ySYh>U3pS$`0(}nJPr+n?e8(0Z1 ae@V;KZfUGKnqUbk7d&14T-G@yGywohIB}~0 literal 0 HcmV?d00001 diff --git a/dist/extension/images/icons/transparent/256x256x2.png b/dist/extension/images/icons/transparent/256x256x2.png new file mode 100644 index 0000000000000000000000000000000000000000..aa5c5da413c095bac09df4ea271d50aecedf87de GIT binary patch literal 2956 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 literal 0 HcmV?d00001 diff --git a/dist/extension/images/icons/transparent/32x24x2.png b/dist/extension/images/icons/transparent/32x24x2.png new file mode 100644 index 0000000000000000000000000000000000000000..b33086fc42811bd9244525ea9d989af1066c6ca1 GIT binary patch literal 881 zcmV-%1CIQOP)4Tx0C=2zkv&MmKpe$iQ>9WW9qb^bLx$>PK~%(1t5Adrp;l;yWphgA|?JWDYS_3;J6>}?mh0_0Ya^GTzbXb^5kw4O1Q3;(S)Y@nG(5-GJ$!t?RNS*tc!=brq9p@P1$%ypVEq_Bu3ND!f*feNaykf2p3#YBeolOF!4s~>f|4rQP{!la4Tx0C=2zkv&MmKpe$iQ>9WW9qb^bLx$>PK~%(1t5Adrp;l;yWphgA|?JWDYS_3;J6>}?mh0_0Ya^GTzbXb^5kw4O1Q3;(S)Y@nG(5-GJ$!t?RNS*tc!=brq9p@P1$%ypVEq_Bu3ND!f*feNaykf2p3#YBeolOF!456lB377IqQKydq&G#AaHTML| z_5?wwhvRDFhl^LnfNc>C9$U~xYna|9qp0J(n`WJ}n_!BgYwS62o_&Mud> z8~5+O-?ln_9C$s905Jj(BLFcMdmW(e5zjAWZTsP3t+xGjvlaFI=63FC+xFvQ9gsN! z5F>!x)&aI=*`Dx=@4K7d(bFSBrLOt0F#^O0K#Tyyoceb?u}w}k8~4wz50cLNE!F{< zBLFc1nD*a>`f8kS9pTL3z8W6t^RbND6D*z+o-za=MgV<#My&1qhDqX{;InvqU)$!t zB07cu#0a48&k0wV^@P+l-(m!a5r7y0^t}g!_Jr&6quTcF;aSt+-q)wD`4%HUi~z(4 bpwRjTM^>80PFp^i00000NkvXXu0mjfk_g1J literal 0 HcmV?d00001 diff --git a/dist/extension/images/icons/transparent/32x32x1.png b/dist/extension/images/icons/transparent/32x32x1.png new file mode 100644 index 0000000000000000000000000000000000000000..ab5323713d1bded8a37b8fe6bb5a35b8fdd2c643 GIT binary patch literal 829 zcmV-D1H$}?P)4Tx0C=2zkv&MmKpe$iQ>9WW9qb^bLx$>PK~%(1t5Adrp;l;yWphgA|?JWDYS_3;J6>}?mh0_0Ya^GTzbXb^5kw4O1Q3;(S)Y@nG(5-GJ$!t?RNS*tc!=brq9p@P1$%ypVEq_Bu3ND!f*feNaykf2p3#YBeolOF!4NnRBECy*@ei_0un~{EjIAw>SwH>fX!?$pBPu7CsIO=Kn5aJ zC-GLtj6|D+4Tx0C=2zkv&MmKpe$iQ>9WW9qb^bLx$>PK~%(1t5Adrp;l;yWphgA|?JWDYS_3;J6>}?mh0_0Ya^GTzbXb^5kw4O1Q3;(S)Y@nG(5-GJ$!t?RNS*tc!=brq9p@P1$%ypVEq_Bu3ND!f*feNaykf2p3#YBeolOF!4^n&N=6-A@oq5=XuKZL{$5@7{)P-BlJAFu13r9 zkh};g7{+DtO%Wn86)L8ZdT$k}QCAWA%0000< KMNUMnLSTYpJ;S&F literal 0 HcmV?d00001 diff --git a/dist/extension/images/icons/transparent/48x48x1.png b/dist/extension/images/icons/transparent/48x48x1.png new file mode 100644 index 0000000000000000000000000000000000000000..e007b96f43546dc408351959ccb0d742afa60f28 GIT binary patch literal 856 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 H6Ept-PDebB literal 0 HcmV?d00001 diff --git a/dist/extension/images/icons/transparent/60x45x2.png b/dist/extension/images/icons/transparent/60x45x2.png new file mode 100644 index 0000000000000000000000000000000000000000..1f035ac204d0d9a5b4bf917b8de144c8d33033e7 GIT binary patch literal 1014 zcmeAS@N?(olHy`uVBq!ia0vp^6+j%t!3HFY8794FU|?*?baoE#baqwD@Kd0KxI#1GY)6B|892+BEB`$4`ym_HxdB@{b%Yye! zD2{x(=V;_=vxw*O{U>@p$@) zr4v>&x%IJLS>Raq=Ry64-O-yZEFY@1PICC)om=0-&=|Ym?v}UjlW*tEXZV-?!0Y|5 zY~6%slfR^ecgr(A%zf}^*J{n`15f!3{)9~Dy*crZNo$f|h5h3EQ_RlW9SY9AUAwve zGP{Cz?9%*(X6J-0E;nDxy_b9O_U(P+x&P`I{FsVn?D}jg#lXO%>*?YcQZeW4-Hl#{ z10;@qlz#ajfqfxI)8YpYo;~7zp`%yZ_(;WqL(i_EnU(#Z=P#Z|YxEynI_p>VeqCY`cf6cfQ;^=TCjI$K(?aCmp^%qYW6{P_STW*A?*zt2gPK7J07QrIe+{ZOGsD zM__{mlbHK~M;eg_<5wtspMU?eS=^q8gO4~OWEi`Z6AI@ltXX5&yQ%8z1FLrhr>)vc zPhPwKP&aN(LLrMzU&B$s4Hh#!!`@_GQ7BsYNlNxd*Yu-WNRm7JZZ8Y``}z8X63tCq zv$n>+>73pw3f0FX7XIX)LdD$7ko|58U+JxH6Pb~cs0J~$Kw3R=r5wxmF5BKsK6dvv zX~v1oJ9$V1>>?JObAL-0miphfsw%22t6eKC60`Qru5Z>g`+tA`=%%O_2K5m~M2um- zXV|;cH$PK@Zkp}8vm)pBzPp)!qQt(x)4sjN0%%(o*aJ*?k5x|ZYK%Ub-Im^36aH8y zd1J4z=&H&DR!{(d9VEK*zxe~3nmM_Wn zsF+hb(bnUzgUr$R;H5!YvTqcYI=8(_aSS;UxH_PPYhhT%)q_ti>1SM;v}A_`*E&{q zd$HAgdgpdo?ci#9z}~U@!IBqBs{iygw{(1butWa+JKJ~n7@bz1GTEFjbD`w)i6xVg z^!979#R3hy60%*YO{#v^Zv9hY}&W!OzAAcB^x?aJp>jmXyjV8VZwq?^`kCr-zLlbTk&}M ziKP=(Gr9G#URmH+_UA$UhuzVeEi50ZwN7&Q-<@0E!_XMJ;O>^U?~`xm&1d+R{=n<~ zuWa3fXOq9Ag?Gy{KFodaXxD1Z>H|;t4E}^n=e;@ck4bBiV1@nS{8P-%+Z_tdzFoVy z|1!IRckI&qhGyr4EiN}-%e|L-@%HU~BKa@0-uyKrFIjA9#BbM*5Ip-sr*yIEOow$aBtj!6Nig}Z+NpHBp(-bJ;Vj$C; zs}uc~?b+>zvwxodeC6;N%Qydi96Ej4=Q<-WzLCM4?te9Thv&2hSTm(%5S$sttg@H`NZQK%PO=nFgHNAXf=dmikj71!?G@8`97IZ2>nWv6kvsXAe zM|)nf^6kmyx!b&dA9^qKyLigKI8);+b%aR}Ltih}j`)~kI&JmF`Wxnvwxv!WOOWgX zGR?NzSzVv?{KlN02fN?j6?%S9y;bu2jje0L{JrO(tNpch@0ql9qUSA-7wy)*zH95v zX!+JE6z6pJ>+G!Q7U^+Xm^NhV zPmEs`fBwDCzHgt_-hCA2(mwNEk?Ao`XavDr4-CeYRlhQNXGJDww%MBf*Qw1ttFvRN z{e1rJ*@5SO{?{)=59-M0>rxjlU%IYzjrrA!oBNNRk6zm`&0--uV&QRISo|Wa_|?{P zEBd)MKT|z7^Eig0oV=XtSzo`rJ-z(s`TqHH{Qkarw71{=-R@%-V>*RV)5|jNPyGLa VXY2Rx*L7=A<$( zRLrTJXzOv>cJq*)c5pR4VDH%dV95(5)qnb$TRJ{I*dhP^o$b4Oj83ahnQYFNxlnTY#F9x# zdi%B5;sjlI5?KVM&WxNtd5MY5@$0rVHG923=)5nk?{nOmV01uT==`Q#iGuk8{`pa* z=YAg%44W4EsQ8g#v!2JF6QxI=nx9i`XPqZ$xM^nPBaV#`uM(HGN8Y^9vApB)s%63Z zCKN|L-E%Z@wOPdTd4F0LHtpMVrgWC!k_{cI9s&y&G;*!lFkwNc`cap*ZUD@6N67VQ7q9aCghw_sO^O<}>_Df8h20 zSGI1#v&mo5!n@@eALc%Iv}?6y^?|2+27f}P^WL2J$D}n$u)=%)< zf0DIg@!WrP41P>SGj@Hpm11CEit==E45^s&_U^{6 zD*+;G52QD4DBwyIWbr)X6NFlC)ytfqH@ieEU~0Y1{5`VZ^QYDV>L z&c3#PpZ>0B&luHnIdJ(i`vaDTUAb5DrSgm6y4cvMwcmbyntSoL-Jg$j-wOM^q~Xp~ zkgDoGQob?Z@2Aq*q}v}?zb?FQ|2}W}JiR-A3zDMWt~M4*Hv>n2yjA( z+W+SYUv0Jz7B4=Vx27c>1;rP{oeA~3CWSrzyj_3(k+FgZf>Flf!P?VpRnUl)E zP%)==qOHea2brVs!Apa*WZx((b#8l=;uvxyaCJZn*TS%js|TN4($BawX~_-?u63;J z_F}8~^v>b2WwJS6=0eHo6H6u~ z>Fw8IixYI=Nn{b2Ix}+q>Cp!2@CzRz)Mg3$qWq4S$|B?{&X_~%EJ zp8I`7Fl<`rqvA(~&3YbxPLv*fYJN_&opqk1;ij3Dk2p3)yh>c!9(nUZ$MTNHtCj`t zn@}A2bkEVq)n*aT=lyA2*tBocnbKK?OEz?PKDLzD<_ACuN3!3z7u`KOqjw>uP^eY6wmU}Pv;_ch}#&iGGG59eR&DizXR*HdvX|kt_V@SoEw|6&o z2?t8FJ(S$I!Ju^mN7K@T#Pr9!8M0C-K_V_2 zI*yioWGiAy|B-f1<+IJxtVz6;mOswVpRtY^7~v3*5cBuM_x%yK-vRA6;{Erg zAFE@S<{IF_*`?tqsH(BZr;>Ns&blXw*(Vxi&SIF0kYI7W^7PGJ*?m{|*4;NYS$n^K z_cLwVZ~K;qRo&ZqdA=}IJB#a-1szdu^wyWnS{+-u)_vZSHjC<@I=-!kb2EPxJArHf zD-jY2Y*KoqG41e4{?KW9{5ExeH)oq(ThBdR;ae=*jHHEox=~Dg)&Fq+N=?a6H;-O1 z*}ki6@oeeSiF|zvI>5$w1^8?}E&W?}Q}B$W|6!Yc{(ikc?SPQ{mnE8DAA#)4_}zE* zlKrpucE7{^KG>eOZkK9R`CrGY_v?TCPJZ(5{H~9d3t?sfLt@3gr$7%F&3PR1;da)5z}kXxiZ|%J6_`|9(|w>tv_5 z`!4Tx0C=2zkv&MmKpe$iQ>9WW9qb^bLx$>PK~%(1t5Adrp;l;yWphgA|?JWDYS_3;J6>}?mh0_0Ya^GTzbXb^5kw4O1Q3;(S)Y@nG(5-GJ$!t?RNS*tc!=brq9p@P1$%ypVEq_Bu3ND!f*feNaykf2p3#YBeolOF!4u7Cs?*8FapE~K#TzTz7D7Y=k>$W*Qhnz_6+-a=sLk-1c(uU7y*bm^)=PD^ZK?m zf3UDzq{KR4$`ODV0pzw0NPSJ!XBarIx5|&Vf9(kt*8?&~0Ad7?`?(-n!Zr?`8sERV zGAMF(xwPH*`u_WEtIv-E_lFT6MgU?2Am(DP1Jpg@`K7FFKU}QUw!dz+qW<38&RuQW z9zNCqnIix(0?2J0U~87`3BNes-TaH59uX>a&5w-{AVvUU1R&!T_khrzaD9GM+ul7qYx=nN{i$od z#Rw2105JmS`?Dh2-k0PP+7sG7>md{)K#Tyy2tX{m9^m7k{%;QfEWRY5kR5!4awEBk%FO&4gdfE07*qoM6N<$f>vAICjbBd literal 0 HcmV?d00001 diff --git a/dist/extension/images/icons/transparent/icon-transparent.png b/dist/extension/images/icons/transparent/icon-transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..bb3dc2fc52df4694e821aa81b14ac4de16da42fc GIT binary patch literal 10462 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%0A$pc7akxp`|@9Q z#8_2LMdEc*#_JTkP341TxE-&ks)DS6O6T_7GBpBSI!R>?x@=%1kd83CZ-LC={Rw^# zN6Y(mVi=te$um?A623`hn_oo_>lK7K`2u3ph;Zz> zmk5-7iEebt{a`lRtsU=}8SX^^I9xT=YjeZS*H23a-~+at(~N^^XC&|e*^RS819zRT zIfwbl4L}8%%f0u5JSW6VB^$$wV0fw" + ], + "js": [ + "birb.js" + ] + } + ], + "permissions": [ + "storage", + "activeTab" + ], + "web_accessible_resources": [ + { + "resources": [ + "images/*" + ], + "matches": [ + "" + ] + } + ], + "browser_specific_settings": { + "gecko": { + "id": "birb@idreesinc.com" + } + } +} \ No newline at end of file diff --git a/dist/birb.user.js b/dist/userscript/birb.user.js similarity index 99% rename from dist/birb.user.js rename to dist/userscript/birb.user.js index 33ae7fb..8b7805a 100644 --- a/dist/birb.user.js +++ b/dist/userscript/birb.user.js @@ -1,11 +1,11 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.11.2.44 +// @version 2025.11.2.60 // @description birb // @author Idrees -// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js -// @updateURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js +// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js +// @updateURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js // @match *://*/* // @grant GM_setValue // @grant GM_getValue @@ -1881,7 +1881,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.2.44", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.2.44"); }, false), + new MenuItem("2025.11.2.60", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.2.60"); }, false), ]; const styleElement = document.createElement("style"); diff --git a/manifest.json b/manifest.json index cd121bb..a2bc31f 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.11.2.44", + "version": "2025.11.2.60", "homepage_url": "https://idreesinc.com", "icons": { "48": "images/icons/transparent/48x48x1.png", @@ -15,7 +15,7 @@ "" ], "js": [ - "./dist/birb.js" + "birb.js" ] } ],