From 2e3ff2870642c318935b7bcfaeabef70f3c377f9 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sat, 15 Nov 2025 21:08:28 -0500 Subject: [PATCH] Failed vscode wip --- .vscode/launch.json | 12 + build.js | 20 + dist/birb.js | 2 +- dist/extension.zip | Bin 149911 -> 149920 bytes dist/extension/birb.js | 2 +- dist/obsidian/main.js | 2 +- dist/userscript/birb.user.js | 2 +- dist/vscode/extension.js | 2834 +++++++++++++++++++++++++ dist/vscode/package.json | 21 + platform-specific/vscode/extension.js | 16 + platform-specific/vscode/inject.sh | 5 + platform-specific/vscode/package.json | 21 + platform-specific/vscode/patch.js | 1 + src/shared.js | 2 +- 14 files changed, 2935 insertions(+), 5 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 dist/vscode/extension.js create mode 100644 dist/vscode/package.json create mode 100644 platform-specific/vscode/extension.js create mode 100644 platform-specific/vscode/inject.sh create mode 100644 platform-specific/vscode/package.json create mode 100644 platform-specific/vscode/patch.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..af31f5f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}/dist/vscode"] + } + ] +} \ No newline at end of file diff --git a/build.js b/build.js index 5ada142..d29a1a8 100644 --- a/build.js +++ b/build.js @@ -15,11 +15,14 @@ const DIST_DIR = "./dist"; const BROWSER_MANIFEST = "./platform-specific/extension/manifest.json"; const OBSIDIAN_MANIFEST = "./platform-specific/obsidian/manifest.json"; const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt"; +const VSCODE_PACKAGE = "./platform-specific/vscode/package.json"; const OBSIDIAN_WRAPPER = "./platform-specific/obsidian/wrapper.js"; +const VSCODE_WRAPPER = "./platform-specific/vscode/extension.js"; const USERSCRIPT_DIR = DIST_DIR + "/userscript"; const EXTENSION_DIR = DIST_DIR + "/extension"; const OBSIDIAN_DIR = DIST_DIR + "/obsidian"; +const VSCODE_DIR = DIST_DIR + "/vscode"; const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css"; const APPLICATION_ENTRY = SRC_DIR + "/application.js"; @@ -183,4 +186,21 @@ let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8'); obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`); writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); +// ============================================= +// Build VSCode extension +// ============================================= + +mkdirSync(VSCODE_DIR, { recursive: true }); + +// Wrap birb.js with VSCode extension boilerplate +let vscodeExtension = readFileSync(VSCODE_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs); + +// Create extension.js with extension code +writeFileSync(VSCODE_DIR + '/extension.js', vscodeExtension); + +// Copy package.json +let vscodePackage = readFileSync(VSCODE_PACKAGE, 'utf8'); +vscodePackage = vscodePackage.replace(VERSION_KEY, version); +writeFileSync(VSCODE_DIR + '/package.json', vscodePackage); + console.log(`Build complete: ${version}`); \ No newline at end of file diff --git a/dist/birb.js b/dist/birb.js index f3319da..b87b98a 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -6,7 +6,7 @@ RIGHT: 1, }; - let debugMode = location.hostname === "127.0.0.1"; + let debugMode = window.location.hostname === "127.0.0.1"; /** * @returns {boolean} Whether debug mode is enabled diff --git a/dist/extension.zip b/dist/extension.zip index 0eca0e49192915ae03c75d18962a63118c8f9079..14403ef9a71058555502a8a445c5e366314ebc6b 100644 GIT binary patch delta 22549 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 delta 22501 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 OeGm^ZwK}e>0RIP?6k)Oe diff --git a/dist/extension/birb.js b/dist/extension/birb.js index f3319da..b87b98a 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -6,7 +6,7 @@ RIGHT: 1, }; - let debugMode = location.hostname === "127.0.0.1"; + let debugMode = window.location.hostname === "127.0.0.1"; /** * @returns {boolean} Whether debug mode is enabled diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index 5cf3f53..f481a12 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -11,7 +11,7 @@ module.exports = class PocketBird extends Plugin { RIGHT: 1, }; - let debugMode = location.hostname === "127.0.0.1"; + let debugMode = window.location.hostname === "127.0.0.1"; /** * @returns {boolean} Whether debug mode is enabled diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index 2614285..b175d28 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -20,7 +20,7 @@ RIGHT: 1, }; - let debugMode = location.hostname === "127.0.0.1"; + let debugMode = window.location.hostname === "127.0.0.1"; /** * @returns {boolean} Whether debug mode is enabled diff --git a/dist/vscode/extension.js b/dist/vscode/extension.js new file mode 100644 index 0000000..25d5b23 --- /dev/null +++ b/dist/vscode/extension.js @@ -0,0 +1,2834 @@ +// The module 'vscode' contains the VS Code extensibility API +const vscode = require("vscode"); + +module.exports = { + activate, + deactivate, +}; + +function activate(context) { + console.log("Loading Pocket Bird..."); + (function () { + 'use strict'; + + const Directions = { + LEFT: -1, + RIGHT: 1, + }; + + let debugMode = window.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 + * @param {HTMLElement} [pageElement] The page element to constrain movement within + */ + function makeDraggable(element, parent = true, callback = () => { }, pageElement) { + 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(); + e.stopPropagation(); + }); + + 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) => { + const page = pageElement || document.documentElement; + const maxX = page.scrollWidth - elementToMove.clientWidth; + const maxY = page.scrollHeight - elementToMove.clientHeight; + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, Math.min(maxX, e.clientX - offsetX))}px`; + elementToMove.style.top = `${Math.max(0, Math.min(maxY, 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] + * @param {boolean} [allowEscape] Whether to allow closing with the Escape key + */ + function makeClosable(func, closeButton, allowEscape = true) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (allowEscape && 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"; + const ROOT_PATH = ""; + + /** + * @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"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + 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(); + } + } + + class ObsidianContext extends Context { + /** + * @override + * @returns {boolean} + */ + isContextActive() { + // @ts-expect-error + return typeof app !== "undefined" && typeof app.vault !== "undefined"; + } + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + return new Promise((resolve) => { + // @ts-expect-error + OBSIDIAN_PLUGIN.loadData().then((data) => { + resolve(data ?? {}); + }); + }); + } + + /** + * @override + * @param {BirbSaveData|{}} saveData + */ + async putSaveData(saveData) { + // @ts-expect-error + await OBSIDIAN_PLUGIN.saveData(saveData); + } + + /** @override */ + resetSaveData() { + this.putSaveData({}); + } + + /** @override */ + getFocusableElements() { + const elements = [ + ".workspace-leaf", + ".cm-callout", + ".HyperMD-codeblock-begin", + ".status-bar", + ".mobile-navbar" + ]; + return super.getFocusableElements().concat(elements); + } + + /** @override */ + getPath() { + // @ts-expect-error + const file = app.workspace.getActiveFile(); + if (file && this.getActiveEditorElement()) { + return file.path; + } else { + return ROOT_PATH; + } + } + + /** @override */ + getActivePage() { + if (this.getPath() === ROOT_PATH) { + // Root page, use document element + return document.documentElement + } + return this.getActiveEditorElement() ?? document.documentElement; + } + + /** @override */ + isPathApplicable(path) { + return path === this.getPath(); + } + + /** @override */ + areStickyNotesEnabled() { + return this.getPath() !== ROOT_PATH; + } + + /** @returns {HTMLElement|null} */ + getActiveEditorElement() { + // @ts-expect-error + const activeLeaf = app.workspace.activeLeaf; + const leafElement = activeLeaf?.view?.containerEl; + return leafElement?.querySelector(".cm-scroller") ?? null; + } + } + + const CONTEXTS = [ + new UserScriptContext(), + new ObsidianContext(), + 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 new LocalContext(); + } + + /** + * 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 }; + }, {}); + } + + /** + * @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; + } + } + + /** + * @param {StickyNote} stickyNote + * @param {HTMLElement} page + * @param {() => void} onSave + * @param {() => void} onDelete + * @returns {HTMLElement} + */ + function renderStickyNote(stickyNote, page, onSave, onDelete) { + const noteElement = makeElement("birb-window"); + noteElement.classList.add("birb-sticky-note"); + const color = getColor(stickyNote.id); + noteElement.style.setProperty("--birb-highlight", color); + noteElement.style.setProperty("--birb-border-color", color); + + // 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`; + page.appendChild(noteElement); + + makeDraggable(header, true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + onSave(); + }, page); + + if (closeButton) { + makeClosable(() => { + if (stickyNote.content.trim() === "" || confirm("Are you sure you want to delete this sticky note?")) { + onDelete(); + noteElement.remove(); + } + }, closeButton, false); + } + + 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 + const pageElement = getContext().getActivePage(); + const context = getContext(); + for (let stickyNote of stickyNotes) { + if (context.isPathApplicable(stickyNote.site)) { + renderStickyNote(stickyNote, pageElement, onSave, () => onDelete(stickyNote)); + } + } + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function createNewStickyNote(stickyNotes, onSave, onDelete) { + if (getContext().areStickyNotesEnabled() === false) { + return; + } + const id = Date.now().toString(); + const site = getContext().getPath(); + const stickyNote = new StickyNote(id, site, ""); + const page = getContext().getActivePage(); + const element = renderStickyNote(stickyNote, page, onSave, () => onDelete(stickyNote)); + element.style.left = `${page.clientWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${page.scrollTop + page.clientHeight / 2 - element.offsetHeight / 2}px`; + stickyNote.top = parseInt(element.style.top, 10); + stickyNote.left = parseInt(element.style.left, 10); + stickyNotes.push(stickyNote); + onSave(); + } + + /** + * Get a color based on the mod of the sticky note ID + * @param {string} id + * @returns {string} A color hex code + */ + function getColor(id) { + const colors = ["#ff8baa", "#79bcff", "#d18bff", "#6de192", "#ffd17c", "#ffb37c", "#ff7c7c"]; + const index = parseInt(id, 10) % colors.length; + return colors[index]; + } + + const MENU_ID = "birb-menu"; + const MENU_EXIT_ID = "birb-menu-exit"; + + class MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {boolean} [removeMenu] + */ + constructor(text, action, removeMenu = true) { + this.text = text; + this.action = action; + this.removeMenu = removeMenu; + } + } + + class ConditionalMenuItem extends MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {() => boolean} condition + * @param {boolean} [removeMenu] + */ + constructor(text, action, condition, removeMenu = true) { + super(text, action, removeMenu); + this.condition = condition; + } + } + + class DebugMenuItem extends ConditionalMenuItem { + /** + * @param {string} text + * @param {() => void} action + */ + constructor(text, action, removeMenu = true) { + super(text, action, () => isDebug(), removeMenu); + } + } + + 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 instanceof ConditionalMenuItem) || item.condition()) { + 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 instanceof ConditionalMenuItem) || item.condition()) { + 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 = `@font-face { + font-family: 'Monocraft'; + src: url("https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf") format('opentype'); + font-weight: normal; + font-style: normal; +} + +: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); + white-space: nowrap; +} + +.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; + animation: fade-in 0.15s ease-in; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.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 !important; + box-shadow: none !important; +}`; + 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 = 150; + 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; + + /** @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 ConditionalMenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote), () => getContext().areStickyNotesEnabled()), + 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.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, 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; + } + + 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 lastPath = getContext().getPath().split("?")[0]; + setInterval(() => { + const currentPath = getContext().getPath().split("?")[0]; + if (currentPath !== lastPath) { + log("Path changed, updating sticky notes: " + currentPath); + lastPath = currentPath; + 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 MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); + const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); + 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 fixedAllowed = getContext() instanceof ObsidianContext; + const nonFixedElements = largeElements.filter((el) => { + if (fixedAllowed) { + return true; + } + 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); + }); + +})(); + + console.log("Pocket Bird loaded!"); +} + +function deactivate() {} + diff --git a/dist/vscode/package.json b/dist/vscode/package.json new file mode 100644 index 0000000..df9b117 --- /dev/null +++ b/dist/vscode/package.json @@ -0,0 +1,21 @@ +{ + "name": "pocket-bird", + "version": "2025.11.15", + "engines": { + "vscode": "^1.32.0" + }, + "activationEvents": [ + "onStartupFinished" + ], + "main": "extension.js", + "contributes": { + "commands": [ + { + "command": "pocket-bird.helloWorld", + "title": "Hello World", + "category": "Example" + } + ] + } +} + diff --git a/platform-specific/vscode/extension.js b/platform-specific/vscode/extension.js new file mode 100644 index 0000000..99dc66f --- /dev/null +++ b/platform-specific/vscode/extension.js @@ -0,0 +1,16 @@ +// The module 'vscode' contains the VS Code extensibility API +const vscode = require("vscode"); + +module.exports = { + activate, + deactivate, +}; + +function activate(context) { + console.log("Loading Pocket Bird..."); + __CODE__ + console.log("Pocket Bird loaded!"); +} + +function deactivate() {} + diff --git a/platform-specific/vscode/inject.sh b/platform-specific/vscode/inject.sh new file mode 100644 index 0000000..8d0a886 --- /dev/null +++ b/platform-specific/vscode/inject.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# my-vscode +export NODE_OPTIONS="--require /Users/idrees/Documents/Programs/JavaScript/Birb/platform-specific/vscode/patch.js" +"/Applications/Visual Studio Code.app/Contents/MacOS/Electron" \ + "$@" \ No newline at end of file diff --git a/platform-specific/vscode/package.json b/platform-specific/vscode/package.json new file mode 100644 index 0000000..a41ad8d --- /dev/null +++ b/platform-specific/vscode/package.json @@ -0,0 +1,21 @@ +{ + "name": "pocket-bird", + "version": "__VERSION__", + "engines": { + "vscode": "^1.32.0" + }, + "activationEvents": [ + "onStartupFinished" + ], + "main": "extension.js", + "contributes": { + "commands": [ + { + "command": "pocket-bird.helloWorld", + "title": "Hello World", + "category": "Example" + } + ] + } +} + diff --git a/platform-specific/vscode/patch.js b/platform-specific/vscode/patch.js new file mode 100644 index 0000000..35746ff --- /dev/null +++ b/platform-specific/vscode/patch.js @@ -0,0 +1 @@ +console.log("Birb patch for VSCode loaded."); \ No newline at end of file diff --git a/src/shared.js b/src/shared.js index f925699..3731936 100644 --- a/src/shared.js +++ b/src/shared.js @@ -3,7 +3,7 @@ export const Directions = { RIGHT: 1, }; -let debugMode = location.hostname === "127.0.0.1"; +let debugMode = window.location.hostname === "127.0.0.1"; /** * @returns {boolean} Whether debug mode is enabled