Merge pull request #1 from IdreesInc/extension

Add extension manifest
This commit is contained in:
Idrees
2025-10-26 12:36:24 -04:00
committed by GitHub
7 changed files with 1174 additions and 1102 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!

723
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 = {
@@ -700,20 +709,23 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
* @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";
} }
function isTestEnvironment() { function isTestEnvironment() {
return window.location.hostname === "127.0.0.1"; return window.location.hostname === "127.0.0.1"
|| window.location.hostname === "localhost"
|| window.location.hostname.startsWith("192.168.");
} }
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");
@@ -721,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) {
@@ -736,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,
@@ -756,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");
@@ -771,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");
@@ -797,6 +816,178 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return settings().birbMode ? "Birb" : "Bird"; return settings().birbMode ? "Birb" : "Bird";
} }
function init() {
if (window !== window.top) {
// Skip installation if within an iframe
log("In iframe, skipping Birb script initialization");
return;
}
log("Sprite sheets loaded successfully, initializing bird...");
// Preload font
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
const fontLink = document.createElement("link");
fontLink.rel = "stylesheet";
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
document.head.appendChild(fontLink);
// Add stylesheet font-face
const fontFace = `
@font-face {
font-family: 'Monocraft';
src: url(${MONOCRAFT_SRC}) format('opentype');
font-weight: normal;
font-style: normal;
}
`;
const fontStyle = document.createElement("style");
fontStyle.innerHTML = fontFace;
document.head.appendChild(fontStyle);
load();
styleElement.innerHTML = STYLESHEET;
document.head.appendChild(styleElement);
canvas.id = "birb";
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas);
window.addEventListener("scroll", () => {
lastActionTimestamp = Date.now();
});
onClick(document, (e) => {
lastActionTimestamp = Date.now();
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
removeMenu();
}
});
onClick(canvas, () => {
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
// Currently being pet, don't open menu
return;
}
insertMenu();
});
canvas.addEventListener("mouseover", () => {
lastActionTimestamp = Date.now();
if (currentState === States.IDLE) {
petStack.push(Date.now());
if (petStack.length > 10) {
petStack.shift();
}
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
if (pets >= 3) {
pet();
// Clear the stack
petStack = [];
}
}
});
canvas.addEventListener("touchmove", (e) => {
pet();
});
drawStickyNotes();
let lastUrl = (window.location.href ?? "").split("?")[0];
setInterval(() => {
const currentUrl = (window.location.href ?? "").split("?")[0];
if (currentUrl !== lastUrl) {
log("URL changed, updating sticky notes");
lastUrl = currentUrl;
drawStickyNotes();
}
}, URL_CHECK_INTERVAL);
setInterval(update, UPDATE_INTERVAL);
}
function update() {
ticks++;
// Hide bird if the browser is fullscreen
if (document.fullscreenElement) {
hideBirb();
// Won't be restored on fullscreen exit
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
if (focusedElement === null) {
// Fly to an element
focusOnElement();
lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while
focusOnElement();
lastActionTimestamp = Date.now();
}
}
} else if (currentState === States.HOP) {
if (updateParabolicPath(HOP_SPEED)) {
setState(States.IDLE);
}
}
// Double the chance of a feather if recently pet
const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
lastPetTimestamp = 0;
activateFeather();
}
updateFeather();
}
function draw() {
requestAnimationFrame(draw);
if (!visible) {
return;
}
updateFocusedElementBounds();
// Update the bird's position
if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) {
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);
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
setAnimation(Animations.STILL);
}
// Update HTML element position
setX(birdX);
setY(birdY);
}
function newStickyNote() { function newStickyNote() {
const id = Date.now().toString(); const id = Date.now().toString();
const site = window.location.href; const site = window.location.href;
@@ -897,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);
@@ -919,88 +1101,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return true; return true;
} }
function init() {
if (window !== window.top) {
// Skip installation if within an iframe
return;
}
// Preload font
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
const fontLink = document.createElement("link");
fontLink.rel = "stylesheet";
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
document.head.appendChild(fontLink);
// Add stylesheet font-face
const fontFace = `
@font-face {
font-family: 'Monocraft';
src: url(${MONOCRAFT_SRC}) format('opentype');
font-weight: normal;
font-style: normal;
}
`;
const fontStyle = document.createElement("style");
fontStyle.innerHTML = fontFace;
document.head.appendChild(fontStyle);
load();
styleElement.innerHTML = STYLESHEET;
document.head.appendChild(styleElement);
canvas.id = "birb";
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas);
window.addEventListener("scroll", () => {
lastActionTimestamp = Date.now();
});
onClick(document, (e) => {
lastActionTimestamp = Date.now();
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
removeMenu();
}
});
onClick(canvas, () => {
insertMenu();
});
canvas.addEventListener("mouseover", () => {
lastActionTimestamp = Date.now();
if (currentState === States.IDLE) {
petStack.push(Date.now());
if (petStack.length > 10) {
petStack.shift();
}
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
if (pets >= 3) {
setAnimation(Animations.HEART);
// Clear the stack
petStack = [];
}
}
});
drawStickyNotes();
let lastUrl = (window.location.href ?? "").split("?")[0];
setInterval(() => {
const currentUrl = (window.location.href ?? "").split("?")[0];
if (currentUrl !== lastUrl) {
log("URL changed, updating sticky notes");
lastUrl = currentUrl;
drawStickyNotes();
}
}, 500);
setInterval(update, 1000 / 60);
}
function drawStickyNotes() { function drawStickyNotes() {
// Remove all existing sticky notes // Remove all existing sticky notes
const existingNotes = document.querySelectorAll(".birb-sticky-note"); const existingNotes = document.querySelectorAll(".birb-sticky-note");
@@ -1013,89 +1113,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
} }
function update() {
ticks++;
// Hide bird if the browser is fullscreen
if (document.fullscreenElement) {
hideBirb();
// Won't be restored on fullscreen exit
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
if (focusedElement === null) {
// Fly to an element
focusOnElement();
lastActionTimestamp = Date.now();
} else if (Math.random() < 1 / (60 * 20)) {
// Fly to another element if idle for a longer while
focusOnElement();
lastActionTimestamp = Date.now();
}
}
} else if (currentState === States.HOP) {
if (updateParabolicPath(HOP_SPEED)) {
setState(States.IDLE);
}
}
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();
}
function draw() {
requestAnimationFrame(draw);
if (!visible) {
return;
}
updateFocusedElementBounds();
// Update the bird's position
if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) {
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);
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
setAnimation(Animations.STILL);
}
// Update HTML element position
setX(birdX);
setY(birdY);
}
init();
draw();
/** /**
* Create an HTML element with the specified parameters * Create an HTML element with the specified parameters
* @param {string} className * @param {string} className
@@ -1115,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");
@@ -1194,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
*/ */
@@ -1225,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">
<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">
${message}
</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"); const modal = createWindow("birb-modal", title, `
if (closeButton) { <div class="birb-message-content">
makeClosable(() => { ${message}
modal.remove(); </div>
}, closeButton); `);
}
modal.style.width = "270px";
centerElement(modal); centerElement(modal);
} }
@@ -1355,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
*/ */
@@ -1522,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,7 +1682,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function getFullWindowHeight() { function getFullWindowHeight() {
return document.documentElement.clientHeight; return document.documentElement.clientHeight;
} }
function focusOnGround() { function focusOnGround() {
console.log("Focusing on ground"); console.log("Focusing on ground");
focusedElement = null; focusedElement = null;
@@ -1697,12 +1697,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
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;
} }
@@ -1719,12 +1718,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() };
return; return;
} }
const rect = focusedElement.getBoundingClientRect(); const { left, right, top } = focusedElement.getBoundingClientRect();
focusedBounds = { focusedBounds = { left, right, top };
left: rect.left,
right: rect.right,
top: rect.top
};
} }
function getCanvasWidth() { function getCanvasWidth() {
@@ -1752,7 +1747,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
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();
} }
@@ -1832,47 +1827,53 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
canvas.style.bottom = `${bottom}px`; canvas.style.bottom = `${bottom}px`;
} }
// Helper functions
/**
* @param {number} startX
* @param {number} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* Parse URL parameters into a key-value map
* @param {string} url
* @returns {Record<string, string>}
*/
function parseUrlParams(url) {
const queryString = url.split("?")[1];
if (!queryString) return {};
return queryString.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
}
// Run the birb
init();
draw();
}).catch((e) => {
error("Error while loading sprite sheets: ", e);
}); });
/**
* @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} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* @param {number} value
*/
function roundToPixel(value) {
return Math.round(value / WINDOW_PIXEL_SIZE) * WINDOW_PIXEL_SIZE;
}
/** /**
* @returns {boolean} Whether the user is on a mobile device * @returns {boolean} Whether the user is on a mobile device
*/ */

View File

@@ -20,11 +20,44 @@ const spriteSheets = [
const STYLESHEET_PATH = "./stylesheet.css"; const STYLESHEET_PATH = "./stylesheet.css";
const STYLESHEET_KEY = "___STYLESHEET___"; const STYLESHEET_KEY = "___STYLESHEET___";
const now = new Date();
const versionDate = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`;
// Get current build number from manifest.json
let buildNumber = 0;
try {
const manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
if (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) {
console.error("Could not read version from manifest.json");
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 Pocket Bird // @name Pocket Bird
// @namespace https://idreesinc.com // @namespace https://idreesinc.com
// @version 2025-10-23-01 // @version ${version}
// @description birb // @description birb
// @author Idrees // @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js

733
dist/birb.js vendored
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 = {};
@@ -294,8 +298,8 @@ const STYLESHEET = `:root {
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;
} }
@@ -342,12 +346,12 @@ const STYLESHEET = `:root {
} }
.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);
@@ -723,6 +727,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;
@@ -730,6 +735,34 @@ const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYA
const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg=="; const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg==";
const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII=";
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
@@ -786,8 +819,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;
@@ -940,8 +979,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", () => {
@@ -970,39 +1007,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 = {
@@ -1043,20 +1052,23 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
* @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";
} }
function isTestEnvironment() { function isTestEnvironment() {
return window.location.hostname === "127.0.0.1"; return window.location.hostname === "127.0.0.1"
|| window.location.hostname === "localhost"
|| window.location.hostname.startsWith("192.168.");
} }
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");
@@ -1064,14 +1076,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) {
@@ -1079,17 +1095,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,
@@ -1099,9 +1117,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");
@@ -1114,7 +1133,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");
@@ -1140,6 +1159,178 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return settings().birbMode ? "Birb" : "Bird"; return settings().birbMode ? "Birb" : "Bird";
} }
function init() {
if (window !== window.top) {
// Skip installation if within an iframe
log("In iframe, skipping Birb script initialization");
return;
}
log("Sprite sheets loaded successfully, initializing bird...");
// Preload font
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
const fontLink = document.createElement("link");
fontLink.rel = "stylesheet";
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
document.head.appendChild(fontLink);
// Add stylesheet font-face
const fontFace = `
@font-face {
font-family: 'Monocraft';
src: url(${MONOCRAFT_SRC}) format('opentype');
font-weight: normal;
font-style: normal;
}
`;
const fontStyle = document.createElement("style");
fontStyle.innerHTML = fontFace;
document.head.appendChild(fontStyle);
load();
styleElement.innerHTML = STYLESHEET;
document.head.appendChild(styleElement);
canvas.id = "birb";
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas);
window.addEventListener("scroll", () => {
lastActionTimestamp = Date.now();
});
onClick(document, (e) => {
lastActionTimestamp = Date.now();
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
removeMenu();
}
});
onClick(canvas, () => {
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
// Currently being pet, don't open menu
return;
}
insertMenu();
});
canvas.addEventListener("mouseover", () => {
lastActionTimestamp = Date.now();
if (currentState === States.IDLE) {
petStack.push(Date.now());
if (petStack.length > 10) {
petStack.shift();
}
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
if (pets >= 3) {
pet();
// Clear the stack
petStack = [];
}
}
});
canvas.addEventListener("touchmove", (e) => {
pet();
});
drawStickyNotes();
let lastUrl = (window.location.href ?? "").split("?")[0];
setInterval(() => {
const currentUrl = (window.location.href ?? "").split("?")[0];
if (currentUrl !== lastUrl) {
log("URL changed, updating sticky notes");
lastUrl = currentUrl;
drawStickyNotes();
}
}, URL_CHECK_INTERVAL);
setInterval(update, UPDATE_INTERVAL);
}
function update() {
ticks++;
// Hide bird if the browser is fullscreen
if (document.fullscreenElement) {
hideBirb();
// Won't be restored on fullscreen exit
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
if (focusedElement === null) {
// Fly to an element
focusOnElement();
lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while
focusOnElement();
lastActionTimestamp = Date.now();
}
}
} else if (currentState === States.HOP) {
if (updateParabolicPath(HOP_SPEED)) {
setState(States.IDLE);
}
}
// Double the chance of a feather if recently pet
const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
lastPetTimestamp = 0;
activateFeather();
}
updateFeather();
}
function draw() {
requestAnimationFrame(draw);
if (!visible) {
return;
}
updateFocusedElementBounds();
// Update the bird's position
if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) {
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);
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
setAnimation(Animations.STILL);
}
// Update HTML element position
setX(birdX);
setY(birdY);
}
function newStickyNote() { function newStickyNote() {
const id = Date.now().toString(); const id = Date.now().toString();
const site = window.location.href; const site = window.location.href;
@@ -1240,17 +1431,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);
@@ -1262,88 +1444,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return true; return true;
} }
function init() {
if (window !== window.top) {
// Skip installation if within an iframe
return;
}
// Preload font
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
const fontLink = document.createElement("link");
fontLink.rel = "stylesheet";
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
document.head.appendChild(fontLink);
// Add stylesheet font-face
const fontFace = `
@font-face {
font-family: 'Monocraft';
src: url(${MONOCRAFT_SRC}) format('opentype');
font-weight: normal;
font-style: normal;
}
`;
const fontStyle = document.createElement("style");
fontStyle.innerHTML = fontFace;
document.head.appendChild(fontStyle);
load();
styleElement.innerHTML = STYLESHEET;
document.head.appendChild(styleElement);
canvas.id = "birb";
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas);
window.addEventListener("scroll", () => {
lastActionTimestamp = Date.now();
});
onClick(document, (e) => {
lastActionTimestamp = Date.now();
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
removeMenu();
}
});
onClick(canvas, () => {
insertMenu();
});
canvas.addEventListener("mouseover", () => {
lastActionTimestamp = Date.now();
if (currentState === States.IDLE) {
petStack.push(Date.now());
if (petStack.length > 10) {
petStack.shift();
}
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
if (pets >= 3) {
setAnimation(Animations.HEART);
// Clear the stack
petStack = [];
}
}
});
drawStickyNotes();
let lastUrl = (window.location.href ?? "").split("?")[0];
setInterval(() => {
const currentUrl = (window.location.href ?? "").split("?")[0];
if (currentUrl !== lastUrl) {
log("URL changed, updating sticky notes");
lastUrl = currentUrl;
drawStickyNotes();
}
}, 500);
setInterval(update, 1000 / 60);
}
function drawStickyNotes() { function drawStickyNotes() {
// Remove all existing sticky notes // Remove all existing sticky notes
const existingNotes = document.querySelectorAll(".birb-sticky-note"); const existingNotes = document.querySelectorAll(".birb-sticky-note");
@@ -1356,89 +1456,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
} }
function update() {
ticks++;
// Hide bird if the browser is fullscreen
if (document.fullscreenElement) {
hideBirb();
// Won't be restored on fullscreen exit
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
if (focusedElement === null) {
// Fly to an element
focusOnElement();
lastActionTimestamp = Date.now();
} else if (Math.random() < 1 / (60 * 20)) {
// Fly to another element if idle for a longer while
focusOnElement();
lastActionTimestamp = Date.now();
}
}
} else if (currentState === States.HOP) {
if (updateParabolicPath(HOP_SPEED)) {
setState(States.IDLE);
}
}
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();
}
function draw() {
requestAnimationFrame(draw);
if (!visible) {
return;
}
updateFocusedElementBounds();
// Update the bird's position
if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) {
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);
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
setAnimation(Animations.STILL);
}
// Update HTML element position
setX(birdX);
setY(birdY);
}
init();
draw();
/** /**
* Create an HTML element with the specified parameters * Create an HTML element with the specified parameters
* @param {string} className * @param {string} className
@@ -1458,6 +1475,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");
@@ -1537,21 +1590,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
*/ */
@@ -1568,28 +1616,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">
<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">
${message}
</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"); const modal = createWindow("birb-modal", title, `
if (closeButton) { <div class="birb-message-content">
makeClosable(() => { ${message}
modal.remove(); </div>
}, closeButton); `);
}
modal.style.width = "270px";
centerElement(modal); centerElement(modal);
} }
@@ -1698,60 +1732,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
*/ */
@@ -1865,7 +1849,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);
}
});
} }
/** /**
@@ -2025,7 +2025,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function getFullWindowHeight() { function getFullWindowHeight() {
return document.documentElement.clientHeight; return document.documentElement.clientHeight;
} }
function focusOnGround() { function focusOnGround() {
console.log("Focusing on ground"); console.log("Focusing on ground");
focusedElement = null; focusedElement = null;
@@ -2040,12 +2040,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
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;
} }
@@ -2062,12 +2061,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() };
return; return;
} }
const rect = focusedElement.getBoundingClientRect(); const { left, right, top } = focusedElement.getBoundingClientRect();
focusedBounds = { focusedBounds = { left, right, top };
left: rect.left,
right: rect.right,
top: rect.top
};
} }
function getCanvasWidth() { function getCanvasWidth() {
@@ -2095,7 +2090,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
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();
} }
@@ -2175,47 +2170,53 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
canvas.style.bottom = `${bottom}px`; canvas.style.bottom = `${bottom}px`;
} }
// Helper functions
/**
* @param {number} startX
* @param {number} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* Parse URL parameters into a key-value map
* @param {string} url
* @returns {Record<string, string>}
*/
function parseUrlParams(url) {
const queryString = url.split("?")[1];
if (!queryString) return {};
return queryString.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
}
// Run the birb
init();
draw();
}).catch((e) => {
error("Error while loading sprite sheets: ", e);
}); });
/**
* @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} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* @param {number} value
*/
function roundToPixel(value) {
return Math.round(value / WINDOW_PIXEL_SIZE) * WINDOW_PIXEL_SIZE;
}
/** /**
* @returns {boolean} Whether the user is on a mobile device * @returns {boolean} Whether the user is on a mobile device
*/ */

735
dist/birb.user.js vendored
View File

@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Pocket Bird // @name Pocket Bird
// @namespace https://idreesinc.com // @namespace https://idreesinc.com
// @version 2025-10-23-01 // @version 2025.10.25.130
// @description birb // @description birb
// @author Idrees // @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
@@ -14,7 +14,6 @@
// @ts-check // @ts-check
// @ts-ignore
const SHARED_CONFIG = { const SHARED_CONFIG = {
birbCssScale: 1, birbCssScale: 1,
uiCssScale: 1, uiCssScale: 1,
@@ -23,9 +22,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 = {
@@ -42,17 +40,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
@@ -62,6 +49,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 = {};
@@ -308,8 +312,8 @@ const STYLESHEET = `:root {
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;
} }
@@ -356,12 +360,12 @@ const STYLESHEET = `:root {
} }
.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);
@@ -737,6 +741,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;
@@ -744,6 +749,34 @@ const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYA
const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg=="; const DECORATIONS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg==";
const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII=";
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
@@ -800,8 +833,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;
@@ -954,8 +993,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", () => {
@@ -984,39 +1021,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 = {
@@ -1057,20 +1066,23 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
* @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";
} }
function isTestEnvironment() { function isTestEnvironment() {
return window.location.hostname === "127.0.0.1"; return window.location.hostname === "127.0.0.1"
|| window.location.hostname === "localhost"
|| window.location.hostname.startsWith("192.168.");
} }
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");
@@ -1078,14 +1090,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) {
@@ -1093,17 +1109,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,
@@ -1113,9 +1131,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");
@@ -1128,7 +1147,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");
@@ -1154,6 +1173,178 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return settings().birbMode ? "Birb" : "Bird"; return settings().birbMode ? "Birb" : "Bird";
} }
function init() {
if (window !== window.top) {
// Skip installation if within an iframe
log("In iframe, skipping Birb script initialization");
return;
}
log("Sprite sheets loaded successfully, initializing bird...");
// Preload font
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
const fontLink = document.createElement("link");
fontLink.rel = "stylesheet";
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
document.head.appendChild(fontLink);
// Add stylesheet font-face
const fontFace = `
@font-face {
font-family: 'Monocraft';
src: url(${MONOCRAFT_SRC}) format('opentype');
font-weight: normal;
font-style: normal;
}
`;
const fontStyle = document.createElement("style");
fontStyle.innerHTML = fontFace;
document.head.appendChild(fontStyle);
load();
styleElement.innerHTML = STYLESHEET;
document.head.appendChild(styleElement);
canvas.id = "birb";
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas);
window.addEventListener("scroll", () => {
lastActionTimestamp = Date.now();
});
onClick(document, (e) => {
lastActionTimestamp = Date.now();
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
removeMenu();
}
});
onClick(canvas, () => {
if (currentAnimation === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) {
// Currently being pet, don't open menu
return;
}
insertMenu();
});
canvas.addEventListener("mouseover", () => {
lastActionTimestamp = Date.now();
if (currentState === States.IDLE) {
petStack.push(Date.now());
if (petStack.length > 10) {
petStack.shift();
}
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
if (pets >= 3) {
pet();
// Clear the stack
petStack = [];
}
}
});
canvas.addEventListener("touchmove", (e) => {
pet();
});
drawStickyNotes();
let lastUrl = (window.location.href ?? "").split("?")[0];
setInterval(() => {
const currentUrl = (window.location.href ?? "").split("?")[0];
if (currentUrl !== lastUrl) {
log("URL changed, updating sticky notes");
lastUrl = currentUrl;
drawStickyNotes();
}
}, URL_CHECK_INTERVAL);
setInterval(update, UPDATE_INTERVAL);
}
function update() {
ticks++;
// Hide bird if the browser is fullscreen
if (document.fullscreenElement) {
hideBirb();
// Won't be restored on fullscreen exit
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
if (focusedElement === null) {
// Fly to an element
focusOnElement();
lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while
focusOnElement();
lastActionTimestamp = Date.now();
}
}
} else if (currentState === States.HOP) {
if (updateParabolicPath(HOP_SPEED)) {
setState(States.IDLE);
}
}
// Double the chance of a feather if recently pet
const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
lastPetTimestamp = 0;
activateFeather();
}
updateFeather();
}
function draw() {
requestAnimationFrame(draw);
if (!visible) {
return;
}
updateFocusedElementBounds();
// Update the bird's position
if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) {
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);
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
setAnimation(Animations.STILL);
}
// Update HTML element position
setX(birdX);
setY(birdY);
}
function newStickyNote() { function newStickyNote() {
const id = Date.now().toString(); const id = Date.now().toString();
const site = window.location.href; const site = window.location.href;
@@ -1254,17 +1445,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);
@@ -1276,88 +1458,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
return true; return true;
} }
function init() {
if (window !== window.top) {
// Skip installation if within an iframe
return;
}
// Preload font
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
const fontLink = document.createElement("link");
fontLink.rel = "stylesheet";
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
document.head.appendChild(fontLink);
// Add stylesheet font-face
const fontFace = `
@font-face {
font-family: 'Monocraft';
src: url(${MONOCRAFT_SRC}) format('opentype');
font-weight: normal;
font-style: normal;
}
`;
const fontStyle = document.createElement("style");
fontStyle.innerHTML = fontFace;
document.head.appendChild(fontStyle);
load();
styleElement.innerHTML = STYLESHEET;
document.head.appendChild(styleElement);
canvas.id = "birb";
canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE;
canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas);
window.addEventListener("scroll", () => {
lastActionTimestamp = Date.now();
});
onClick(document, (e) => {
lastActionTimestamp = Date.now();
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
removeMenu();
}
});
onClick(canvas, () => {
insertMenu();
});
canvas.addEventListener("mouseover", () => {
lastActionTimestamp = Date.now();
if (currentState === States.IDLE) {
petStack.push(Date.now());
if (petStack.length > 10) {
petStack.shift();
}
const pets = petStack.filter((time) => Date.now() - time < 1000).length;
if (pets >= 3) {
setAnimation(Animations.HEART);
// Clear the stack
petStack = [];
}
}
});
drawStickyNotes();
let lastUrl = (window.location.href ?? "").split("?")[0];
setInterval(() => {
const currentUrl = (window.location.href ?? "").split("?")[0];
if (currentUrl !== lastUrl) {
log("URL changed, updating sticky notes");
lastUrl = currentUrl;
drawStickyNotes();
}
}, 500);
setInterval(update, 1000 / 60);
}
function drawStickyNotes() { function drawStickyNotes() {
// Remove all existing sticky notes // Remove all existing sticky notes
const existingNotes = document.querySelectorAll(".birb-sticky-note"); const existingNotes = document.querySelectorAll(".birb-sticky-note");
@@ -1370,89 +1470,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
} }
function update() {
ticks++;
// Hide bird if the browser is fullscreen
if (document.fullscreenElement) {
hideBirb();
// Won't be restored on fullscreen exit
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
if (focusedElement === null) {
// Fly to an element
focusOnElement();
lastActionTimestamp = Date.now();
} else if (Math.random() < 1 / (60 * 20)) {
// Fly to another element if idle for a longer while
focusOnElement();
lastActionTimestamp = Date.now();
}
}
} else if (currentState === States.HOP) {
if (updateParabolicPath(HOP_SPEED)) {
setState(States.IDLE);
}
}
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();
}
function draw() {
requestAnimationFrame(draw);
if (!visible) {
return;
}
updateFocusedElementBounds();
// Update the bird's position
if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) {
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);
if (currentAnimation.draw(ctx, direction, animStart, species[currentSpecies])) {
setAnimation(Animations.STILL);
}
// Update HTML element position
setX(birdX);
setY(birdY);
}
init();
draw();
/** /**
* Create an HTML element with the specified parameters * Create an HTML element with the specified parameters
* @param {string} className * @param {string} className
@@ -1472,6 +1489,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");
@@ -1551,21 +1604,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
*/ */
@@ -1582,28 +1630,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">
<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">
${message}
</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"); const modal = createWindow("birb-modal", title, `
if (closeButton) { <div class="birb-message-content">
makeClosable(() => { ${message}
modal.remove(); </div>
}, closeButton); `);
}
modal.style.width = "270px";
centerElement(modal); centerElement(modal);
} }
@@ -1712,60 +1746,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
*/ */
@@ -1879,7 +1863,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);
}
});
} }
/** /**
@@ -2039,7 +2039,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
function getFullWindowHeight() { function getFullWindowHeight() {
return document.documentElement.clientHeight; return document.documentElement.clientHeight;
} }
function focusOnGround() { function focusOnGround() {
console.log("Focusing on ground"); console.log("Focusing on ground");
focusedElement = null; focusedElement = null;
@@ -2054,12 +2054,11 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
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;
} }
@@ -2076,12 +2075,8 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() };
return; return;
} }
const rect = focusedElement.getBoundingClientRect(); const { left, right, top } = focusedElement.getBoundingClientRect();
focusedBounds = { focusedBounds = { left, right, top };
left: rect.left,
right: rect.right,
top: rect.top
};
} }
function getCanvasWidth() { function getCanvasWidth() {
@@ -2109,7 +2104,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
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();
} }
@@ -2189,47 +2184,53 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI
} }
canvas.style.bottom = `${bottom}px`; canvas.style.bottom = `${bottom}px`;
} }
// Helper functions
/**
* @param {number} startX
* @param {number} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* Parse URL parameters into a key-value map
* @param {string} url
* @returns {Record<string, string>}
*/
function parseUrlParams(url) {
const queryString = url.split("?")[1];
if (!queryString) return {};
return queryString.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
}
// Run the birb
init();
draw();
}).catch((e) => {
error("Error while loading sprite sheets: ", e);
}); });
/**
* @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} startY
* @param {number} endX
* @param {number} endY
* @param {number} amount
* @param {number} [intensity]
* @returns {{x: number, y: number}}
*/
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
const dx = endX - startX;
const dy = endY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const midX = startX + Math.cos(angle) * distance / 2;
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
const t = amount;
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
return { x, y };
}
/**
* @param {number} value
*/
function roundToPixel(value) {
return Math.round(value / WINDOW_PIXEL_SIZE) * WINDOW_PIXEL_SIZE;
}
/** /**
* @returns {boolean} Whether the user is on a mobile device * @returns {boolean} Whether the user is on a mobile device
*/ */

36
manifest.json Normal file
View File

@@ -0,0 +1,36 @@
{
"manifest_version": 3,
"name": "Pocket Bird",
"description": "It's a bird, in your browser. What more could you want?",
"version": "2025.10.25.130",
"homepage_url": "https://idreesinc.com",
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"./dist/birb.js"
]
}
],
"permissions": [
"storage",
"activeTab"
],
"web_accessible_resources": [
{
"resources": [
"images/*"
],
"matches": [
"<all_urls>"
]
}
],
"browser_specific_settings": {
"gecko": {
"id": "birb@idreesinc.com"
}
}
}

View File

@@ -241,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;
} }
@@ -289,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);