Add hat item

This commit is contained in:
Idrees Hassan
2026-01-21 22:25:49 -05:00
parent f5742ac3a7
commit 3b2081943d
11 changed files with 1034 additions and 252 deletions

BIN
dist/extension.zip vendored

Binary file not shown.

212
dist/extension/birb.js vendored
View File

@@ -629,6 +629,7 @@
FLOWER_HAT: "flower-hat" FLOWER_HAT: "flower-hat"
}; };
/** @type {{ [hatId: string]: { name: string, description: string } }} */
const HAT_METADATA = { const HAT_METADATA = {
[HAT.NONE]: { [HAT.NONE]: {
name: "Invisible Hat", name: "Invisible Hat",
@@ -684,62 +685,108 @@
} }
const index = i - 1; const index = i - 1;
const hatKey = HAT[hatName]; const hatKey = HAT[hatName];
const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); const hatLayer = buildHatLayer(spriteSheet, hatKey, index);
const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1);
hatLayers.base.push(hatLayer); hatLayers.base.push(hatLayer);
hatLayers.down.push(downHatLayer); hatLayers.down.push(downHatLayer);
} }
return hatLayers; 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[][]} spriteSheet
* @param {string} hatName * @param {string} hatName
* @param {number} hatIndex * @param {number} hatIndex
* @param {boolean} [outlineBottom=false]
* @param {number} [yOffset=0] * @param {number} [yOffset=0]
* @returns {Layer} * @returns {Layer}
*/ */
function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) {
const LEFT_PADDING = 6; const LEFT_PADDING = 6;
const RIGHT_PADDING = 14; const RIGHT_PADDING = 14;
const TOP_PADDING = 5 + yOffset; const TOP_PADDING = 5 + yOffset;
const BOTTOM_PADDING = Math.max(0, 15 - yOffset); const BOTTOM_PADDING = Math.max(0, 15 - yOffset);
const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
const paddedHatPixels = []; 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 // Top padding
for (let y = 0; y < TOP_PADDING; y++) { for (let y = 0; y < top; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.fill(PALETTE.TRANSPARENT)
);
} }
// Left and right padding // Left and right padding
for (let y = 0; y < hatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
const row = []; const row = [];
for (let x = 0; x < LEFT_PADDING; x++) { for (let x = 0; x < left; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
for (let x = 0; x < pixels[y].length; x++) {
for (let x = 0; x < hatPixels[y].length; x++) { row.push(pixels[y][x]);
row.push(hatPixels[y][x]);
} }
for (let x = 0; x < right; x++) {
for (let x = 0; x < RIGHT_PADDING; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
paddedPixels.push(row);
paddedHatPixels.push(row);
} }
// Bottom padding // Bottom padding
for (let y = 0; y < BOTTOM_PADDING; y++) { for (let y = 0; y < bottom; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.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 = [ let neighborOffsets = [
[-1, 0], [-1, 0],
[1, 0], [1, 0],
@@ -750,21 +797,42 @@
if (outlineBottom) { if (outlineBottom) {
neighborOffsets.push([0, 1], [-1, 1], [1, 1]); neighborOffsets.push([0, 1], [-1, 1], [1, 1]);
} }
for (let y = 0; y < paddedHatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
for (let x = 0; x < paddedHatPixels[y].length; x++) { for (let x = 0; x < pixels[y].length; x++) {
const pixel = paddedHatPixels[y][x]; const pixel = pixels[y][x];
if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) {
for (let [dx, dy] of neighborOffsets) { for (let [dx, dy] of neighborOffsets) {
const newX = x + dx; const newX = x + dx;
const newY = y + dy; const newY = y + dy;
if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) {
paddedHatPixels[newY][newX] = PALETTE.BORDER; 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; 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 { .birb-window {
font-family: "Monocraft", monospace !important; font-family: "Monocraft", monospace !important;
line-height: initial !important; line-height: initial !important;
@@ -1950,6 +2028,7 @@
const FIELD_GUIDE_ID = "birb-field-guide"; const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather"; const FEATHER_ID = "birb-feather";
const WARDROBE_ID = "birb-wardrobe"; const WARDROBE_ID = "birb-wardrobe";
const HAT_ID = "birb-hat";
const DEFAULT_BIRD = "bluebird"; const DEFAULT_BIRD = "bluebird";
const DEFAULT_HAT = HAT.NONE; const DEFAULT_HAT = HAT.NONE;
@@ -2068,7 +2147,7 @@
insertModal(`${birdBirb()} Mode`, message); insertModal(`${birdBirb()} Mode`, message);
}), }),
new Separator(), 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"); const styleElement = document.createElement("style");
@@ -2257,6 +2336,9 @@
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); focusOnElement(true);
// TODO: This is for testing
generateHat();
} }
function update() { 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 * @param {string} birdType
*/ */
@@ -2768,14 +2891,9 @@
} }
/** /**
* Focus on an element within the viewport * @returns {HTMLElement|null} The random element, or null if no valid element was found
* @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) { function getRandomValidElement() {
if (frozen) {
return false;
}
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
@@ -2797,10 +2915,22 @@
} }
}); });
if (nonFixedElements.length === 0) { if (nonFixedElements.length === 0) {
return false; return null;
} }
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; 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); log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
@@ -2808,7 +2938,7 @@
} else { } else {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return randomElement !== null; return focusedElement !== null;
} }
/** /**

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Pocket Bird", "name": "Pocket Bird",
"description": "It's a pet bird in your browser, what more could you want?", "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", "homepage_url": "https://idreesinc.com",
"icons": { "icons": {
"48": "images/icons/transparent/48x48x1.png", "48": "images/icons/transparent/48x48x1.png",

214
dist/obsidian/main.js vendored
View File

@@ -1,7 +1,7 @@
const { Plugin, Notice } = require('obsidian'); const { Plugin, Notice } = require('obsidian');
module.exports = class PocketBird extends Plugin { module.exports = class PocketBird extends Plugin {
onload() { onload() {
console.log("Loading Pocket Bird version 2026.1.20..."); console.log("Loading Pocket Bird version 2026.1.21...");
const OBSIDIAN_PLUGIN = this; const OBSIDIAN_PLUGIN = this;
(function () { (function () {
'use strict'; 'use strict';
@@ -634,6 +634,7 @@ module.exports = class PocketBird extends Plugin {
FLOWER_HAT: "flower-hat" FLOWER_HAT: "flower-hat"
}; };
/** @type {{ [hatId: string]: { name: string, description: string } }} */
const HAT_METADATA = { const HAT_METADATA = {
[HAT.NONE]: { [HAT.NONE]: {
name: "Invisible Hat", name: "Invisible Hat",
@@ -689,62 +690,108 @@ module.exports = class PocketBird extends Plugin {
} }
const index = i - 1; const index = i - 1;
const hatKey = HAT[hatName]; const hatKey = HAT[hatName];
const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); const hatLayer = buildHatLayer(spriteSheet, hatKey, index);
const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1);
hatLayers.base.push(hatLayer); hatLayers.base.push(hatLayer);
hatLayers.down.push(downHatLayer); hatLayers.down.push(downHatLayer);
} }
return hatLayers; 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[][]} spriteSheet
* @param {string} hatName * @param {string} hatName
* @param {number} hatIndex * @param {number} hatIndex
* @param {boolean} [outlineBottom=false]
* @param {number} [yOffset=0] * @param {number} [yOffset=0]
* @returns {Layer} * @returns {Layer}
*/ */
function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) {
const LEFT_PADDING = 6; const LEFT_PADDING = 6;
const RIGHT_PADDING = 14; const RIGHT_PADDING = 14;
const TOP_PADDING = 5 + yOffset; const TOP_PADDING = 5 + yOffset;
const BOTTOM_PADDING = Math.max(0, 15 - yOffset); const BOTTOM_PADDING = Math.max(0, 15 - yOffset);
const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
const paddedHatPixels = []; 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 // Top padding
for (let y = 0; y < TOP_PADDING; y++) { for (let y = 0; y < top; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.fill(PALETTE.TRANSPARENT)
);
} }
// Left and right padding // Left and right padding
for (let y = 0; y < hatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
const row = []; const row = [];
for (let x = 0; x < LEFT_PADDING; x++) { for (let x = 0; x < left; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
for (let x = 0; x < pixels[y].length; x++) {
for (let x = 0; x < hatPixels[y].length; x++) { row.push(pixels[y][x]);
row.push(hatPixels[y][x]);
} }
for (let x = 0; x < right; x++) {
for (let x = 0; x < RIGHT_PADDING; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
paddedPixels.push(row);
paddedHatPixels.push(row);
} }
// Bottom padding // Bottom padding
for (let y = 0; y < BOTTOM_PADDING; y++) { for (let y = 0; y < bottom; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.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 = [ let neighborOffsets = [
[-1, 0], [-1, 0],
[1, 0], [1, 0],
@@ -755,21 +802,42 @@ module.exports = class PocketBird extends Plugin {
if (outlineBottom) { if (outlineBottom) {
neighborOffsets.push([0, 1], [-1, 1], [1, 1]); neighborOffsets.push([0, 1], [-1, 1], [1, 1]);
} }
for (let y = 0; y < paddedHatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
for (let x = 0; x < paddedHatPixels[y].length; x++) { for (let x = 0; x < pixels[y].length; x++) {
const pixel = paddedHatPixels[y][x]; const pixel = pixels[y][x];
if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) {
for (let [dx, dy] of neighborOffsets) { for (let [dx, dy] of neighborOffsets) {
const newX = x + dx; const newX = x + dx;
const newY = y + dy; const newY = y + dy;
if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) {
paddedHatPixels[newY][newX] = PALETTE.BORDER; 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; 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 { .birb-window {
font-family: "Monocraft", monospace !important; font-family: "Monocraft", monospace !important;
line-height: initial !important; line-height: initial !important;
@@ -1993,6 +2071,7 @@ module.exports = class PocketBird extends Plugin {
const FIELD_GUIDE_ID = "birb-field-guide"; const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather"; const FEATHER_ID = "birb-feather";
const WARDROBE_ID = "birb-wardrobe"; const WARDROBE_ID = "birb-wardrobe";
const HAT_ID = "birb-hat";
const DEFAULT_BIRD = "bluebird"; const DEFAULT_BIRD = "bluebird";
const DEFAULT_HAT = HAT.NONE; const DEFAULT_HAT = HAT.NONE;
@@ -2111,7 +2190,7 @@ module.exports = class PocketBird extends Plugin {
insertModal(`${birdBirb()} Mode`, message); insertModal(`${birdBirb()} Mode`, message);
}), }),
new Separator(), 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"); const styleElement = document.createElement("style");
@@ -2300,6 +2379,9 @@ module.exports = class PocketBird extends Plugin {
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); focusOnElement(true);
// TODO: This is for testing
generateHat();
} }
function update() { 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 * @param {string} birdType
*/ */
@@ -2811,14 +2934,9 @@ module.exports = class PocketBird extends Plugin {
} }
/** /**
* Focus on an element within the viewport * @returns {HTMLElement|null} The random element, or null if no valid element was found
* @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) { function getRandomValidElement() {
if (frozen) {
return false;
}
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
@@ -2840,10 +2958,22 @@ module.exports = class PocketBird extends Plugin {
} }
}); });
if (nonFixedElements.length === 0) { if (nonFixedElements.length === 0) {
return false; return null;
} }
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; 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); log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
@@ -2851,7 +2981,7 @@ module.exports = class PocketBird extends Plugin {
} else { } else {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return randomElement !== null; return focusedElement !== null;
} }
/** /**

View File

@@ -1,7 +1,7 @@
{ {
"id": "pocket-bird", "id": "pocket-bird",
"name": "Pocket Bird", "name": "Pocket Bird",
"version": "2026.1.20", "version": "2026.1.21",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "Add a pet bird to fly around your notes and keep you company!", "description": "Add a pet bird to fly around your notes and keep you company!",
"author": "Idrees Hassan", "author": "Idrees Hassan",

View File

@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Pocket Bird // @name Pocket Bird
// @namespace https://idreesinc.com // @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? // @description It's a pet bird in your browser, what more could you want?
// @author Idrees // @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js
@@ -643,6 +643,7 @@
FLOWER_HAT: "flower-hat" FLOWER_HAT: "flower-hat"
}; };
/** @type {{ [hatId: string]: { name: string, description: string } }} */
const HAT_METADATA = { const HAT_METADATA = {
[HAT.NONE]: { [HAT.NONE]: {
name: "Invisible Hat", name: "Invisible Hat",
@@ -698,62 +699,108 @@
} }
const index = i - 1; const index = i - 1;
const hatKey = HAT[hatName]; const hatKey = HAT[hatName];
const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); const hatLayer = buildHatLayer(spriteSheet, hatKey, index);
const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1);
hatLayers.base.push(hatLayer); hatLayers.base.push(hatLayer);
hatLayers.down.push(downHatLayer); hatLayers.down.push(downHatLayer);
} }
return hatLayers; 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[][]} spriteSheet
* @param {string} hatName * @param {string} hatName
* @param {number} hatIndex * @param {number} hatIndex
* @param {boolean} [outlineBottom=false]
* @param {number} [yOffset=0] * @param {number} [yOffset=0]
* @returns {Layer} * @returns {Layer}
*/ */
function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) {
const LEFT_PADDING = 6; const LEFT_PADDING = 6;
const RIGHT_PADDING = 14; const RIGHT_PADDING = 14;
const TOP_PADDING = 5 + yOffset; const TOP_PADDING = 5 + yOffset;
const BOTTOM_PADDING = Math.max(0, 15 - yOffset); const BOTTOM_PADDING = Math.max(0, 15 - yOffset);
const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
const paddedHatPixels = []; 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 // Top padding
for (let y = 0; y < TOP_PADDING; y++) { for (let y = 0; y < top; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.fill(PALETTE.TRANSPARENT)
);
} }
// Left and right padding // Left and right padding
for (let y = 0; y < hatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
const row = []; const row = [];
for (let x = 0; x < LEFT_PADDING; x++) { for (let x = 0; x < left; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
for (let x = 0; x < pixels[y].length; x++) {
for (let x = 0; x < hatPixels[y].length; x++) { row.push(pixels[y][x]);
row.push(hatPixels[y][x]);
} }
for (let x = 0; x < right; x++) {
for (let x = 0; x < RIGHT_PADDING; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
paddedPixels.push(row);
paddedHatPixels.push(row);
} }
// Bottom padding // Bottom padding
for (let y = 0; y < BOTTOM_PADDING; y++) { for (let y = 0; y < bottom; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.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 = [ let neighborOffsets = [
[-1, 0], [-1, 0],
[1, 0], [1, 0],
@@ -764,21 +811,42 @@
if (outlineBottom) { if (outlineBottom) {
neighborOffsets.push([0, 1], [-1, 1], [1, 1]); neighborOffsets.push([0, 1], [-1, 1], [1, 1]);
} }
for (let y = 0; y < paddedHatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
for (let x = 0; x < paddedHatPixels[y].length; x++) { for (let x = 0; x < pixels[y].length; x++) {
const pixel = paddedHatPixels[y][x]; const pixel = pixels[y][x];
if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) {
for (let [dx, dy] of neighborOffsets) { for (let [dx, dy] of neighborOffsets) {
const newX = x + dx; const newX = x + dx;
const newY = y + dy; const newY = y + dy;
if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) {
paddedHatPixels[newY][newX] = PALETTE.BORDER; 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; 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 { .birb-window {
font-family: "Monocraft", monospace !important; font-family: "Monocraft", monospace !important;
line-height: initial !important; line-height: initial !important;
@@ -1955,6 +2033,7 @@
const FIELD_GUIDE_ID = "birb-field-guide"; const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather"; const FEATHER_ID = "birb-feather";
const WARDROBE_ID = "birb-wardrobe"; const WARDROBE_ID = "birb-wardrobe";
const HAT_ID = "birb-hat";
const DEFAULT_BIRD = "bluebird"; const DEFAULT_BIRD = "bluebird";
const DEFAULT_HAT = HAT.NONE; const DEFAULT_HAT = HAT.NONE;
@@ -2073,7 +2152,7 @@
insertModal(`${birdBirb()} Mode`, message); insertModal(`${birdBirb()} Mode`, message);
}), }),
new Separator(), 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"); const styleElement = document.createElement("style");
@@ -2262,6 +2341,9 @@
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); focusOnElement(true);
// TODO: This is for testing
generateHat();
} }
function update() { 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 * @param {string} birdType
*/ */
@@ -2773,14 +2896,9 @@
} }
/** /**
* Focus on an element within the viewport * @returns {HTMLElement|null} The random element, or null if no valid element was found
* @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) { function getRandomValidElement() {
if (frozen) {
return false;
}
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
@@ -2802,10 +2920,22 @@
} }
}); });
if (nonFixedElements.length === 0) { if (nonFixedElements.length === 0) {
return false; return null;
} }
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; 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); log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
@@ -2813,7 +2943,7 @@
} else { } else {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return randomElement !== null; return focusedElement !== null;
} }
/** /**

212
dist/web/birb.embed.js vendored
View File

@@ -629,6 +629,7 @@
FLOWER_HAT: "flower-hat" FLOWER_HAT: "flower-hat"
}; };
/** @type {{ [hatId: string]: { name: string, description: string } }} */
const HAT_METADATA = { const HAT_METADATA = {
[HAT.NONE]: { [HAT.NONE]: {
name: "Invisible Hat", name: "Invisible Hat",
@@ -684,62 +685,108 @@
} }
const index = i - 1; const index = i - 1;
const hatKey = HAT[hatName]; const hatKey = HAT[hatName];
const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); const hatLayer = buildHatLayer(spriteSheet, hatKey, index);
const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1);
hatLayers.base.push(hatLayer); hatLayers.base.push(hatLayer);
hatLayers.down.push(downHatLayer); hatLayers.down.push(downHatLayer);
} }
return hatLayers; 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[][]} spriteSheet
* @param {string} hatName * @param {string} hatName
* @param {number} hatIndex * @param {number} hatIndex
* @param {boolean} [outlineBottom=false]
* @param {number} [yOffset=0] * @param {number} [yOffset=0]
* @returns {Layer} * @returns {Layer}
*/ */
function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) {
const LEFT_PADDING = 6; const LEFT_PADDING = 6;
const RIGHT_PADDING = 14; const RIGHT_PADDING = 14;
const TOP_PADDING = 5 + yOffset; const TOP_PADDING = 5 + yOffset;
const BOTTOM_PADDING = Math.max(0, 15 - yOffset); const BOTTOM_PADDING = Math.max(0, 15 - yOffset);
const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
const paddedHatPixels = []; 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 // Top padding
for (let y = 0; y < TOP_PADDING; y++) { for (let y = 0; y < top; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.fill(PALETTE.TRANSPARENT)
);
} }
// Left and right padding // Left and right padding
for (let y = 0; y < hatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
const row = []; const row = [];
for (let x = 0; x < LEFT_PADDING; x++) { for (let x = 0; x < left; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
for (let x = 0; x < pixels[y].length; x++) {
for (let x = 0; x < hatPixels[y].length; x++) { row.push(pixels[y][x]);
row.push(hatPixels[y][x]);
} }
for (let x = 0; x < right; x++) {
for (let x = 0; x < RIGHT_PADDING; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
paddedPixels.push(row);
paddedHatPixels.push(row);
} }
// Bottom padding // Bottom padding
for (let y = 0; y < BOTTOM_PADDING; y++) { for (let y = 0; y < bottom; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.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 = [ let neighborOffsets = [
[-1, 0], [-1, 0],
[1, 0], [1, 0],
@@ -750,21 +797,42 @@
if (outlineBottom) { if (outlineBottom) {
neighborOffsets.push([0, 1], [-1, 1], [1, 1]); neighborOffsets.push([0, 1], [-1, 1], [1, 1]);
} }
for (let y = 0; y < paddedHatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
for (let x = 0; x < paddedHatPixels[y].length; x++) { for (let x = 0; x < pixels[y].length; x++) {
const pixel = paddedHatPixels[y][x]; const pixel = pixels[y][x];
if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) {
for (let [dx, dy] of neighborOffsets) { for (let [dx, dy] of neighborOffsets) {
const newX = x + dx; const newX = x + dx;
const newY = y + dy; const newY = y + dy;
if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) {
paddedHatPixels[newY][newX] = PALETTE.BORDER; 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; 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 { .birb-window {
font-family: "Monocraft", monospace !important; font-family: "Monocraft", monospace !important;
line-height: initial !important; line-height: initial !important;
@@ -1935,6 +2013,7 @@
const FIELD_GUIDE_ID = "birb-field-guide"; const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather"; const FEATHER_ID = "birb-feather";
const WARDROBE_ID = "birb-wardrobe"; const WARDROBE_ID = "birb-wardrobe";
const HAT_ID = "birb-hat";
const DEFAULT_BIRD = "bluebird"; const DEFAULT_BIRD = "bluebird";
const DEFAULT_HAT = HAT.NONE; const DEFAULT_HAT = HAT.NONE;
@@ -2053,7 +2132,7 @@
insertModal(`${birdBirb()} Mode`, message); insertModal(`${birdBirb()} Mode`, message);
}), }),
new Separator(), 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"); const styleElement = document.createElement("style");
@@ -2242,6 +2321,9 @@
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); focusOnElement(true);
// TODO: This is for testing
generateHat();
} }
function update() { 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 * @param {string} birdType
*/ */
@@ -2753,14 +2876,9 @@
} }
/** /**
* Focus on an element within the viewport * @returns {HTMLElement|null} The random element, or null if no valid element was found
* @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) { function getRandomValidElement() {
if (frozen) {
return false;
}
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
@@ -2782,10 +2900,22 @@
} }
}); });
if (nonFixedElements.length === 0) { if (nonFixedElements.length === 0) {
return false; return null;
} }
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; 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); log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
@@ -2793,7 +2923,7 @@
} else { } else {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return randomElement !== null; return focusedElement !== null;
} }
/** /**

212
dist/web/birb.js vendored
View File

@@ -629,6 +629,7 @@
FLOWER_HAT: "flower-hat" FLOWER_HAT: "flower-hat"
}; };
/** @type {{ [hatId: string]: { name: string, description: string } }} */
const HAT_METADATA = { const HAT_METADATA = {
[HAT.NONE]: { [HAT.NONE]: {
name: "Invisible Hat", name: "Invisible Hat",
@@ -684,62 +685,108 @@
} }
const index = i - 1; const index = i - 1;
const hatKey = HAT[hatName]; const hatKey = HAT[hatName];
const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); const hatLayer = buildHatLayer(spriteSheet, hatKey, index);
const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1);
hatLayers.base.push(hatLayer); hatLayers.base.push(hatLayer);
hatLayers.down.push(downHatLayer); hatLayers.down.push(downHatLayer);
} }
return hatLayers; 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[][]} spriteSheet
* @param {string} hatName * @param {string} hatName
* @param {number} hatIndex * @param {number} hatIndex
* @param {boolean} [outlineBottom=false]
* @param {number} [yOffset=0] * @param {number} [yOffset=0]
* @returns {Layer} * @returns {Layer}
*/ */
function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) {
const LEFT_PADDING = 6; const LEFT_PADDING = 6;
const RIGHT_PADDING = 14; const RIGHT_PADDING = 14;
const TOP_PADDING = 5 + yOffset; const TOP_PADDING = 5 + yOffset;
const BOTTOM_PADDING = Math.max(0, 15 - yOffset); const BOTTOM_PADDING = Math.max(0, 15 - yOffset);
const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
const paddedHatPixels = []; 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 // Top padding
for (let y = 0; y < TOP_PADDING; y++) { for (let y = 0; y < top; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.fill(PALETTE.TRANSPARENT)
);
} }
// Left and right padding // Left and right padding
for (let y = 0; y < hatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
const row = []; const row = [];
for (let x = 0; x < LEFT_PADDING; x++) { for (let x = 0; x < left; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
for (let x = 0; x < pixels[y].length; x++) {
for (let x = 0; x < hatPixels[y].length; x++) { row.push(pixels[y][x]);
row.push(hatPixels[y][x]);
} }
for (let x = 0; x < right; x++) {
for (let x = 0; x < RIGHT_PADDING; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
paddedPixels.push(row);
paddedHatPixels.push(row);
} }
// Bottom padding // Bottom padding
for (let y = 0; y < BOTTOM_PADDING; y++) { for (let y = 0; y < bottom; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.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 = [ let neighborOffsets = [
[-1, 0], [-1, 0],
[1, 0], [1, 0],
@@ -750,21 +797,42 @@
if (outlineBottom) { if (outlineBottom) {
neighborOffsets.push([0, 1], [-1, 1], [1, 1]); neighborOffsets.push([0, 1], [-1, 1], [1, 1]);
} }
for (let y = 0; y < paddedHatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
for (let x = 0; x < paddedHatPixels[y].length; x++) { for (let x = 0; x < pixels[y].length; x++) {
const pixel = paddedHatPixels[y][x]; const pixel = pixels[y][x];
if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) {
for (let [dx, dy] of neighborOffsets) { for (let [dx, dy] of neighborOffsets) {
const newX = x + dx; const newX = x + dx;
const newY = y + dy; const newY = y + dy;
if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) {
paddedHatPixels[newY][newX] = PALETTE.BORDER; 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; 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 { .birb-window {
font-family: "Monocraft", monospace !important; font-family: "Monocraft", monospace !important;
line-height: initial !important; line-height: initial !important;
@@ -1935,6 +2013,7 @@
const FIELD_GUIDE_ID = "birb-field-guide"; const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather"; const FEATHER_ID = "birb-feather";
const WARDROBE_ID = "birb-wardrobe"; const WARDROBE_ID = "birb-wardrobe";
const HAT_ID = "birb-hat";
const DEFAULT_BIRD = "bluebird"; const DEFAULT_BIRD = "bluebird";
const DEFAULT_HAT = HAT.NONE; const DEFAULT_HAT = HAT.NONE;
@@ -2053,7 +2132,7 @@
insertModal(`${birdBirb()} Mode`, message); insertModal(`${birdBirb()} Mode`, message);
}), }),
new Separator(), 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"); const styleElement = document.createElement("style");
@@ -2242,6 +2321,9 @@
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); focusOnElement(true);
// TODO: This is for testing
generateHat();
} }
function update() { 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 * @param {string} birdType
*/ */
@@ -2753,14 +2876,9 @@
} }
/** /**
* Focus on an element within the viewport * @returns {HTMLElement|null} The random element, or null if no valid element was found
* @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) { function getRandomValidElement() {
if (frozen) {
return false;
}
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
@@ -2782,10 +2900,22 @@
} }
}); });
if (nonFixedElements.length === 0) { if (nonFixedElements.length === 0) {
return false; return null;
} }
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; 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); log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
@@ -2793,7 +2923,7 @@
} else { } else {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return randomElement !== null; return focusedElement !== null;
} }
/** /**

View File

@@ -1,5 +1,5 @@
import Frame from './animation/frame.js'; 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 Anim from './animation/anim.js';
import { Birb, Animations } from './birb.js'; import { Birb, Animations } from './birb.js';
import { Birdsong } from './sound.js'; import { Birdsong } from './sound.js';
@@ -43,7 +43,7 @@ import {
switchMenuItems, switchMenuItems,
MENU_EXIT_ID MENU_EXIT_ID
} from './menu.js'; } 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 FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather"; const FEATHER_ID = "birb-feather";
const WARDROBE_ID = "birb-wardrobe"; const WARDROBE_ID = "birb-wardrobe";
const HAT_ID = "birb-hat";
const DEFAULT_BIRD = "bluebird"; const DEFAULT_BIRD = "bluebird";
const DEFAULT_HAT = HAT.NONE; const DEFAULT_HAT = HAT.NONE;
@@ -393,6 +394,9 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true); focusOnElement(true);
// TODO: This is for testing
generateHat();
} }
function update() { 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 * @param {string} birdType
*/ */
@@ -912,14 +957,9 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
} }
/** /**
* Focus on an element within the viewport * @returns {HTMLElement|null} The random element, or null if no valid element was found
* @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) { function getRandomValidElement() {
if (frozen) {
return false;
}
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
@@ -947,10 +987,22 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
return style.position !== "fixed" && style.position !== "sticky"; return style.position !== "fixed" && style.position !== "sticky";
}); });
if (nonFixedElements.length === 0) { if (nonFixedElements.length === 0) {
return false; return null;
} }
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; 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); log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
@@ -958,7 +1010,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
} else { } else {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return randomElement !== null; return focusedElement !== null;
} }
/** /**

View File

@@ -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 { PALETTE } from "./animation/sprites.js";
import { getLayerPixels } from "./shared.js"; import { getLayerPixels } from "./shared.js";
@@ -16,6 +18,7 @@ export const HAT = {
FLOWER_HAT: "flower-hat" FLOWER_HAT: "flower-hat"
}; };
/** @type {{ [hatId: string]: { name: string, description: string } }} */
export const HAT_METADATA = { export const HAT_METADATA = {
[HAT.NONE]: { [HAT.NONE]: {
name: "Invisible Hat", name: "Invisible Hat",
@@ -71,62 +74,108 @@ export function createHatLayers(spriteSheet) {
} }
const index = i - 1; const index = i - 1;
const hatKey = HAT[hatName]; const hatKey = HAT[hatName];
const hatLayer = buildHatLayer(spriteSheet, hatKey, index, false); const hatLayer = buildHatLayer(spriteSheet, hatKey, index);
const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, false, 1); const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1);
hatLayers.base.push(hatLayer); hatLayers.base.push(hatLayer);
hatLayers.down.push(downHatLayer); hatLayers.down.push(downHatLayer);
} }
return hatLayers; 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[][]} spriteSheet
* @param {string} hatName * @param {string} hatName
* @param {number} hatIndex * @param {number} hatIndex
* @param {boolean} [outlineBottom=false]
* @param {number} [yOffset=0] * @param {number} [yOffset=0]
* @returns {Layer} * @returns {Layer}
*/ */
function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yOffset = 0) { function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) {
const LEFT_PADDING = 6; const LEFT_PADDING = 6;
const RIGHT_PADDING = 14; const RIGHT_PADDING = 14;
const TOP_PADDING = 5 + yOffset; const TOP_PADDING = 5 + yOffset;
const BOTTOM_PADDING = Math.max(0, 15 - yOffset); const BOTTOM_PADDING = Math.max(0, 15 - yOffset);
const hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH); let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
const paddedHatPixels = []; 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 // Top padding
for (let y = 0; y < TOP_PADDING; y++) { for (let y = 0; y < top; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.fill(PALETTE.TRANSPARENT)
);
} }
// Left and right padding // Left and right padding
for (let y = 0; y < hatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
const row = []; const row = [];
for (let x = 0; x < LEFT_PADDING; x++) { for (let x = 0; x < left; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
for (let x = 0; x < pixels[y].length; x++) {
for (let x = 0; x < hatPixels[y].length; x++) { row.push(pixels[y][x]);
row.push(hatPixels[y][x]);
} }
for (let x = 0; x < right; x++) {
for (let x = 0; x < RIGHT_PADDING; x++) {
row.push(PALETTE.TRANSPARENT); row.push(PALETTE.TRANSPARENT);
} }
paddedPixels.push(row);
paddedHatPixels.push(row);
} }
// Bottom padding // Bottom padding
for (let y = 0; y < BOTTOM_PADDING; y++) { for (let y = 0; y < bottom; y++) {
paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
.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 = [ let neighborOffsets = [
[-1, 0], [-1, 0],
[1, 0], [1, 0],
@@ -137,19 +186,40 @@ function buildHatLayer(spriteSheet, hatName, hatIndex, outlineBottom = false, yO
if (outlineBottom) { if (outlineBottom) {
neighborOffsets.push([0, 1], [-1, 1], [1, 1]); neighborOffsets.push([0, 1], [-1, 1], [1, 1]);
} }
for (let y = 0; y < paddedHatPixels.length; y++) { for (let y = 0; y < pixels.length; y++) {
for (let x = 0; x < paddedHatPixels[y].length; x++) { for (let x = 0; x < pixels[y].length; x++) {
const pixel = paddedHatPixels[y][x]; const pixel = pixels[y][x];
if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) {
for (let [dx, dy] of neighborOffsets) { for (let [dx, dy] of neighborOffsets) {
const newX = x + dx; const newX = x + dx;
const newY = y + dy; const newY = y + dy;
if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) {
paddedHatPixels[newY][newX] = PALETTE.BORDER; 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;
} }

View File

@@ -41,6 +41,16 @@
z-index: 2147483630 !important; 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 { .birb-window {
font-family: "Monocraft", monospace !important; font-family: "Monocraft", monospace !important;
line-height: initial !important; line-height: initial !important;