mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-26 12:17:23 +00:00
Add hat item
This commit is contained in:
BIN
dist/extension.zip
vendored
BIN
dist/extension.zip
vendored
Binary file not shown.
212
dist/extension/birb.js
vendored
212
dist/extension/birb.js
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
2
dist/extension/manifest.json
vendored
2
dist/extension/manifest.json
vendored
@@ -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
214
dist/obsidian/main.js
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
2
dist/obsidian/manifest.json
vendored
2
dist/obsidian/manifest.json
vendored
@@ -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",
|
||||||
|
|||||||
214
dist/userscript/birb.user.js
vendored
214
dist/userscript/birb.user.js
vendored
@@ -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
212
dist/web/birb.embed.js
vendored
@@ -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
212
dist/web/birb.js
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
132
src/hats.js
132
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 { 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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user