19 Commits

Author SHA1 Message Date
Idrees Hassan
4b206f638d Reformat promise 2025-10-26 12:19:25 -04:00
Idrees Hassan
6124fcd969 Group sticky note functions together 2025-10-25 23:49:55 -04:00
Idrees Hassan
b9304fe11b Clean up code 2025-10-25 23:46:50 -04:00
Idrees Hassan
7f9c388853 Allow clicking after short delay following petting 2025-10-25 22:55:55 -04:00
Idrees Hassan
3aa0308746 Move comments around 2025-10-25 20:41:10 -04:00
Idrees Hassan
f45eb0ce61 Fix petting on mobile 2025-10-25 20:40:25 -04:00
Idrees Hassan
e55f0e7412 Remove PICO-8 and Spotify windows 2025-10-25 19:03:14 -04:00
Idrees Hassan
ff98390e10 Fix manifest 2025-10-25 19:01:04 -04:00
Idrees Hassan
f73e29a723 Correct padding on field guide 2025-10-25 18:29:41 -04:00
Idrees Hassan
bf1bb74219 Update README.md 2025-10-25 15:41:49 -04:00
Idrees Hassan
ca3bc5be4b Merge branch 'main' into extension 2025-10-25 15:33:01 -04:00
Idrees Hassan
29f1766a95 Switch to absolute positioning when on element 2025-10-23 23:32:01 -04:00
Idrees Hassan
5298b7801b Treat ground as yet another element 2025-10-23 21:25:43 -04:00
Idrees Hassan
bed3d37940 Bump version 2025-10-22 22:38:04 -04:00
Idrees Hassan
bae44cc98c Adjust startY to account for scrolling 2025-10-22 22:33:27 -04:00
Idrees Hassan
2767caeb84 Update userscript name 2025-10-22 22:03:07 -04:00
Idrees Hassan
270367139d Don't focus on ground immediately for mobile 2025-10-22 22:00:01 -04:00
Idrees Hassan
9fca0b2046 Bump version 2025-10-22 21:56:11 -04:00
Idrees Hassan
5860171184 Remove afk delay on mobile 2025-10-22 21:46:12 -04:00
9 changed files with 1057 additions and 1053 deletions

View File

@@ -1,8 +1,8 @@
# Browser Bird (Work in Progress!) # Pocket Bird (Work in Progress!)
This project is still being worked on, but if you wish to help me beta test it, join the [Discord](https://discord.gg/6yxE9prcNc) and follow the installation instructions below! This project is still being worked on, but if you wish to help me beta test it, join the [Discord](https://discord.gg/6yxE9prcNc) and follow the installation instructions below!
1. Install [Tampermonkey](https://www.tampermonkey.net/) on your web browser 1. Install [Tampermonkey](https://www.tampermonkey.net/) on your web browser
2. Enable the Tampermonkey extension and give it the permissions requested 2. Enable the Tampermonkey extension and give it the permissions requested
3. Install my Browser Bird script by going to this link and clicking install: [https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js](https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js) 3. Install my Pocket Bird script by going to this link and clicking install: [https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js](https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js)
4. Now any websites you visit will have a little bird hopping around! 4. Now any websites you visit will have a little bird hopping around!

593
birb.js
View File

@@ -1,6 +1,5 @@
// @ts-check // @ts-check
// @ts-ignore
const SHARED_CONFIG = { const SHARED_CONFIG = {
birbCssScale: 1, birbCssScale: 1,
uiCssScale: 1, uiCssScale: 1,
@@ -9,9 +8,8 @@ const SHARED_CONFIG = {
hopDistance: 45, hopDistance: 45,
}; };
const DESKTOP_CONFIG = { const DESKTOP_CONFIG = {
flySpeed: 0.2, flySpeed: 0.25
}; };
const MOBILE_CONFIG = { const MOBILE_CONFIG = {
@@ -28,17 +26,6 @@ const BIRB_CSS_SCALE = CONFIG.birbCssScale;
const UI_CSS_SCALE = CONFIG.uiCssScale; const UI_CSS_SCALE = CONFIG.uiCssScale;
const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize; const CANVAS_PIXEL_SIZE = CONFIG.canvasPixelSize;
const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE;
const HOP_SPEED = CONFIG.hopSpeed;
const FLY_SPEED = CONFIG.flySpeed;
const HOP_DISTANCE = CONFIG.hopDistance;
// Time in milliseconds until the user is considered AFK
const AFK_TIME = debugMode ? 0 : 1000 * 30;
const SPRITE_HEIGHT = 32;
const MENU_ID = "birb-menu";
const MENU_EXIT_ID = "birb-menu-exit";
const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather";
const DEFAULT_SETTINGS = { const DEFAULT_SETTINGS = {
birbMode: false birbMode: false
@@ -48,6 +35,23 @@ const DEFAULT_SETTINGS = {
* @typedef {typeof DEFAULT_SETTINGS} Settings * @typedef {typeof DEFAULT_SETTINGS} Settings
*/ */
/**
* @typedef {Object} SavedStickyNote
* @property {string} id
* @property {string} site
* @property {string} content
* @property {number} top
* @property {number} left
*/
/**
* @typedef {Object} BirbSaveData
* @property {string[]} unlockedSpecies
* @property {string} currentSpecies
* @property {Partial<Settings>} settings
* @property {SavedStickyNote[]} [stickyNotes]
*/
/** @type {Partial<Settings>} */ /** @type {Partial<Settings>} */
let userSettings = {}; let userSettings = {};
@@ -380,6 +384,7 @@ const Directions = {
}; };
const SPRITE_WIDTH = 32; const SPRITE_WIDTH = 32;
const SPRITE_HEIGHT = 32;
const DECORATIONS_SPRITE_WIDTH = 48; const DECORATIONS_SPRITE_WIDTH = 48;
const FEATHER_SPRITE_WIDTH = 32; const FEATHER_SPRITE_WIDTH = 32;
@@ -387,6 +392,34 @@ const SPRITE_SHEET = "__SPRITE_SHEET__";
const DECORATIONS_SPRITE_SHEET = "__DECORATIONS_SPRITE_SHEET__"; const DECORATIONS_SPRITE_SHEET = "__DECORATIONS_SPRITE_SHEET__";
const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__"; const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__";
const MENU_ID = "birb-menu";
const MENU_EXIT_ID = "birb-menu-exit";
const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather";
const HOP_SPEED = CONFIG.hopSpeed;
const FLY_SPEED = CONFIG.flySpeed;
const HOP_DISTANCE = CONFIG.hopDistance;
/** Speed at which the feather falls per tick */
const FEATHER_FALL_SPEED = 1;
/** Time in milliseconds until the user is considered AFK */
const AFK_TIME = debugMode ? 0 : 1000 * 30;
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
// Per-frame chances
const HOP_CHANCE = 1 / (60 * 3); // 3 seconds
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // 20 seconds
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 2 hours
/** Multiplier after petting that increases the feather drop chance */
const PET_FEATHER_BOOST = 2;
/** How long the pet boost lasts in milliseconds */
const PET_BOOST_DURATION = 1000 * 60 * 5;
const MIN_FOCUS_ELEMENT_WIDTH = 100;
const MIN_FOCUS_ELEMENT_TOP = 80;
/** Time between checking whether the URL has changed */
const URL_CHECK_INTERVAL = 500;
/** Time after petting before the menu can be opened */
const PET_MENU_COOLDOWN = 1000;
/** /**
* Load the sprite sheet and return the pixel-map template * Load the sprite sheet and return the pixel-map template
* @param {string} dataUri * @param {string} dataUri
@@ -443,8 +476,14 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
}); });
} }
// @ts-ignore log("Loading sprite sheets...");
Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false), loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)]).then(([birbPixels, decorationPixels, featherPixels]) => {
Promise.all([
loadSpriteSheetPixels(SPRITE_SHEET),
loadSpriteSheetPixels(DECORATIONS_SPRITE_SHEET, false),
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
]).then(([birbPixels, decorationPixels, featherPixels]) => {
const SPRITE_SHEET = birbPixels; const SPRITE_SHEET = birbPixels;
const DECORATIONS_SPRITE_SHEET = decorationPixels; const DECORATIONS_SPRITE_SHEET = decorationPixels;
const FEATHER_SPRITE_SHEET = featherPixels; const FEATHER_SPRITE_SHEET = featherPixels;
@@ -597,8 +636,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
const menuItems = [ const menuItems = [
new MenuItem(`Pet ${birdBirb()}`, pet), new MenuItem(`Pet ${birdBirb()}`, pet),
new MenuItem("Field Guide", insertFieldGuide), new MenuItem("Field Guide", insertFieldGuide),
// new MenuItem("Decorations", insertDecoration),
new DebugMenuItem("Applications", () => switchMenuItems(otherItems), false),
new MenuItem("Sticky Note", newStickyNote), new MenuItem("Sticky Note", newStickyNote),
new MenuItem(`Hide ${birdBirb()}`, hideBirb), new MenuItem(`Hide ${birdBirb()}`, hideBirb),
new DebugMenuItem("Freeze/Unfreeze", () => { new DebugMenuItem("Freeze/Unfreeze", () => {
@@ -627,39 +664,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
}) })
]; ];
const otherItems = [
new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
new Separator(),
new MenuItem("Video Games", () => switchMenuItems(gameItems), false),
new MenuItem("Utilities", () => switchMenuItems(utilityItems), false),
new MenuItem("Music Player", () => insertMusicPlayer(), false),
];
const gameItems = [
new MenuItem("Go Back", () => switchMenuItems(otherItems), false),
new Separator(),
new MenuItem("Pinball", () => insertPico8("Terra Nova Pinball", "terra_nova_pinball")),
new MenuItem("Pico Dino", () => insertPico8("Pico Dino", "picodino")),
new MenuItem("Woodworm", () => insertPico8("Woodworm", "woodworm")),
new MenuItem("Pico and Chill", () => insertPico8("Pico and Chill", "picochill")),
new MenuItem("Lani's Trek", () => insertPico8("Celeste 2 Lani's Trek", "celeste_classic_2")),
new MenuItem("Tetraminis", () => insertPico8("Tetraminis", "tetraminisdeffect")),
// new MenuItem("Pool", () => insertPico8("Pool", "mot_pool")),
];
const utilityItems = [
new MenuItem("Go Back", () => switchMenuItems(otherItems), false),
new Separator(),
new MenuItem("Pomodoro Timer", () => insertPico8("Pomodoro", "pomodorotimerv1")),
new MenuItem("Ohm Calculator", () => insertPico8("Resistor Calculator", "picoohm")),
new MenuItem("Wobblepaint ", () => insertPico8("Wobblepaint", "wobblepaint")),
];
const styleElement = document.createElement("style"); const styleElement = document.createElement("style");
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
/** @type {CanvasRenderingContext2D} */ /** @type {CanvasRenderingContext2D} */
// @ts-ignore // @ts-expect-error
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
const States = { const States = {
@@ -676,7 +685,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
let ticks = 0; let ticks = 0;
// Bird's current position // Bird's current position
let birdY = 0; let birdY = 0;
let birdX = 0; let birdX = 40;
// Bird's starting position (when flying) // Bird's starting position (when flying)
let startX = 0; let startX = 0;
let startY = 0; let startY = 0;
@@ -685,6 +694,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
let targetY = 0; let targetY = 0;
/** @type {HTMLElement|null} */ /** @type {HTMLElement|null} */
let focusedElement = null; let focusedElement = null;
let focusedBounds = { left: 0, right: 0, top: 0 };
let lastActionTimestamp = Date.now(); let lastActionTimestamp = Date.now();
/** @type {number[]} */ /** @type {number[]} */
let petStack = []; let petStack = [];
@@ -695,12 +705,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
/** @type {StickyNote[]} */ /** @type {StickyNote[]} */
let stickyNotes = []; let stickyNotes = [];
/** /**
* @returns {boolean} Whether the script is running in a userscript extension context * @returns {boolean} Whether the script is running in a userscript extension context
*/ */
function isUserScript() { function isUserScript() {
// @ts-ignore // @ts-expect-error
return typeof GM_getValue === "function"; return typeof GM_getValue === "function";
} }
@@ -713,9 +722,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function load() { function load() {
/** @type {Record<string, any>} */ /** @type {Record<string, any>} */
let saveData = {}; let saveData = {};
if (isUserScript()) { if (isUserScript()) {
log("Loading save data from UserScript storage"); log("Loading save data from UserScript storage");
// @ts-ignore // @ts-expect-error
saveData = GM_getValue("birbSaveData", {}) ?? {}; saveData = GM_getValue("birbSaveData", {}) ?? {};
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, loading save data from localStorage"); log("Test environment detected, loading save data from localStorage");
@@ -723,14 +733,18 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} else { } else {
log("Not a UserScript"); log("Not a UserScript");
} }
debug("Loaded data: " + JSON.stringify(saveData)); debug("Loaded data: " + JSON.stringify(saveData));
if (!saveData.settings) { if (!saveData.settings) {
log("No user settings found in save data, starting fresh"); log("No user settings found in save data, starting fresh");
} }
userSettings = saveData.settings ?? {}; userSettings = saveData.settings ?? {};
unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD];
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
stickyNotes = []; stickyNotes = [];
if (saveData.stickyNotes) { if (saveData.stickyNotes) {
for (let note of saveData.stickyNotes) { for (let note of saveData.stickyNotes) {
if (note.id) { if (note.id) {
@@ -738,17 +752,19 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
} }
} }
log(stickyNotes.length + " sticky notes loaded"); log(stickyNotes.length + " sticky notes loaded");
switchSpecies(currentSpecies); switchSpecies(currentSpecies);
} }
function save() { function save() {
/** @type {Record<string, any>} */ /** @type {BirbSaveData} */
let saveData = { const saveData = {
unlockedSpecies: unlockedSpecies, unlockedSpecies,
currentSpecies: currentSpecies, currentSpecies,
settings: userSettings settings: userSettings
}; };
if (stickyNotes.length > 0) { if (stickyNotes.length > 0) {
saveData.stickyNotes = stickyNotes.map(note => ({ saveData.stickyNotes = stickyNotes.map(note => ({
id: note.id, id: note.id,
@@ -758,9 +774,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
left: note.left left: note.left
})); }));
} }
if (isUserScript()) { if (isUserScript()) {
log("Saving data to UserScript storage"); log("Saving data to UserScript storage");
// @ts-ignore // @ts-expect-error
GM_setValue("birbSaveData", saveData); GM_setValue("birbSaveData", saveData);
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, saving data to localStorage"); log("Test environment detected, saving data to localStorage");
@@ -773,7 +790,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function resetSaveData() { function resetSaveData() {
if (isUserScript()) { if (isUserScript()) {
log("Resetting save data in UserScript storage"); log("Resetting save data in UserScript storage");
// @ts-ignore // @ts-expect-error
GM_deleteValue("birbSaveData"); GM_deleteValue("birbSaveData");
} else if (isTestEnvironment()) { } else if (isTestEnvironment()) {
log("Test environment detected, resetting save data in localStorage"); log("Test environment detected, resetting save data in localStorage");
@@ -802,8 +819,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function init() { function init() {
if (window !== window.top) { if (window !== window.top) {
// Skip installation if within an iframe // Skip installation if within an iframe
log("In iframe, skipping Birb script initialization");
return; return;
} }
log("Sprite sheets loaded successfully, initializing bird...");
// Preload font // Preload font
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf"; const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
@@ -835,20 +854,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas); document.body.appendChild(canvas);
/** @type {NodeJS.Timeout} */
let scrollTimeout;
window.addEventListener("scroll", () => { window.addEventListener("scroll", () => {
// TODO: Only do this if focused on the ground
if (focusedElement === null && currentState !== States.FLYING) {
canvas.style.transition = "opacity 0.2s";
canvas.style.opacity = "0";
}
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
canvas.style.transition = "opacity 0.4s";
canvas.style.opacity = "1";
}, 100);
}); });
onClick(document, (e) => { onClick(document, (e) => {
@@ -859,6 +866,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
}); });
onClick(canvas, () => { onClick(canvas, () => {
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
// Currently being pet, don't open menu
return;
}
insertMenu(); insertMenu();
}); });
@@ -871,13 +882,17 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
const pets = petStack.filter((time) => Date.now() - time < 1000).length; const pets = petStack.filter((time) => Date.now() - time < 1000).length;
if (pets >= 3) { if (pets >= 3) {
setAnimation(Animations.HEART); pet();
// Clear the stack // Clear the stack
petStack = []; petStack = [];
} }
} }
}); });
canvas.addEventListener("touchmove", (e) => {
pet();
});
drawStickyNotes(); drawStickyNotes();
let lastUrl = (window.location.href ?? "").split("?")[0]; let lastUrl = (window.location.href ?? "").split("?")[0];
@@ -888,31 +903,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
lastUrl = currentUrl; lastUrl = currentUrl;
drawStickyNotes(); drawStickyNotes();
} }
}, 500); }, URL_CHECK_INTERVAL);
setInterval(update, 1000 / 60); setInterval(update, UPDATE_INTERVAL);
birdY = getWindowBottom();
// TODO: For testing only
hop();
} }
function drawStickyNotes() {
// Remove all existing sticky notes
const existingNotes = document.querySelectorAll(".birb-sticky-note");
existingNotes.forEach(note => note.remove());
// Render all sticky notes
for (let stickyNote of stickyNotes) {
if (isStickyNoteApplicable(stickyNote)) {
renderStickyNote(stickyNote);
}
}
}
/**
* Run the bird's behavior logic
*/
function update() { function update() {
ticks++; ticks++;
@@ -922,69 +917,65 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
// Won't be restored on fullscreen exit // Won't be restored on fullscreen exit
} }
if (currentState === States.IDLE) { if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART && !isMenuOpen()) { if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop(); hop();
} else if (Math.random() < 1 / (60 * 20) && Date.now() - lastActionTimestamp > AFK_TIME && !isMenuOpen()) { } else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
if (focusedElement === null) {
// Fly to an element
focusOnElement();
lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while
focusOnElement(); focusOnElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} }
} }
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // 1 every 2 hours (ticks * seconds * minutes * hours)
// Double the chance of a feather if recently pet
let petMod = Date.now() - lastPetTimestamp < 1000 * 60 * 5 ? 2 : 1;
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
lastPetTimestamp = 0;
activateFeather();
}
updateFeather();
}
/**
* Render the bird in the dom and update its position if necessary
*/
function draw() {
requestAnimationFrame(draw);
if (!visible) {
return;
}
// Update the bird's position
if (currentState === States.IDLE) {
if (focusedElement !== null) {
birdY = getFocusedElementY();
if (!isWithinHorizontalBounds(birdX)) {
focusOnGround();
}
} else {
// Ground the bird
birdY = getWindowBottom();
}
} else if (currentState === States.FLYING) {
// Fly to target location (even if in the air)
if (updateParabolicPath(FLY_SPEED)) {
setState(States.IDLE);
}
} else if (currentState === States.HOP) { } else if (currentState === States.HOP) {
if (updateParabolicPath(HOP_SPEED)) { if (updateParabolicPath(HOP_SPEED)) {
setState(States.IDLE); setState(States.IDLE);
} }
} }
if (focusedElement === null) { // Double the chance of a feather if recently pet
if (Date.now() - lastActionTimestamp > AFK_TIME && !isMenuOpen()) { const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
// Fly to an element if the user is AFK if (visible && Math.random() < FEATHER_CHANCE * petMod) {
// focusOnElement(); lastPetTimestamp = 0;
// lastActionTimestamp = Date.now(); activateFeather();
} }
} else if (focusedElement !== null) { updateFeather();
targetY = getFocusedElementY(); }
if (targetY < window.scrollY || targetY > window.scrollY + window.innerHeight) {
// Fly to ground if the focused element moves out of bounds function draw() {
requestAnimationFrame(draw);
if (!visible) {
return;
}
updateFocusedElementBounds();
// Update the bird's position
if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) {
focusOnGround(); focusOnGround();
} }
birdY = getFocusedY();
} else if (currentState === States.FLYING) {
// Fly to target location (even if in the air)
if (updateParabolicPath(FLY_SPEED)) {
setState(States.IDLE);
}
}
const oldTargetY = targetY;
targetY = getFocusedY();
// Adjust startY to account for scrolling
startY += targetY - oldTargetY;
if (targetY < 0 || targetY > window.innerHeight) {
// Fly to ground if the focused element moves out of bounds
focusOnGround();
} }
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -997,9 +988,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
setY(birdY); setY(birdY);
} }
init();
draw();
function newStickyNote() { function newStickyNote() {
const id = Date.now().toString(); const id = Date.now().toString();
const site = window.location.href; const site = window.location.href;
@@ -1100,17 +1088,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return false; return false;
} }
/** @type {Record<string, string>} */ const stickyNoteParams = parseUrlParams(stickyNoteUrl);
const stickyNoteParams = stickyNoteUrl.split("?")[1]?.split("&").reduce((params, param) => { const currentParams = parseUrlParams(currentUrl);
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
/** @type {Record<string, string>} */
const currentParams = currentUrl.split("?")[1]?.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
debug("Comparing params: ", stickyNoteParams, currentParams); debug("Comparing params: ", stickyNoteParams, currentParams);
@@ -1122,6 +1101,18 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return true; return true;
} }
function drawStickyNotes() {
// Remove all existing sticky notes
const existingNotes = document.querySelectorAll(".birb-sticky-note");
existingNotes.forEach(note => note.remove());
// Render all sticky notes
for (let stickyNote of stickyNotes) {
if (isStickyNoteApplicable(stickyNote)) {
renderStickyNote(stickyNote);
}
}
}
/** /**
* Create an HTML element with the specified parameters * Create an HTML element with the specified parameters
* @param {string} className * @param {string} className
@@ -1141,6 +1132,42 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return element; return element;
} }
/**
* Create a window element with header and content
* @param {string} id
* @param {string} title
* @param {string} contentHtml
* @param {() => void} [onClose]
* @returns {HTMLElement}
*/
function createWindow(id, title, contentHtml, onClose) {
const window = makeElement("birb-window", undefined, id);
window.innerHTML = `
<div class="birb-window-header">
<div class="birb-window-title">${title}</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
${contentHtml}
</div>
`;
document.body.appendChild(window);
makeDraggable(window.querySelector(".birb-window-header"));
const closeButton = window.querySelector(".birb-window-close");
if (closeButton) {
makeClosable(() => {
if (onClose) {
onClose();
}
window.remove();
}, closeButton);
}
return window;
}
function insertDecoration() { function insertDecoration() {
// Create a canvas element for the decoration // Create a canvas element for the decoration
const decorationCanvas = document.createElement("canvas"); const decorationCanvas = document.createElement("canvas");
@@ -1220,21 +1247,16 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function updateFeather() { function updateFeather() {
const feather = document.querySelector("#birb-feather"); const feather = document.querySelector("#birb-feather");
const featherGravity = 1;
if (!feather || !(feather instanceof HTMLElement)) { if (!feather || !(feather instanceof HTMLElement)) {
return; return;
} }
const y = parseInt(feather.style.top || "0") + featherGravity; const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED;
feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`; feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`;
if (y < window.innerHeight - feather.offsetHeight) { if (y < window.innerHeight - feather.offsetHeight) {
feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`;
} }
} }
// insertDecoration();
// insertFieldGuide();
/** /**
* @param {HTMLElement} element * @param {HTMLElement} element
*/ */
@@ -1251,28 +1273,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
if (document.querySelector("#" + FIELD_GUIDE_ID)) { if (document.querySelector("#" + FIELD_GUIDE_ID)) {
return; return;
} }
let html = `
<div class="birb-window-header"> const modal = createWindow("birb-modal", title, `
<div class="birb-window-title">${title}</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
<div class="birb-message-content"> <div class="birb-message-content">
${message} ${message}
</div> </div>
</div>` `);
const modal = makeElement("birb-window");
modal.style.width = "270px";
modal.innerHTML = html;
document.body.appendChild(modal);
makeDraggable(modal.querySelector(".birb-window-header"));
const closeButton = modal.querySelector(".birb-window-close"); modal.style.width = "270px";
if (closeButton) {
makeClosable(() => {
modal.remove();
}, closeButton);
}
centerElement(modal); centerElement(modal);
} }
@@ -1381,60 +1389,10 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
} }
// insertPico8();
function isSafari() { function isSafari() {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
} }
/**
* @param {string} name
* @param {string} pid
*/
function insertPico8(name, pid) {
let html = `
<div class="birb-window-header">
<div class="birb-window-title">${name}</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content birb-pico-8-content">
<iframe src="https://www.lexaloffle.com/bbs/widget.php?pid=${pid}" scrolling='${isSafari() ? "yes" : "no"}'></iframe>
</div>`
const pico8 = makeElement("birb-window");
pico8.innerHTML = html;
document.body.appendChild(pico8);
makeDraggable(pico8.querySelector(".birb-window-header"));
const close = pico8.querySelector(".birb-window-close");
if (close) {
makeClosable(() => {
pico8.remove();
}, close);
}
centerElement(pico8);
}
function insertMusicPlayer() {
let html = `
<div class="birb-window-header">
<div class="birb-window-title">Music Player</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content birb-music-player-content">
<iframe style="border-radius:12px" src="https://open.spotify.com/embed/playlist/31FWVQBp3WQydWLNhO0ACi?utm_source=generator" width="250" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
</div>`;
const pico8 = makeElement("birb-window");
pico8.innerHTML = html;
document.body.appendChild(pico8);
makeDraggable(pico8.querySelector(".birb-window-header"));
const close = pico8.querySelector(".birb-window-close");
if (close) {
makeClosable(() => {
pico8.remove();
}, close);
}
centerElement(pico8);
}
/** /**
* @param {string} type * @param {string} type
*/ */
@@ -1548,7 +1506,23 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
*/ */
function onClick(element, action) { function onClick(element, action) {
element.addEventListener("click", (e) => action(e)); element.addEventListener("click", (e) => action(e));
element.addEventListener("touchstart", (e) => action(e)); element.addEventListener("touchend", (e) => {
if (e instanceof TouchEvent === false) {
return;
} else if (element instanceof HTMLElement === false) {
return;
}
const touch = e.changedTouches[0];
const rect = element.getBoundingClientRect();
if (
touch.clientX >= rect.left &&
touch.clientX <= rect.right &&
touch.clientY >= rect.top &&
touch.clientY <= rect.bottom
) {
action(e);
}
});
} }
/** /**
@@ -1682,63 +1656,70 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
function getFocusedElementRandomX() { function getFocusedElementRandomX() {
if (focusedElement === null) { return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left;
return Math.random() * window.innerWidth;
}
const rect = focusedElement.getBoundingClientRect();
return Math.random() * (rect.right - rect.left) + rect.left + window.scrollX;
} }
function getFocusedElementY() { function isWithinHorizontalBounds() {
if (focusedElement === null) { return birdX >= focusedBounds.left && birdX <= focusedBounds.right;
return getWindowBottom();
} }
const rect = focusedElement.getBoundingClientRect();
return rect.top + window.scrollY; function getFocusedY() {
return getFullWindowHeight() - focusedBounds.top;
} }
/** /**
* @param {number} x * @returns The render-safe height of the inner browser window
* @returns {boolean} Whether the x coordinate is within the horizontal bounds of the focused element
*/ */
function isWithinHorizontalBounds(x) { function getSafeWindowHeight() {
if (focusedElement === null) { // Necessary because iOS 26 Safari is terrible and won't render
return true; // fixed elements behind the address bar
return window.innerHeight;
} }
const rect = focusedElement.getBoundingClientRect();
return x >= rect.left && x <= rect.right; /**
* @returns The true height of the inner browser window
*/
function getFullWindowHeight() {
return document.documentElement.clientHeight;
} }
function focusOnGround() { function focusOnGround() {
if (focusedElement === null) {
// Already focused on ground
return;
}
console.log("Focusing on ground"); console.log("Focusing on ground");
focusedElement = null; focusedElement = null;
flyTo(Math.random() * window.innerWidth, getWindowBottom()); focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() };
flyTo(Math.random() * window.innerWidth, 0);
} }
function focusOnElement() { function focusOnElement() {
if (frozen) { if (frozen) {
return; return;
} }
console.log("Focusing on element");
const elements = document.querySelectorAll("img, video"); const elements = document.querySelectorAll("img, video");
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
const rect = img.getBoundingClientRect(); const rect = img.getBoundingClientRect();
return rect.left >= 0 && rect.top >= 80 && rect.right <= window.innerWidth && rect.top <= window.innerHeight; return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight;
}); });
const MIN_WIDTH = 100;
/** @type {HTMLElement[]} */ /** @type {HTMLElement[]} */
// @ts-ignore // @ts-expect-error
const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_WIDTH); const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
if (largeElements.length === 0) { if (largeElements.length === 0) {
return; return;
} }
const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)]; const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)];
focusedElement = randomElement; focusedElement = randomElement;
flyTo(getFocusedElementRandomX(), getFocusedElementY()); log("Focusing on element: ", focusedElement);
updateFocusedElementBounds();
flyTo(getFocusedElementRandomX(), getFocusedY());
}
function updateFocusedElementBounds() {
if (focusedElement === null) {
// Update ground location to bottom of window
focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() };
return;
}
const { left, right, top } = focusedElement.getBoundingClientRect();
focusedBounds = { left, right, top };
} }
function getCanvasWidth() { function getCanvasWidth() {
@@ -1749,40 +1730,24 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return canvas.height * BIRB_CSS_SCALE return canvas.height * BIRB_CSS_SCALE
} }
function getWindowBottom() {
return window.scrollY + window.innerHeight;
}
function hop() { function hop() {
if (frozen) { if (frozen) {
return; return;
} }
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
// Determine bounds for hopping
let minX = 0;
let maxX = window.innerWidth;
let y = getWindowBottom();
if (focusedElement !== null) {
// Hop on the element
const rect = focusedElement.getBoundingClientRect();
minX = rect.left;
maxX = rect.right;
y = window.innerHeight - rect.top;
}
setState(States.HOP); setState(States.HOP);
setAnimation(Animations.FLYING); setAnimation(Animations.FLYING);
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > minX) || birdX + HOP_DISTANCE > maxX) { if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
targetX = birdX - HOP_DISTANCE; targetX = birdX - HOP_DISTANCE;
} else { } else {
targetX = birdX + HOP_DISTANCE; targetX = birdX + HOP_DISTANCE;
} }
targetY = y; targetY = getFocusedY();
console.log("hopping from", birdX, birdY, "to", targetX, targetY);
} }
} }
function pet() { function pet() {
if (currentState === States.IDLE) { if (currentState === States.IDLE && currentAnimation !== Animations.HEART) {
setAnimation(Animations.HEART); setAnimation(Animations.HEART);
lastPetTimestamp = Date.now(); lastPetTimestamp = Date.now();
} }
@@ -1798,13 +1763,19 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
* @param {number} y * @param {number} y
*/ */
function flyTo(x, y) { function flyTo(x, y) {
console.log("Flying to", x, y);
targetX = x; targetX = x;
targetY = y; targetY = y;
setState(States.FLYING); setState(States.FLYING);
setAnimation(Animations.FLYING); setAnimation(Animations.FLYING);
} }
/**
* @returns {boolean} Whether the bird should be absolutely positioned
*/
function isAbsolute() {
return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP);
}
/** /**
* Set the current animation and reset the animation timer * Set the current animation and reset the animation timer
* @param {Anim} animation * @param {Anim} animation
@@ -1819,7 +1790,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
* @param {string} state * @param {string} state
*/ */
function setState(state) { function setState(state) {
console.log("State:", state);
stateStart = Date.now(); stateStart = Date.now();
startX = birdX; startX = birdX;
startY = birdY; startY = birdY;
@@ -1827,38 +1797,40 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
if (state === States.IDLE) { if (state === States.IDLE) {
setAnimation(Animations.BOB); setAnimation(Animations.BOB);
} }
if (isAbsolute()) {
canvas.classList.add("birb-absolute");
} else {
canvas.classList.remove("birb-absolute");
}
setY(birdY);
} }
/** /**
* Set the bird element's X position, with the element origin at the center of the bird
* @param {number} x * @param {number} x
*/ */
function setX(x) { function setX(x) {
const mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2)); let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2));
canvas.style.left = `${x + mod}px`; canvas.style.left = `${x + mod}px`;
} }
/** /**
* Set the bird element's Y position, with the element origin at the bottom of the bird
* @param {number} y * @param {number} y
*/ */
function setY(y) { function setY(y) {
const mod = getCanvasHeight() + WINDOW_PIXEL_SIZE; let bottom;
canvas.style.top = `${y - mod}px`; if (isAbsolute()) {
// Position is absolute, convert from fixed
bottom = y - window.scrollY;
} else {
// Position is fixed
bottom = y;
}
canvas.style.bottom = `${bottom}px`;
} }
});
/** // Helper functions
* @param {number} start
* @param {number} end
* @param {number} amount
* @returns {number}
*/
function linearLerp(start, end, amount) {
return start + (end - start) * amount;
}
/** /**
* @param {number} startX * @param {number} startX
* @param {number} startY * @param {number} startY
* @param {number} endX * @param {number} endX
@@ -1867,25 +1839,40 @@ function linearLerp(start, end, amount) {
* @param {number} [intensity] * @param {number} [intensity]
* @returns {{x: number, y: number}} * @returns {{x: number, y: number}}
*/ */
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) { function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX; const dx = endX - startX;
const dy = endY - startY; const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy); const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx); const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2; const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 - distance / 4 * intensity; const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount; const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX; const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY; const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y }; return { x, y };
} }
/** /**
* @param {number} value * Parse URL parameters into a key-value map
* @param {string} url
* @returns {Record<string, string>}
*/ */
function roundToPixel(value) { function parseUrlParams(url) {
return Math.round(value / WINDOW_PIXEL_SIZE) * WINDOW_PIXEL_SIZE; const queryString = url.split("?")[1];
} if (!queryString) return {};
return queryString.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
}
// Run the birb
init();
draw();
}).catch((e) => {
error("Error while loading sprite sheets: ", e);
});
/** /**
* @returns {boolean} Whether the user is on a mobile device * @returns {boolean} Whether the user is on a mobile device

View File

@@ -20,27 +20,48 @@ const spriteSheets = [
const STYLESHEET_PATH = "./stylesheet.css"; const STYLESHEET_PATH = "./stylesheet.css";
const STYLESHEET_KEY = "___STYLESHEET___"; const STYLESHEET_KEY = "___STYLESHEET___";
let version = "0.0.0"; const now = new Date();
// Try to read version from manifest.json const versionDate = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`;
// Get current build number from manifest.json
let buildNumber = 0;
try { try {
const manifest = JSON.parse(readFileSync('manifest.json', 'utf8')); const manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
if (manifest.version) { if (manifest.version) {
version = manifest.version; if (manifest.version.startsWith(versionDate)) {
// Same day, increment build number
const parts = manifest.version.split('.');
if (parts.length === 4) {
buildNumber = parseInt(parts[3], 10) + 1;
}
}
} }
} catch (e) { } catch (e) {
console.error("Could not read version from manifest.json"); console.error("Could not read version from manifest.json");
throw e; throw e;
} }
// Update manifest.json with new version
const version = `${versionDate}.${buildNumber}`;
try {
const manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
manifest.version = version;
writeFileSync('manifest.json', JSON.stringify(manifest, null, 4), 'utf8');
} catch (e) {
console.error("Could not update version in manifest.json");
throw e;
}
const userScriptHeader = const userScriptHeader =
`// ==UserScript== `// ==UserScript==
// @name Browser Bird // @name Pocket Bird
// @namespace https://idreesinc.com // @namespace https://idreesinc.com
// @version ${version} // @version ${version}
// @description birb // @description birb
// @author Idrees // @author Idrees
// @downloadURL https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
// @updateURL https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js // @updateURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
// @match *://*/* // @match *://*/*
// @grant GM_setValue // @grant GM_setValue
// @grant GM_getValue // @grant GM_getValue

610
dist/birb.js vendored

File diff suppressed because it is too large Load Diff

618
dist/birb.user.js vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,17 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Browser Bird", "name": "Pocket Bird",
"description": "It's a bird, in your browser. What more could you want?", "description": "It's a bird, in your browser. What more could you want?",
"version": "2025.9.16.1", "version": "2025.10.25.130",
"homepage_url": "https://idreesinc.com", "homepage_url": "https://idreesinc.com",
"content_scripts": [ "content_scripts": [
{ {
"matches": ["<all_urls>"], "matches": [
"js": ["birb.js"] "<all_urls>"
],
"js": [
"./dist/birb.js"
]
} }
], ],
"permissions": [ "permissions": [
@@ -16,8 +20,12 @@
], ],
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"resources": ["images/*"], "resources": [
"matches": ["<all_urls>"] "images/*"
],
"matches": [
"<all_urls>"
]
} }
], ],
"browser_specific_settings": { "browser_specific_settings": {
@@ -26,4 +34,3 @@
} }
} }
} }

View File

@@ -7,7 +7,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "node build.js", "build": "node build.js",
"dev": "nodemon --watch birb.js --watch stylesheet.css --watch build.js --watch manifest.json --exec \"npm run build\"" "dev": "nodemon --watch birb.js --watch stylesheet.css --watch build.js --exec \"npm run build\""
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"

View File

@@ -12,7 +12,7 @@
} }
#spacer { #spacer {
height: 200vh; height: 100vh;
} }
</style> </style>
</head> </head>

View File

@@ -13,13 +13,18 @@
#birb { #birb {
image-rendering: pixelated; image-rendering: pixelated;
position: absolute; position: fixed;
bottom: 0;
transform: scale(var(--birb-scale)) !important; transform: scale(var(--birb-scale)) !important;
transform-origin: bottom; transform-origin: bottom;
z-index: 2147483638 !important; z-index: 2147483638 !important;
cursor: pointer; cursor: pointer;
} }
.birb-absolute {
position: absolute !important;
}
.birb-decoration { .birb-decoration {
image-rendering: pixelated; image-rendering: pixelated;
position: fixed; position: fixed;
@@ -236,8 +241,8 @@
flex-direction: row; flex-direction: row;
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
padding-left: 15px; padding-left: 10px;
padding-right: 15px; padding-right: 10px;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -284,12 +289,12 @@
} }
.birb-field-guide-description { .birb-field-guide-description {
width: calc(100% - 16px); width: calc(100% - 20px);
margin-top: 10px; margin-top: 5px;
padding: 8px; padding: 8px;
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
margin-bottom: 6px; margin-bottom: 10px;
font-size: 14px; font-size: 14px;
box-sizing: border-box; box-sizing: border-box;
color: rgb(124, 108, 75); color: rgb(124, 108, 75);