From 3b2081943d02219795145c55897445ca5738781e Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Wed, 21 Jan 2026 22:25:49 -0500 Subject: [PATCH] Add hat item --- dist/extension.zip | Bin 152927 -> 154034 bytes dist/extension/birb.js | 212 +++++++++++++++++++++++++++------- dist/extension/manifest.json | 2 +- dist/obsidian/main.js | 214 ++++++++++++++++++++++++++++------- dist/obsidian/manifest.json | 2 +- dist/userscript/birb.user.js | 214 ++++++++++++++++++++++++++++------- dist/web/birb.embed.js | 212 +++++++++++++++++++++++++++------- dist/web/birb.js | 212 +++++++++++++++++++++++++++------- src/application.js | 76 +++++++++++-- src/hats.js | 132 ++++++++++++++++----- src/stylesheet.css | 10 ++ 11 files changed, 1034 insertions(+), 252 deletions(-) diff --git a/dist/extension.zip b/dist/extension.zip index a087c4742264118ed40695e939e2681c83431c2a..88c37aecf11fcbbbb54eb11e17fd7a0495e459a5 100644 GIT binary patch delta 31589 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 D)|h8g diff --git a/dist/extension/birb.js b/dist/extension/birb.js index 24d6ef6..cecc246 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -629,6 +629,7 @@ FLOWER_HAT: "flower-hat" }; + /** @type {{ [hatId: string]: { name: string, description: string } }} */ const HAT_METADATA = { [HAT.NONE]: { name: "Invisible Hat", @@ -684,62 +685,108 @@ } const index = i - 1; const hatKey = HAT[hatName]; - const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); - const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); + const hatLayer = buildHatLayer(spriteSheet, hatKey, index); + const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1); hatLayers.base.push(hatLayer); hatLayers.down.push(downHatLayer); } return hatLayers; } + /** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Anim} + */ + function createHatItemAnimation(hatId, spriteSheet) { + const hatLayer = buildHatItemLayer(spriteSheet, hatId); + const frames = [ + new Frame([hatLayer]) + ]; + return new Anim(frames, [1000], true); + } + /** * @param {string[][]} spriteSheet * @param {string} hatName * @param {number} hatIndex - * @param {boolean} [outlineBottom=false] * @param {number} [yOffset=0] * @returns {Layer} */ - function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { + function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) { const LEFT_PADDING = 6; const RIGHT_PADDING = 14; const TOP_PADDING = 5 + yOffset; const BOTTOM_PADDING = Math.max(0, 15 - yOffset); - const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); - const paddedHatPixels = []; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, TOP_PADDING, BOTTOM_PADDING, LEFT_PADDING, RIGHT_PADDING); + hatPixels = drawOutline(hatPixels, false); + return new Layer(hatPixels, hatName); + } + + /** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Layer} + */ + function buildHatItemLayer(spriteSheet, hatId) { + if (hatId === HAT.NONE) { + return new Layer([], TAG.DEFAULT); + } + const hatIndex = Object.keys(HAT).indexOf(hatId) - 1; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, 1, 1, 1, 1); + hatPixels = drawOutline(hatPixels, true); + hatPixels = pushToBottom(hatPixels); + return new Layer(hatPixels, TAG.DEFAULT); + } + + /** + * Add transparent padding around the pixel array + * @param {string[][]} pixels + * @param {number} top + * @param {number} bottom + * @param {number} left + * @param {number} right + * @returns {string[][]} + */ + function pad(pixels, top, bottom, left, right) { + const paddedPixels = []; + const rowLength = pixels[0].length + left + right; // Top padding - for (let y = 0; y < TOP_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < top; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } // Left and right padding - for (let y = 0; y < hatPixels.length; y++) { + for (let y = 0; y < pixels.length; y++) { const row = []; - for (let x = 0; x < LEFT_PADDING; x++) { + for (let x = 0; x < left; x++) { row.push(PALETTE.TRANSPARENT); } - - for (let x = 0; x < hatPixels[y].length; x++) { - row.push(hatPixels[y][x]); + for (let x = 0; x < pixels[y].length; x++) { + row.push(pixels[y][x]); } - - for (let x = 0; x < RIGHT_PADDING; x++) { + for (let x = 0; x < right; x++) { row.push(PALETTE.TRANSPARENT); } - - paddedHatPixels.push(row); + paddedPixels.push(row); } // Bottom padding - for (let y = 0; y < BOTTOM_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < bottom; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } + return paddedPixels; + } - // Add outline + /** + * Draw an outline around non-transparent pixels + * @param {string[][]} pixels + * @param {boolean} [outlineBottom=false] + * @return {string[][]} + */ + function drawOutline(pixels, outlineBottom = false) { let neighborOffsets = [ [-1, 0], [1, 0], @@ -750,21 +797,42 @@ if (outlineBottom) { neighborOffsets.push([0, 1], [-1, 1], [1, 1]); } - for (let y = 0; y < paddedHatPixels.length; y++) { - for (let x = 0; x < paddedHatPixels[y].length; x++) { - const pixel = paddedHatPixels[y][x]; + for (let y = 0; y < pixels.length; y++) { + for (let x = 0; x < pixels[y].length; x++) { + const pixel = pixels[y][x]; if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { for (let [dx, dy] of neighborOffsets) { const newX = x + dx; const newY = y + dy; - if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { - paddedHatPixels[newY][newX] = PALETTE.BORDER; + if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) { + pixels[newY][newX] = PALETTE.BORDER; } } } } } - return new Layer(paddedHatPixels, hatName); + return pixels; + } + + /** + * Trim transparent rows from the bottom and push them to the top + * @param {string[][]} pixels + * @returns {string[][]} + */ + function pushToBottom(pixels) { + let trimmedPixels = pixels.slice(); + let trimCount = 0; + while (trimmedPixels.length > 1) { + const firstRow = trimmedPixels[trimmedPixels.length - 1]; + if (firstRow.every(pixel => pixel === PALETTE.TRANSPARENT)) { + trimmedPixels.pop(); + trimCount++; + } else { + break; + } + } + trimmedPixels = pad(trimmedPixels, trimCount, 0, 0, 0); + return trimmedPixels; } /** @@ -1602,6 +1670,16 @@ z-index: 2147483630 !important; } +.birb-item { + image-rendering: pixelated; + position: absolute; + bottom: 0; + transform-origin: bottom; + transform: scale(calc(var(--birb-scale) * 1.5)) !important; + transform-origin: bottom; + z-index: 2147483630 !important; +} + .birb-window { font-family: "Monocraft", monospace !important; line-height: initial !important; @@ -1950,6 +2028,7 @@ const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; const WARDROBE_ID = "birb-wardrobe"; + const HAT_ID = "birb-hat"; const DEFAULT_BIRD = "bluebird"; const DEFAULT_HAT = HAT.NONE; @@ -2068,7 +2147,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2026.1.20", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.20"); }, false), + new MenuItem("2026.1.21", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.21"); }, false), ]; const styleElement = document.createElement("style"); @@ -2257,6 +2336,9 @@ setInterval(update, UPDATE_INTERVAL); focusOnElement(true); + + // TODO: This is for testing + generateHat(); } function update() { @@ -2437,6 +2519,47 @@ } } + /** + * Insert the hat as an item element in the document if possible + */ + function generateHat() { + if (document.querySelector("#" + HAT_ID)) { + return; + } + // Select a random hat + const hatKeys = Object.keys(HAT); + const hatId = hatKeys[Math.floor(Math.random() * (hatKeys.length - 1)) + 1]; + + // Find a random valid element to place the hat on + const element = getRandomValidElement(); + if (!element) { + return; + } + + // Create hat element + const hatCanvas = document.createElement("canvas"); + hatCanvas.id = HAT_ID; + hatCanvas.classList.add("birb-item"); + hatCanvas.width = 14 * CANVAS_PIXEL_SIZE; + hatCanvas.height = 14 * CANVAS_PIXEL_SIZE; + const hatCtx = hatCanvas.getContext("2d"); + if (!hatCtx) { + return; + } + + // Create hat animation + const hatAnimation = createHatItemAnimation(hatId, HATS_SPRITE_SHEET); + hatAnimation.draw(hatCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, SPECIES[currentSpecies].colors, [TAG.DEFAULT]); + + // Position hat above the element + const rect = element.getBoundingClientRect(); + hatCanvas.style.left = (rect.left + rect.width / 2 - hatCanvas.width / 2) + "px"; + hatCanvas.style.top = (rect.top - hatCanvas.height + window.scrollY) + "px"; + + // Append to document + document.body.appendChild(hatCanvas); + } + /** * @param {string} birdType */ @@ -2768,14 +2891,9 @@ } /** - * 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 + * @returns {HTMLElement|null} The random element, or null if no valid element was found */ - function focusOnElement(teleport = false) { - if (frozen) { - return false; - } + function getRandomValidElement() { const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const inWindow = Array.from(elements).filter((img) => { @@ -2797,10 +2915,22 @@ } }); if (nonFixedElements.length === 0) { - return false; + return null; } const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; - focusedElement = randomElement; + return randomElement; + } + + /** + * 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; + } + focusedElement = getRandomValidElement(); log("Focusing on element: ", focusedElement); updateFocusedElementBounds(); if (teleport) { @@ -2808,7 +2938,7 @@ } else { flyTo(getFocusedElementRandomX(), getFocusedY()); } - return randomElement !== null; + return focusedElement !== null; } /** diff --git a/dist/extension/manifest.json b/dist/extension/manifest.json index 713ccb1..283d052 100644 --- a/dist/extension/manifest.json +++ b/dist/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a pet bird in your browser, what more could you want?", - "version": "2026.1.20", + "version": "2026.1.21", "homepage_url": "https://idreesinc.com", "icons": { "48": "images/icons/transparent/48x48x1.png", diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index dbe0fe1..4b1fd4c 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -1,7 +1,7 @@ const { Plugin, Notice } = require('obsidian'); module.exports = class PocketBird extends Plugin { onload() { - console.log("Loading Pocket Bird version 2026.1.20..."); + console.log("Loading Pocket Bird version 2026.1.21..."); const OBSIDIAN_PLUGIN = this; (function () { 'use strict'; @@ -634,6 +634,7 @@ module.exports = class PocketBird extends Plugin { FLOWER_HAT: "flower-hat" }; + /** @type {{ [hatId: string]: { name: string, description: string } }} */ const HAT_METADATA = { [HAT.NONE]: { name: "Invisible Hat", @@ -689,62 +690,108 @@ module.exports = class PocketBird extends Plugin { } const index = i - 1; const hatKey = HAT[hatName]; - const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); - const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); + const hatLayer = buildHatLayer(spriteSheet, hatKey, index); + const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1); hatLayers.base.push(hatLayer); hatLayers.down.push(downHatLayer); } return hatLayers; } + /** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Anim} + */ + function createHatItemAnimation(hatId, spriteSheet) { + const hatLayer = buildHatItemLayer(spriteSheet, hatId); + const frames = [ + new Frame([hatLayer]) + ]; + return new Anim(frames, [1000], true); + } + /** * @param {string[][]} spriteSheet * @param {string} hatName * @param {number} hatIndex - * @param {boolean} [outlineBottom=false] * @param {number} [yOffset=0] * @returns {Layer} */ - function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { + function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) { const LEFT_PADDING = 6; const RIGHT_PADDING = 14; const TOP_PADDING = 5 + yOffset; const BOTTOM_PADDING = Math.max(0, 15 - yOffset); - const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); - const paddedHatPixels = []; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, TOP_PADDING, BOTTOM_PADDING, LEFT_PADDING, RIGHT_PADDING); + hatPixels = drawOutline(hatPixels, false); + return new Layer(hatPixels, hatName); + } + + /** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Layer} + */ + function buildHatItemLayer(spriteSheet, hatId) { + if (hatId === HAT.NONE) { + return new Layer([], TAG.DEFAULT); + } + const hatIndex = Object.keys(HAT).indexOf(hatId) - 1; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, 1, 1, 1, 1); + hatPixels = drawOutline(hatPixels, true); + hatPixels = pushToBottom(hatPixels); + return new Layer(hatPixels, TAG.DEFAULT); + } + + /** + * Add transparent padding around the pixel array + * @param {string[][]} pixels + * @param {number} top + * @param {number} bottom + * @param {number} left + * @param {number} right + * @returns {string[][]} + */ + function pad(pixels, top, bottom, left, right) { + const paddedPixels = []; + const rowLength = pixels[0].length + left + right; // Top padding - for (let y = 0; y < TOP_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < top; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } // Left and right padding - for (let y = 0; y < hatPixels.length; y++) { + for (let y = 0; y < pixels.length; y++) { const row = []; - for (let x = 0; x < LEFT_PADDING; x++) { + for (let x = 0; x < left; x++) { row.push(PALETTE.TRANSPARENT); } - - for (let x = 0; x < hatPixels[y].length; x++) { - row.push(hatPixels[y][x]); + for (let x = 0; x < pixels[y].length; x++) { + row.push(pixels[y][x]); } - - for (let x = 0; x < RIGHT_PADDING; x++) { + for (let x = 0; x < right; x++) { row.push(PALETTE.TRANSPARENT); } - - paddedHatPixels.push(row); + paddedPixels.push(row); } // Bottom padding - for (let y = 0; y < BOTTOM_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < bottom; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } + return paddedPixels; + } - // Add outline + /** + * Draw an outline around non-transparent pixels + * @param {string[][]} pixels + * @param {boolean} [outlineBottom=false] + * @return {string[][]} + */ + function drawOutline(pixels, outlineBottom = false) { let neighborOffsets = [ [-1, 0], [1, 0], @@ -755,21 +802,42 @@ module.exports = class PocketBird extends Plugin { if (outlineBottom) { neighborOffsets.push([0, 1], [-1, 1], [1, 1]); } - for (let y = 0; y < paddedHatPixels.length; y++) { - for (let x = 0; x < paddedHatPixels[y].length; x++) { - const pixel = paddedHatPixels[y][x]; + for (let y = 0; y < pixels.length; y++) { + for (let x = 0; x < pixels[y].length; x++) { + const pixel = pixels[y][x]; if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { for (let [dx, dy] of neighborOffsets) { const newX = x + dx; const newY = y + dy; - if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { - paddedHatPixels[newY][newX] = PALETTE.BORDER; + if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) { + pixels[newY][newX] = PALETTE.BORDER; } } } } } - return new Layer(paddedHatPixels, hatName); + return pixels; + } + + /** + * Trim transparent rows from the bottom and push them to the top + * @param {string[][]} pixels + * @returns {string[][]} + */ + function pushToBottom(pixels) { + let trimmedPixels = pixels.slice(); + let trimCount = 0; + while (trimmedPixels.length > 1) { + const firstRow = trimmedPixels[trimmedPixels.length - 1]; + if (firstRow.every(pixel => pixel === PALETTE.TRANSPARENT)) { + trimmedPixels.pop(); + trimCount++; + } else { + break; + } + } + trimmedPixels = pad(trimmedPixels, trimCount, 0, 0, 0); + return trimmedPixels; } /** @@ -1645,6 +1713,16 @@ module.exports = class PocketBird extends Plugin { z-index: 2147483630 !important; } +.birb-item { + image-rendering: pixelated; + position: absolute; + bottom: 0; + transform-origin: bottom; + transform: scale(calc(var(--birb-scale) * 1.5)) !important; + transform-origin: bottom; + z-index: 2147483630 !important; +} + .birb-window { font-family: "Monocraft", monospace !important; line-height: initial !important; @@ -1993,6 +2071,7 @@ module.exports = class PocketBird extends Plugin { const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; const WARDROBE_ID = "birb-wardrobe"; + const HAT_ID = "birb-hat"; const DEFAULT_BIRD = "bluebird"; const DEFAULT_HAT = HAT.NONE; @@ -2111,7 +2190,7 @@ module.exports = class PocketBird extends Plugin { insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2026.1.20", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.20"); }, false), + new MenuItem("2026.1.21", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.21"); }, false), ]; const styleElement = document.createElement("style"); @@ -2300,6 +2379,9 @@ module.exports = class PocketBird extends Plugin { setInterval(update, UPDATE_INTERVAL); focusOnElement(true); + + // TODO: This is for testing + generateHat(); } function update() { @@ -2480,6 +2562,47 @@ module.exports = class PocketBird extends Plugin { } } + /** + * Insert the hat as an item element in the document if possible + */ + function generateHat() { + if (document.querySelector("#" + HAT_ID)) { + return; + } + // Select a random hat + const hatKeys = Object.keys(HAT); + const hatId = hatKeys[Math.floor(Math.random() * (hatKeys.length - 1)) + 1]; + + // Find a random valid element to place the hat on + const element = getRandomValidElement(); + if (!element) { + return; + } + + // Create hat element + const hatCanvas = document.createElement("canvas"); + hatCanvas.id = HAT_ID; + hatCanvas.classList.add("birb-item"); + hatCanvas.width = 14 * CANVAS_PIXEL_SIZE; + hatCanvas.height = 14 * CANVAS_PIXEL_SIZE; + const hatCtx = hatCanvas.getContext("2d"); + if (!hatCtx) { + return; + } + + // Create hat animation + const hatAnimation = createHatItemAnimation(hatId, HATS_SPRITE_SHEET); + hatAnimation.draw(hatCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, SPECIES[currentSpecies].colors, [TAG.DEFAULT]); + + // Position hat above the element + const rect = element.getBoundingClientRect(); + hatCanvas.style.left = (rect.left + rect.width / 2 - hatCanvas.width / 2) + "px"; + hatCanvas.style.top = (rect.top - hatCanvas.height + window.scrollY) + "px"; + + // Append to document + document.body.appendChild(hatCanvas); + } + /** * @param {string} birdType */ @@ -2811,14 +2934,9 @@ module.exports = class PocketBird extends Plugin { } /** - * 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 + * @returns {HTMLElement|null} The random element, or null if no valid element was found */ - function focusOnElement(teleport = false) { - if (frozen) { - return false; - } + function getRandomValidElement() { const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const inWindow = Array.from(elements).filter((img) => { @@ -2840,10 +2958,22 @@ module.exports = class PocketBird extends Plugin { } }); if (nonFixedElements.length === 0) { - return false; + return null; } const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; - focusedElement = randomElement; + return randomElement; + } + + /** + * 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; + } + focusedElement = getRandomValidElement(); log("Focusing on element: ", focusedElement); updateFocusedElementBounds(); if (teleport) { @@ -2851,7 +2981,7 @@ module.exports = class PocketBird extends Plugin { } else { flyTo(getFocusedElementRandomX(), getFocusedY()); } - return randomElement !== null; + return focusedElement !== null; } /** diff --git a/dist/obsidian/manifest.json b/dist/obsidian/manifest.json index cb12fa6..662f18c 100644 --- a/dist/obsidian/manifest.json +++ b/dist/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "pocket-bird", "name": "Pocket Bird", - "version": "2026.1.20", + "version": "2026.1.21", "minAppVersion": "0.15.0", "description": "Add a pet bird to fly around your notes and keep you company!", "author": "Idrees Hassan", diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index a42b11e..3a33416 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2026.1.20 +// @version 2026.1.21 // @description It's a pet bird in your browser, what more could you want? // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js @@ -643,6 +643,7 @@ FLOWER_HAT: "flower-hat" }; + /** @type {{ [hatId: string]: { name: string, description: string } }} */ const HAT_METADATA = { [HAT.NONE]: { name: "Invisible Hat", @@ -698,62 +699,108 @@ } const index = i - 1; const hatKey = HAT[hatName]; - const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); - const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); + const hatLayer = buildHatLayer(spriteSheet, hatKey, index); + const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1); hatLayers.base.push(hatLayer); hatLayers.down.push(downHatLayer); } return hatLayers; } + /** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Anim} + */ + function createHatItemAnimation(hatId, spriteSheet) { + const hatLayer = buildHatItemLayer(spriteSheet, hatId); + const frames = [ + new Frame([hatLayer]) + ]; + return new Anim(frames, [1000], true); + } + /** * @param {string[][]} spriteSheet * @param {string} hatName * @param {number} hatIndex - * @param {boolean} [outlineBottom=false] * @param {number} [yOffset=0] * @returns {Layer} */ - function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { + function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) { const LEFT_PADDING = 6; const RIGHT_PADDING = 14; const TOP_PADDING = 5 + yOffset; const BOTTOM_PADDING = Math.max(0, 15 - yOffset); - const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); - const paddedHatPixels = []; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, TOP_PADDING, BOTTOM_PADDING, LEFT_PADDING, RIGHT_PADDING); + hatPixels = drawOutline(hatPixels, false); + return new Layer(hatPixels, hatName); + } + + /** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Layer} + */ + function buildHatItemLayer(spriteSheet, hatId) { + if (hatId === HAT.NONE) { + return new Layer([], TAG.DEFAULT); + } + const hatIndex = Object.keys(HAT).indexOf(hatId) - 1; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, 1, 1, 1, 1); + hatPixels = drawOutline(hatPixels, true); + hatPixels = pushToBottom(hatPixels); + return new Layer(hatPixels, TAG.DEFAULT); + } + + /** + * Add transparent padding around the pixel array + * @param {string[][]} pixels + * @param {number} top + * @param {number} bottom + * @param {number} left + * @param {number} right + * @returns {string[][]} + */ + function pad(pixels, top, bottom, left, right) { + const paddedPixels = []; + const rowLength = pixels[0].length + left + right; // Top padding - for (let y = 0; y < TOP_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < top; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } // Left and right padding - for (let y = 0; y < hatPixels.length; y++) { + for (let y = 0; y < pixels.length; y++) { const row = []; - for (let x = 0; x < LEFT_PADDING; x++) { + for (let x = 0; x < left; x++) { row.push(PALETTE.TRANSPARENT); } - - for (let x = 0; x < hatPixels[y].length; x++) { - row.push(hatPixels[y][x]); + for (let x = 0; x < pixels[y].length; x++) { + row.push(pixels[y][x]); } - - for (let x = 0; x < RIGHT_PADDING; x++) { + for (let x = 0; x < right; x++) { row.push(PALETTE.TRANSPARENT); } - - paddedHatPixels.push(row); + paddedPixels.push(row); } // Bottom padding - for (let y = 0; y < BOTTOM_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < bottom; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } + return paddedPixels; + } - // Add outline + /** + * Draw an outline around non-transparent pixels + * @param {string[][]} pixels + * @param {boolean} [outlineBottom=false] + * @return {string[][]} + */ + function drawOutline(pixels, outlineBottom = false) { let neighborOffsets = [ [-1, 0], [1, 0], @@ -764,21 +811,42 @@ if (outlineBottom) { neighborOffsets.push([0, 1], [-1, 1], [1, 1]); } - for (let y = 0; y < paddedHatPixels.length; y++) { - for (let x = 0; x < paddedHatPixels[y].length; x++) { - const pixel = paddedHatPixels[y][x]; + for (let y = 0; y < pixels.length; y++) { + for (let x = 0; x < pixels[y].length; x++) { + const pixel = pixels[y][x]; if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { for (let [dx, dy] of neighborOffsets) { const newX = x + dx; const newY = y + dy; - if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { - paddedHatPixels[newY][newX] = PALETTE.BORDER; + if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) { + pixels[newY][newX] = PALETTE.BORDER; } } } } } - return new Layer(paddedHatPixels, hatName); + return pixels; + } + + /** + * Trim transparent rows from the bottom and push them to the top + * @param {string[][]} pixels + * @returns {string[][]} + */ + function pushToBottom(pixels) { + let trimmedPixels = pixels.slice(); + let trimCount = 0; + while (trimmedPixels.length > 1) { + const firstRow = trimmedPixels[trimmedPixels.length - 1]; + if (firstRow.every(pixel => pixel === PALETTE.TRANSPARENT)) { + trimmedPixels.pop(); + trimCount++; + } else { + break; + } + } + trimmedPixels = pad(trimmedPixels, trimCount, 0, 0, 0); + return trimmedPixels; } /** @@ -1607,6 +1675,16 @@ z-index: 2147483630 !important; } +.birb-item { + image-rendering: pixelated; + position: absolute; + bottom: 0; + transform-origin: bottom; + transform: scale(calc(var(--birb-scale) * 1.5)) !important; + transform-origin: bottom; + z-index: 2147483630 !important; +} + .birb-window { font-family: "Monocraft", monospace !important; line-height: initial !important; @@ -1955,6 +2033,7 @@ const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; const WARDROBE_ID = "birb-wardrobe"; + const HAT_ID = "birb-hat"; const DEFAULT_BIRD = "bluebird"; const DEFAULT_HAT = HAT.NONE; @@ -2073,7 +2152,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2026.1.20", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.20"); }, false), + new MenuItem("2026.1.21", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.21"); }, false), ]; const styleElement = document.createElement("style"); @@ -2262,6 +2341,9 @@ setInterval(update, UPDATE_INTERVAL); focusOnElement(true); + + // TODO: This is for testing + generateHat(); } function update() { @@ -2442,6 +2524,47 @@ } } + /** + * Insert the hat as an item element in the document if possible + */ + function generateHat() { + if (document.querySelector("#" + HAT_ID)) { + return; + } + // Select a random hat + const hatKeys = Object.keys(HAT); + const hatId = hatKeys[Math.floor(Math.random() * (hatKeys.length - 1)) + 1]; + + // Find a random valid element to place the hat on + const element = getRandomValidElement(); + if (!element) { + return; + } + + // Create hat element + const hatCanvas = document.createElement("canvas"); + hatCanvas.id = HAT_ID; + hatCanvas.classList.add("birb-item"); + hatCanvas.width = 14 * CANVAS_PIXEL_SIZE; + hatCanvas.height = 14 * CANVAS_PIXEL_SIZE; + const hatCtx = hatCanvas.getContext("2d"); + if (!hatCtx) { + return; + } + + // Create hat animation + const hatAnimation = createHatItemAnimation(hatId, HATS_SPRITE_SHEET); + hatAnimation.draw(hatCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, SPECIES[currentSpecies].colors, [TAG.DEFAULT]); + + // Position hat above the element + const rect = element.getBoundingClientRect(); + hatCanvas.style.left = (rect.left + rect.width / 2 - hatCanvas.width / 2) + "px"; + hatCanvas.style.top = (rect.top - hatCanvas.height + window.scrollY) + "px"; + + // Append to document + document.body.appendChild(hatCanvas); + } + /** * @param {string} birdType */ @@ -2773,14 +2896,9 @@ } /** - * 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 + * @returns {HTMLElement|null} The random element, or null if no valid element was found */ - function focusOnElement(teleport = false) { - if (frozen) { - return false; - } + function getRandomValidElement() { const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const inWindow = Array.from(elements).filter((img) => { @@ -2802,10 +2920,22 @@ } }); if (nonFixedElements.length === 0) { - return false; + return null; } const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; - focusedElement = randomElement; + return randomElement; + } + + /** + * 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; + } + focusedElement = getRandomValidElement(); log("Focusing on element: ", focusedElement); updateFocusedElementBounds(); if (teleport) { @@ -2813,7 +2943,7 @@ } else { flyTo(getFocusedElementRandomX(), getFocusedY()); } - return randomElement !== null; + return focusedElement !== null; } /** diff --git a/dist/web/birb.embed.js b/dist/web/birb.embed.js index a77d732..5992b61 100644 --- a/dist/web/birb.embed.js +++ b/dist/web/birb.embed.js @@ -629,6 +629,7 @@ FLOWER_HAT: "flower-hat" }; + /** @type {{ [hatId: string]: { name: string, description: string } }} */ const HAT_METADATA = { [HAT.NONE]: { name: "Invisible Hat", @@ -684,62 +685,108 @@ } const index = i - 1; const hatKey = HAT[hatName]; - const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); - const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); + const hatLayer = buildHatLayer(spriteSheet, hatKey, index); + const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1); hatLayers.base.push(hatLayer); hatLayers.down.push(downHatLayer); } return hatLayers; } + /** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Anim} + */ + function createHatItemAnimation(hatId, spriteSheet) { + const hatLayer = buildHatItemLayer(spriteSheet, hatId); + const frames = [ + new Frame([hatLayer]) + ]; + return new Anim(frames, [1000], true); + } + /** * @param {string[][]} spriteSheet * @param {string} hatName * @param {number} hatIndex - * @param {boolean} [outlineBottom=false] * @param {number} [yOffset=0] * @returns {Layer} */ - function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { + function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) { const LEFT_PADDING = 6; const RIGHT_PADDING = 14; const TOP_PADDING = 5 + yOffset; const BOTTOM_PADDING = Math.max(0, 15 - yOffset); - const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); - const paddedHatPixels = []; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, TOP_PADDING, BOTTOM_PADDING, LEFT_PADDING, RIGHT_PADDING); + hatPixels = drawOutline(hatPixels, false); + return new Layer(hatPixels, hatName); + } + + /** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Layer} + */ + function buildHatItemLayer(spriteSheet, hatId) { + if (hatId === HAT.NONE) { + return new Layer([], TAG.DEFAULT); + } + const hatIndex = Object.keys(HAT).indexOf(hatId) - 1; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, 1, 1, 1, 1); + hatPixels = drawOutline(hatPixels, true); + hatPixels = pushToBottom(hatPixels); + return new Layer(hatPixels, TAG.DEFAULT); + } + + /** + * Add transparent padding around the pixel array + * @param {string[][]} pixels + * @param {number} top + * @param {number} bottom + * @param {number} left + * @param {number} right + * @returns {string[][]} + */ + function pad(pixels, top, bottom, left, right) { + const paddedPixels = []; + const rowLength = pixels[0].length + left + right; // Top padding - for (let y = 0; y < TOP_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < top; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } // Left and right padding - for (let y = 0; y < hatPixels.length; y++) { + for (let y = 0; y < pixels.length; y++) { const row = []; - for (let x = 0; x < LEFT_PADDING; x++) { + for (let x = 0; x < left; x++) { row.push(PALETTE.TRANSPARENT); } - - for (let x = 0; x < hatPixels[y].length; x++) { - row.push(hatPixels[y][x]); + for (let x = 0; x < pixels[y].length; x++) { + row.push(pixels[y][x]); } - - for (let x = 0; x < RIGHT_PADDING; x++) { + for (let x = 0; x < right; x++) { row.push(PALETTE.TRANSPARENT); } - - paddedHatPixels.push(row); + paddedPixels.push(row); } // Bottom padding - for (let y = 0; y < BOTTOM_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < bottom; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } + return paddedPixels; + } - // Add outline + /** + * Draw an outline around non-transparent pixels + * @param {string[][]} pixels + * @param {boolean} [outlineBottom=false] + * @return {string[][]} + */ + function drawOutline(pixels, outlineBottom = false) { let neighborOffsets = [ [-1, 0], [1, 0], @@ -750,21 +797,42 @@ if (outlineBottom) { neighborOffsets.push([0, 1], [-1, 1], [1, 1]); } - for (let y = 0; y < paddedHatPixels.length; y++) { - for (let x = 0; x < paddedHatPixels[y].length; x++) { - const pixel = paddedHatPixels[y][x]; + for (let y = 0; y < pixels.length; y++) { + for (let x = 0; x < pixels[y].length; x++) { + const pixel = pixels[y][x]; if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { for (let [dx, dy] of neighborOffsets) { const newX = x + dx; const newY = y + dy; - if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { - paddedHatPixels[newY][newX] = PALETTE.BORDER; + if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) { + pixels[newY][newX] = PALETTE.BORDER; } } } } } - return new Layer(paddedHatPixels, hatName); + return pixels; + } + + /** + * Trim transparent rows from the bottom and push them to the top + * @param {string[][]} pixels + * @returns {string[][]} + */ + function pushToBottom(pixels) { + let trimmedPixels = pixels.slice(); + let trimCount = 0; + while (trimmedPixels.length > 1) { + const firstRow = trimmedPixels[trimmedPixels.length - 1]; + if (firstRow.every(pixel => pixel === PALETTE.TRANSPARENT)) { + trimmedPixels.pop(); + trimCount++; + } else { + break; + } + } + trimmedPixels = pad(trimmedPixels, trimCount, 0, 0, 0); + return trimmedPixels; } /** @@ -1587,6 +1655,16 @@ z-index: 2147483630 !important; } +.birb-item { + image-rendering: pixelated; + position: absolute; + bottom: 0; + transform-origin: bottom; + transform: scale(calc(var(--birb-scale) * 1.5)) !important; + transform-origin: bottom; + z-index: 2147483630 !important; +} + .birb-window { font-family: "Monocraft", monospace !important; line-height: initial !important; @@ -1935,6 +2013,7 @@ const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; const WARDROBE_ID = "birb-wardrobe"; + const HAT_ID = "birb-hat"; const DEFAULT_BIRD = "bluebird"; const DEFAULT_HAT = HAT.NONE; @@ -2053,7 +2132,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2026.1.20", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.20"); }, false), + new MenuItem("2026.1.21", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.21"); }, false), ]; const styleElement = document.createElement("style"); @@ -2242,6 +2321,9 @@ setInterval(update, UPDATE_INTERVAL); focusOnElement(true); + + // TODO: This is for testing + generateHat(); } function update() { @@ -2422,6 +2504,47 @@ } } + /** + * Insert the hat as an item element in the document if possible + */ + function generateHat() { + if (document.querySelector("#" + HAT_ID)) { + return; + } + // Select a random hat + const hatKeys = Object.keys(HAT); + const hatId = hatKeys[Math.floor(Math.random() * (hatKeys.length - 1)) + 1]; + + // Find a random valid element to place the hat on + const element = getRandomValidElement(); + if (!element) { + return; + } + + // Create hat element + const hatCanvas = document.createElement("canvas"); + hatCanvas.id = HAT_ID; + hatCanvas.classList.add("birb-item"); + hatCanvas.width = 14 * CANVAS_PIXEL_SIZE; + hatCanvas.height = 14 * CANVAS_PIXEL_SIZE; + const hatCtx = hatCanvas.getContext("2d"); + if (!hatCtx) { + return; + } + + // Create hat animation + const hatAnimation = createHatItemAnimation(hatId, HATS_SPRITE_SHEET); + hatAnimation.draw(hatCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, SPECIES[currentSpecies].colors, [TAG.DEFAULT]); + + // Position hat above the element + const rect = element.getBoundingClientRect(); + hatCanvas.style.left = (rect.left + rect.width / 2 - hatCanvas.width / 2) + "px"; + hatCanvas.style.top = (rect.top - hatCanvas.height + window.scrollY) + "px"; + + // Append to document + document.body.appendChild(hatCanvas); + } + /** * @param {string} birdType */ @@ -2753,14 +2876,9 @@ } /** - * 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 + * @returns {HTMLElement|null} The random element, or null if no valid element was found */ - function focusOnElement(teleport = false) { - if (frozen) { - return false; - } + function getRandomValidElement() { const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const inWindow = Array.from(elements).filter((img) => { @@ -2782,10 +2900,22 @@ } }); if (nonFixedElements.length === 0) { - return false; + return null; } const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; - focusedElement = randomElement; + return randomElement; + } + + /** + * 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; + } + focusedElement = getRandomValidElement(); log("Focusing on element: ", focusedElement); updateFocusedElementBounds(); if (teleport) { @@ -2793,7 +2923,7 @@ } else { flyTo(getFocusedElementRandomX(), getFocusedY()); } - return randomElement !== null; + return focusedElement !== null; } /** diff --git a/dist/web/birb.js b/dist/web/birb.js index a77d732..5992b61 100644 --- a/dist/web/birb.js +++ b/dist/web/birb.js @@ -629,6 +629,7 @@ FLOWER_HAT: "flower-hat" }; + /** @type {{ [hatId: string]: { name: string, description: string } }} */ const HAT_METADATA = { [HAT.NONE]: { name: "Invisible Hat", @@ -684,62 +685,108 @@ } const index = i - 1; const hatKey = HAT[hatName]; - const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); - const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); + const hatLayer = buildHatLayer(spriteSheet, hatKey, index); + const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1); hatLayers.base.push(hatLayer); hatLayers.down.push(downHatLayer); } return hatLayers; } + /** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Anim} + */ + function createHatItemAnimation(hatId, spriteSheet) { + const hatLayer = buildHatItemLayer(spriteSheet, hatId); + const frames = [ + new Frame([hatLayer]) + ]; + return new Anim(frames, [1000], true); + } + /** * @param {string[][]} spriteSheet * @param {string} hatName * @param {number} hatIndex - * @param {boolean} [outlineBottom=false] * @param {number} [yOffset=0] * @returns {Layer} */ - function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { + function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) { const LEFT_PADDING = 6; const RIGHT_PADDING = 14; const TOP_PADDING = 5 + yOffset; const BOTTOM_PADDING = Math.max(0, 15 - yOffset); - const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); - const paddedHatPixels = []; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, TOP_PADDING, BOTTOM_PADDING, LEFT_PADDING, RIGHT_PADDING); + hatPixels = drawOutline(hatPixels, false); + return new Layer(hatPixels, hatName); + } + + /** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Layer} + */ + function buildHatItemLayer(spriteSheet, hatId) { + if (hatId === HAT.NONE) { + return new Layer([], TAG.DEFAULT); + } + const hatIndex = Object.keys(HAT).indexOf(hatId) - 1; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, 1, 1, 1, 1); + hatPixels = drawOutline(hatPixels, true); + hatPixels = pushToBottom(hatPixels); + return new Layer(hatPixels, TAG.DEFAULT); + } + + /** + * Add transparent padding around the pixel array + * @param {string[][]} pixels + * @param {number} top + * @param {number} bottom + * @param {number} left + * @param {number} right + * @returns {string[][]} + */ + function pad(pixels, top, bottom, left, right) { + const paddedPixels = []; + const rowLength = pixels[0].length + left + right; // Top padding - for (let y = 0; y < TOP_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < top; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } // Left and right padding - for (let y = 0; y < hatPixels.length; y++) { + for (let y = 0; y < pixels.length; y++) { const row = []; - for (let x = 0; x < LEFT_PADDING; x++) { + for (let x = 0; x < left; x++) { row.push(PALETTE.TRANSPARENT); } - - for (let x = 0; x < hatPixels[y].length; x++) { - row.push(hatPixels[y][x]); + for (let x = 0; x < pixels[y].length; x++) { + row.push(pixels[y][x]); } - - for (let x = 0; x < RIGHT_PADDING; x++) { + for (let x = 0; x < right; x++) { row.push(PALETTE.TRANSPARENT); } - - paddedHatPixels.push(row); + paddedPixels.push(row); } // Bottom padding - for (let y = 0; y < BOTTOM_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < bottom; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } + return paddedPixels; + } - // Add outline + /** + * Draw an outline around non-transparent pixels + * @param {string[][]} pixels + * @param {boolean} [outlineBottom=false] + * @return {string[][]} + */ + function drawOutline(pixels, outlineBottom = false) { let neighborOffsets = [ [-1, 0], [1, 0], @@ -750,21 +797,42 @@ if (outlineBottom) { neighborOffsets.push([0, 1], [-1, 1], [1, 1]); } - for (let y = 0; y < paddedHatPixels.length; y++) { - for (let x = 0; x < paddedHatPixels[y].length; x++) { - const pixel = paddedHatPixels[y][x]; + for (let y = 0; y < pixels.length; y++) { + for (let x = 0; x < pixels[y].length; x++) { + const pixel = pixels[y][x]; if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { for (let [dx, dy] of neighborOffsets) { const newX = x + dx; const newY = y + dy; - if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { - paddedHatPixels[newY][newX] = PALETTE.BORDER; + if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) { + pixels[newY][newX] = PALETTE.BORDER; } } } } } - return new Layer(paddedHatPixels, hatName); + return pixels; + } + + /** + * Trim transparent rows from the bottom and push them to the top + * @param {string[][]} pixels + * @returns {string[][]} + */ + function pushToBottom(pixels) { + let trimmedPixels = pixels.slice(); + let trimCount = 0; + while (trimmedPixels.length > 1) { + const firstRow = trimmedPixels[trimmedPixels.length - 1]; + if (firstRow.every(pixel => pixel === PALETTE.TRANSPARENT)) { + trimmedPixels.pop(); + trimCount++; + } else { + break; + } + } + trimmedPixels = pad(trimmedPixels, trimCount, 0, 0, 0); + return trimmedPixels; } /** @@ -1587,6 +1655,16 @@ z-index: 2147483630 !important; } +.birb-item { + image-rendering: pixelated; + position: absolute; + bottom: 0; + transform-origin: bottom; + transform: scale(calc(var(--birb-scale) * 1.5)) !important; + transform-origin: bottom; + z-index: 2147483630 !important; +} + .birb-window { font-family: "Monocraft", monospace !important; line-height: initial !important; @@ -1935,6 +2013,7 @@ const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; const WARDROBE_ID = "birb-wardrobe"; + const HAT_ID = "birb-hat"; const DEFAULT_BIRD = "bluebird"; const DEFAULT_HAT = HAT.NONE; @@ -2053,7 +2132,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2026.1.20", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.20"); }, false), + new MenuItem("2026.1.21", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.21"); }, false), ]; const styleElement = document.createElement("style"); @@ -2242,6 +2321,9 @@ setInterval(update, UPDATE_INTERVAL); focusOnElement(true); + + // TODO: This is for testing + generateHat(); } function update() { @@ -2422,6 +2504,47 @@ } } + /** + * Insert the hat as an item element in the document if possible + */ + function generateHat() { + if (document.querySelector("#" + HAT_ID)) { + return; + } + // Select a random hat + const hatKeys = Object.keys(HAT); + const hatId = hatKeys[Math.floor(Math.random() * (hatKeys.length - 1)) + 1]; + + // Find a random valid element to place the hat on + const element = getRandomValidElement(); + if (!element) { + return; + } + + // Create hat element + const hatCanvas = document.createElement("canvas"); + hatCanvas.id = HAT_ID; + hatCanvas.classList.add("birb-item"); + hatCanvas.width = 14 * CANVAS_PIXEL_SIZE; + hatCanvas.height = 14 * CANVAS_PIXEL_SIZE; + const hatCtx = hatCanvas.getContext("2d"); + if (!hatCtx) { + return; + } + + // Create hat animation + const hatAnimation = createHatItemAnimation(hatId, HATS_SPRITE_SHEET); + hatAnimation.draw(hatCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, SPECIES[currentSpecies].colors, [TAG.DEFAULT]); + + // Position hat above the element + const rect = element.getBoundingClientRect(); + hatCanvas.style.left = (rect.left + rect.width / 2 - hatCanvas.width / 2) + "px"; + hatCanvas.style.top = (rect.top - hatCanvas.height + window.scrollY) + "px"; + + // Append to document + document.body.appendChild(hatCanvas); + } + /** * @param {string} birdType */ @@ -2753,14 +2876,9 @@ } /** - * 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 + * @returns {HTMLElement|null} The random element, or null if no valid element was found */ - function focusOnElement(teleport = false) { - if (frozen) { - return false; - } + function getRandomValidElement() { const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const inWindow = Array.from(elements).filter((img) => { @@ -2782,10 +2900,22 @@ } }); if (nonFixedElements.length === 0) { - return false; + return null; } const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; - focusedElement = randomElement; + return randomElement; + } + + /** + * 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; + } + focusedElement = getRandomValidElement(); log("Focusing on element: ", focusedElement); updateFocusedElementBounds(); if (teleport) { @@ -2793,7 +2923,7 @@ } else { flyTo(getFocusedElementRandomX(), getFocusedY()); } - return randomElement !== null; + return focusedElement !== null; } /** diff --git a/src/application.js b/src/application.js index cb76bea..6f1af83 100644 --- a/src/application.js +++ b/src/application.js @@ -1,5 +1,5 @@ import Frame from './animation/frame.js'; -import Layer from './animation/layer.js'; +import Layer, { TAG } from './animation/layer.js'; import Anim from './animation/anim.js'; import { Birb, Animations } from './birb.js'; import { Birdsong } from './sound.js'; @@ -43,7 +43,7 @@ import { switchMenuItems, MENU_EXIT_ID } from './menu.js'; -import { HAT, HAT_METADATA } from './hats.js'; +import { HAT, HAT_METADATA, createHatItemAnimation } from './hats.js'; /** @@ -86,6 +86,7 @@ const HATS_SPRITE_SHEET = "__HATS_SPRITE_SHEET__"; const FIELD_GUIDE_ID = "birb-field-guide"; const FEATHER_ID = "birb-feather"; const WARDROBE_ID = "birb-wardrobe"; +const HAT_ID = "birb-hat"; const DEFAULT_BIRD = "bluebird"; const DEFAULT_HAT = HAT.NONE; @@ -393,6 +394,9 @@ function startApplication(birbPixels, featherPixels, hatsPixels) { setInterval(update, UPDATE_INTERVAL); focusOnElement(true); + + // TODO: This is for testing + generateHat(); } function update() { @@ -576,6 +580,47 @@ function startApplication(birbPixels, featherPixels, hatsPixels) { } } + /** + * Insert the hat as an item element in the document if possible + */ + function generateHat() { + if (document.querySelector("#" + HAT_ID)) { + return; + } + // Select a random hat + const hatKeys = Object.keys(HAT); + const hatId = hatKeys[Math.floor(Math.random() * (hatKeys.length - 1)) + 1]; + + // Find a random valid element to place the hat on + const element = getRandomValidElement(); + if (!element) { + return; + } + + // Create hat element + const hatCanvas = document.createElement("canvas"); + hatCanvas.id = HAT_ID; + hatCanvas.classList.add("birb-item"); + hatCanvas.width = 14 * CANVAS_PIXEL_SIZE; + hatCanvas.height = 14 * CANVAS_PIXEL_SIZE; + const hatCtx = hatCanvas.getContext("2d"); + if (!hatCtx) { + return; + } + + // Create hat animation + const hatAnimation = createHatItemAnimation(hatId, HATS_SPRITE_SHEET); + hatAnimation.draw(hatCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, SPECIES[currentSpecies].colors, [TAG.DEFAULT]); + + // Position hat above the element + const rect = element.getBoundingClientRect(); + hatCanvas.style.left = (rect.left + rect.width / 2 - hatCanvas.width / 2) + "px"; + hatCanvas.style.top = (rect.top - hatCanvas.height + window.scrollY) + "px"; + + // Append to document + document.body.appendChild(hatCanvas); + } + /** * @param {string} birdType */ @@ -912,14 +957,9 @@ function startApplication(birbPixels, featherPixels, hatsPixels) { } /** - * 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 + * @returns {HTMLElement|null} The random element, or null if no valid element was found */ - function focusOnElement(teleport = false) { - if (frozen) { - return false; - } + function getRandomValidElement() { const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const inWindow = Array.from(elements).filter((img) => { @@ -947,10 +987,22 @@ function startApplication(birbPixels, featherPixels, hatsPixels) { return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { - return false; + return null; } const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; - focusedElement = randomElement; + return randomElement; + } + + /** + * 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; + } + focusedElement = getRandomValidElement(); log("Focusing on element: ", focusedElement); updateFocusedElementBounds(); if (teleport) { @@ -958,7 +1010,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) { } else { flyTo(getFocusedElementRandomX(), getFocusedY()); } - return randomElement !== null; + return focusedElement !== null; } /** diff --git a/src/hats.js b/src/hats.js index c93916d..c875e59 100644 --- a/src/hats.js +++ b/src/hats.js @@ -1,4 +1,6 @@ -import Layer from "./animation/layer.js"; +import Anim from "./animation/anim.js"; +import Frame from "./animation/frame.js"; +import Layer, { TAG } from "./animation/layer.js"; import { PALETTE } from "./animation/sprites.js"; import { getLayerPixels } from "./shared.js"; @@ -16,6 +18,7 @@ export const HAT = { FLOWER_HAT: "flower-hat" }; +/** @type {{ [hatId: string]: { name: string, description: string } }} */ export const HAT_METADATA = { [HAT.NONE]: { name: "Invisible Hat", @@ -71,62 +74,108 @@ export function createHatLayers(spriteSheet) { } const index = i - 1; const hatKey = HAT[hatName]; - const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); - const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); + const hatLayer = buildHatLayer(spriteSheet, hatKey, index); + const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1); hatLayers.base.push(hatLayer); hatLayers.down.push(downHatLayer); } return hatLayers; } +/** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Anim} + */ +export function createHatItemAnimation(hatId, spriteSheet) { + const hatLayer = buildHatItemLayer(spriteSheet, hatId); + const frames = [ + new Frame([hatLayer]) + ]; + return new Anim(frames, [1000], true); +} + /** * @param {string[][]} spriteSheet * @param {string} hatName * @param {number} hatIndex - * @param {boolean} [outlineBottom=false] * @param {number} [yOffset=0] * @returns {Layer} */ -function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { +function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) { const LEFT_PADDING = 6; const RIGHT_PADDING = 14; const TOP_PADDING = 5 + yOffset; const BOTTOM_PADDING = Math.max(0, 15 - yOffset); - const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); - const paddedHatPixels = []; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, TOP_PADDING, BOTTOM_PADDING, LEFT_PADDING, RIGHT_PADDING); + hatPixels = drawOutline(hatPixels, false); + return new Layer(hatPixels, hatName); +} + +/** + * @param {string[][]} spriteSheet + * @param {string} hatId + * @returns {Layer} + */ +function buildHatItemLayer(spriteSheet, hatId) { + if (hatId === HAT.NONE) { + return new Layer([], TAG.DEFAULT); + } + const hatIndex = Object.keys(HAT).indexOf(hatId) - 1; + let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); + hatPixels = pad(hatPixels, 1, 1, 1, 1); + hatPixels = drawOutline(hatPixels, true); + hatPixels = pushToBottom(hatPixels); + return new Layer(hatPixels, TAG.DEFAULT); +} + +/** + * Add transparent padding around the pixel array + * @param {string[][]} pixels + * @param {number} top + * @param {number} bottom + * @param {number} left + * @param {number} right + * @returns {string[][]} + */ +function pad(pixels, top, bottom, left, right) { + const paddedPixels = []; + const rowLength = pixels[0].length + left + right; // Top padding - for (let y = 0; y < TOP_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < top; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } // Left and right padding - for (let y = 0; y < hatPixels.length; y++) { + for (let y = 0; y < pixels.length; y++) { const row = []; - for (let x = 0; x < LEFT_PADDING; x++) { + for (let x = 0; x < left; x++) { row.push(PALETTE.TRANSPARENT); } - - for (let x = 0; x < hatPixels[y].length; x++) { - row.push(hatPixels[y][x]); + for (let x = 0; x < pixels[y].length; x++) { + row.push(pixels[y][x]); } - - for (let x = 0; x < RIGHT_PADDING; x++) { + for (let x = 0; x < right; x++) { row.push(PALETTE.TRANSPARENT); } - - paddedHatPixels.push(row); + paddedPixels.push(row); } // Bottom padding - for (let y = 0; y < BOTTOM_PADDING; y++) { - paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) - .fill(PALETTE.TRANSPARENT) - ); + for (let y = 0; y < bottom; y++) { + paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT)); } + return paddedPixels; +} - // Add outline +/** + * Draw an outline around non-transparent pixels + * @param {string[][]} pixels + * @param {boolean} [outlineBottom=false] + * @return {string[][]} + */ +function drawOutline(pixels, outlineBottom = false) { let neighborOffsets = [ [-1, 0], [1, 0], @@ -137,19 +186,40 @@ function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yO if (outlineBottom) { neighborOffsets.push([0, 1], [-1, 1], [1, 1]); } - for (let y = 0; y < paddedHatPixels.length; y++) { - for (let x = 0; x < paddedHatPixels[y].length; x++) { - const pixel = paddedHatPixels[y][x]; + for (let y = 0; y < pixels.length; y++) { + for (let x = 0; x < pixels[y].length; x++) { + const pixel = pixels[y][x]; if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { for (let [dx, dy] of neighborOffsets) { const newX = x + dx; const newY = y + dy; - if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { - paddedHatPixels[newY][newX] = PALETTE.BORDER; + if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) { + pixels[newY][newX] = PALETTE.BORDER; } } } } } - return new Layer(paddedHatPixels, hatName); + return pixels; +} + +/** + * Trim transparent rows from the bottom and push them to the top + * @param {string[][]} pixels + * @returns {string[][]} + */ +function pushToBottom(pixels) { + let trimmedPixels = pixels.slice(); + let trimCount = 0; + while (trimmedPixels.length > 1) { + const firstRow = trimmedPixels[trimmedPixels.length - 1]; + if (firstRow.every(pixel => pixel === PALETTE.TRANSPARENT)) { + trimmedPixels.pop(); + trimCount++; + } else { + break; + } + } + trimmedPixels = pad(trimmedPixels, trimCount, 0, 0, 0); + return trimmedPixels; } \ No newline at end of file diff --git a/src/stylesheet.css b/src/stylesheet.css index 4df8d50..c02f3d3 100644 --- a/src/stylesheet.css +++ b/src/stylesheet.css @@ -41,6 +41,16 @@ z-index: 2147483630 !important; } +.birb-item { + image-rendering: pixelated; + position: absolute; + bottom: 0; + transform-origin: bottom; + transform: scale(calc(var(--birb-scale) * 1.5)) !important; + transform-origin: bottom; + z-index: 2147483630 !important; +} + .birb-window { font-family: "Monocraft", monospace !important; line-height: initial !important;