Allow bird to follow sticky notes

This commit is contained in:
Idrees Hassan
2025-10-28 17:15:19 -04:00
parent c832011011
commit 314ded2562
29 changed files with 224 additions and 64 deletions

74
dist/birb.js vendored
View File

@@ -1482,7 +1482,7 @@
// Focus element constraints // Focus element constraints
const MIN_FOCUS_ELEMENT_WIDTH = 100; const MIN_FOCUS_ELEMENT_WIDTH = 100;
const MIN_FOCUS_ELEMENT_TOP = 80; const MIN_FOCUS_ELEMENT_TOP = 40;
/** @type {Partial<Settings>} */ /** @type {Partial<Settings>} */
let userSettings = {}; let userSettings = {};
@@ -1606,7 +1606,7 @@
insertModal(`${birdBirb()} Mode`, message); insertModal(`${birdBirb()} Mode`, message);
}), }),
new Separator(), new Separator(),
new MenuItem("2025.10.26.568", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.10.26.568"); }, false), new MenuItem("2025.10.28.45", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.10.28.45"); }, false),
]; ];
const styleElement = document.createElement("style"); const styleElement = document.createElement("style");
@@ -1853,6 +1853,8 @@
}, URL_CHECK_INTERVAL); }, URL_CHECK_INTERVAL);
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true);
} }
function update() { function update() {
@@ -1906,7 +1908,7 @@
// Update the bird's position // Update the bird's position
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) { if (focusedElement && !isWithinHorizontalBounds()) {
focusOnGround(); flySomewhere();
} }
birdY = getFocusedY(); birdY = getFocusedY();
} else if (currentState === States.FLYING) { } else if (currentState === States.FLYING) {
@@ -1921,8 +1923,8 @@
// Adjust startY to account for scrolling // Adjust startY to account for scrolling
startY += targetY - oldTargetY; startY += targetY - oldTargetY;
if (targetY < 0 || targetY > window.innerHeight) { if (targetY < 0 || targetY > window.innerHeight) {
// Fly to ground if the focused element moves out of bounds // Fly to another element or the ground if the focused element moves out of bounds
focusOnGround(); flySomewhere();
} }
if (birb.draw(SPECIES[currentSpecies])) { if (birb.draw(SPECIES[currentSpecies])) {
@@ -2253,15 +2255,34 @@
return document.documentElement.clientHeight; return document.documentElement.clientHeight;
} }
/**
* Fly to either an element or the ground
*/
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() { function focusOnGround() {
focusedElement = null; focusedElement = null;
focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() };
flyTo(Math.random() * window.innerWidth, 0); flyTo(Math.random() * window.innerWidth, 0);
} }
function focusOnElement() { /**
* Focus on an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found
*/
function focusOnElement(teleport = false) {
if (frozen) { if (frozen) {
return; return false;
} }
const elements = document.querySelectorAll("img, video, .birb-sticky-note"); const elements = document.querySelectorAll("img, video, .birb-sticky-note");
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
@@ -2271,20 +2292,35 @@
/** @type {HTMLElement[]} */ /** @type {HTMLElement[]} */
// @ts-expect-error // @ts-expect-error
const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
if (largeElements.length === 0) {
return;
}
// Ensure the bird doesn't land on fixed or sticky elements // Ensure the bird doesn't land on fixed or sticky elements
const nonFixedElements = largeElements.filter((el) => { const nonFixedElements = largeElements.filter((el) => {
const style = window.getComputedStyle(el); const style = window.getComputedStyle(el);
return style.position !== "fixed" && style.position !== "sticky"; return style.position !== "fixed" && style.position !== "sticky";
}); });
if (nonFixedElements.length === 0) {
return false;
}
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)];
focusedElement = randomElement; focusedElement = randomElement;
log("Focusing on element: ", focusedElement); log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY());
} else {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return randomElement !== null;
}
/**
* @param {number} x
* @param {number} y
*/
function teleportTo(x, y) {
birdX = x;
birdY = y;
setState(States.IDLE);
}
function updateFocusedElementBounds() { function updateFocusedElementBounds() {
if (focusedElement === null) { if (focusedElement === null) {
@@ -2294,7 +2330,23 @@
} }
let { left, right, top } = focusedElement.getBoundingClientRect(); let { left, right, top } = focusedElement.getBoundingClientRect();
if (focusedElement.classList.contains("birb-sticky-note")) { if (focusedElement.classList.contains("birb-sticky-note")) {
top -= 4 * UI_CSS_SCALE; top -= 4.5 * UI_CSS_SCALE;
if (focusedBounds.left !== left) {
// Sticky note has moved
const oldWidth = focusedBounds.right - focusedBounds.left;
const newWidth = right - left;
if (oldWidth === newWidth) {
// Move bird along with note
if (currentState === States.IDLE) {
birdX += left - focusedBounds.left;
} else if (currentState === States.HOP) {
startX += left - focusedBounds.left;
startY += top - focusedBounds.top;
targetX += left - focusedBounds.left;
targetY += top - focusedBounds.top;
}
}
}
} }
focusedBounds = { left, right, top }; focusedBounds = { left, right, top };
} }

76
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.26.568 // @version 2025.10.28.45
// @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
@@ -1496,7 +1496,7 @@
// Focus element constraints // Focus element constraints
const MIN_FOCUS_ELEMENT_WIDTH = 100; const MIN_FOCUS_ELEMENT_WIDTH = 100;
const MIN_FOCUS_ELEMENT_TOP = 80; const MIN_FOCUS_ELEMENT_TOP = 40;
/** @type {Partial<Settings>} */ /** @type {Partial<Settings>} */
let userSettings = {}; let userSettings = {};
@@ -1620,7 +1620,7 @@
insertModal(`${birdBirb()} Mode`, message); insertModal(`${birdBirb()} Mode`, message);
}), }),
new Separator(), new Separator(),
new MenuItem("2025.10.26.568", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.10.26.568"); }, false), new MenuItem("2025.10.28.45", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.10.28.45"); }, false),
]; ];
const styleElement = document.createElement("style"); const styleElement = document.createElement("style");
@@ -1867,6 +1867,8 @@
}, URL_CHECK_INTERVAL); }, URL_CHECK_INTERVAL);
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true);
} }
function update() { function update() {
@@ -1920,7 +1922,7 @@
// Update the bird's position // Update the bird's position
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) { if (focusedElement && !isWithinHorizontalBounds()) {
focusOnGround(); flySomewhere();
} }
birdY = getFocusedY(); birdY = getFocusedY();
} else if (currentState === States.FLYING) { } else if (currentState === States.FLYING) {
@@ -1935,8 +1937,8 @@
// Adjust startY to account for scrolling // Adjust startY to account for scrolling
startY += targetY - oldTargetY; startY += targetY - oldTargetY;
if (targetY < 0 || targetY > window.innerHeight) { if (targetY < 0 || targetY > window.innerHeight) {
// Fly to ground if the focused element moves out of bounds // Fly to another element or the ground if the focused element moves out of bounds
focusOnGround(); flySomewhere();
} }
if (birb.draw(SPECIES[currentSpecies])) { if (birb.draw(SPECIES[currentSpecies])) {
@@ -2267,15 +2269,34 @@
return document.documentElement.clientHeight; return document.documentElement.clientHeight;
} }
/**
* Fly to either an element or the ground
*/
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() { function focusOnGround() {
focusedElement = null; focusedElement = null;
focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() };
flyTo(Math.random() * window.innerWidth, 0); flyTo(Math.random() * window.innerWidth, 0);
} }
function focusOnElement() { /**
* Focus on an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found
*/
function focusOnElement(teleport = false) {
if (frozen) { if (frozen) {
return; return false;
} }
const elements = document.querySelectorAll("img, video, .birb-sticky-note"); const elements = document.querySelectorAll("img, video, .birb-sticky-note");
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
@@ -2285,20 +2306,35 @@
/** @type {HTMLElement[]} */ /** @type {HTMLElement[]} */
// @ts-expect-error // @ts-expect-error
const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
if (largeElements.length === 0) {
return;
}
// Ensure the bird doesn't land on fixed or sticky elements // Ensure the bird doesn't land on fixed or sticky elements
const nonFixedElements = largeElements.filter((el) => { const nonFixedElements = largeElements.filter((el) => {
const style = window.getComputedStyle(el); const style = window.getComputedStyle(el);
return style.position !== "fixed" && style.position !== "sticky"; return style.position !== "fixed" && style.position !== "sticky";
}); });
if (nonFixedElements.length === 0) {
return false;
}
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)];
focusedElement = randomElement; focusedElement = randomElement;
log("Focusing on element: ", focusedElement); log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY());
} else {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return randomElement !== null;
}
/**
* @param {number} x
* @param {number} y
*/
function teleportTo(x, y) {
birdX = x;
birdY = y;
setState(States.IDLE);
}
function updateFocusedElementBounds() { function updateFocusedElementBounds() {
if (focusedElement === null) { if (focusedElement === null) {
@@ -2308,7 +2344,23 @@
} }
let { left, right, top } = focusedElement.getBoundingClientRect(); let { left, right, top } = focusedElement.getBoundingClientRect();
if (focusedElement.classList.contains("birb-sticky-note")) { if (focusedElement.classList.contains("birb-sticky-note")) {
top -= 4 * UI_CSS_SCALE; top -= 4.5 * UI_CSS_SCALE;
if (focusedBounds.left !== left) {
// Sticky note has moved
const oldWidth = focusedBounds.right - focusedBounds.left;
const newWidth = right - left;
if (oldWidth === newWidth) {
// Move bird along with note
if (currentState === States.IDLE) {
birdX += left - focusedBounds.left;
} else if (currentState === States.HOP) {
startX += left - focusedBounds.left;
startY += top - focusedBounds.top;
targetX += left - focusedBounds.left;
targetY += top - focusedBounds.top;
}
}
}
} }
focusedBounds = { left, right, top }; focusedBounds = { left, right, top };
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 651 B

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Pocket Bird", "name": "Pocket Bird",
"description": "It's a bird, in your browser. What more could you want?", "description": "It's a bird, in your browser. What more could you want?",
"version": "2025.10.26.568", "version": "2025.10.28.45",
"homepage_url": "https://idreesinc.com", "homepage_url": "https://idreesinc.com",
"icons": { "icons": {
"48": "images/icons/transparent/48x48x1.png", "48": "images/icons/transparent/48x48x1.png",

View File

@@ -101,7 +101,7 @@ const PET_FEATHER_BOOST = 2;
// Focus element constraints // Focus element constraints
const MIN_FOCUS_ELEMENT_WIDTH = 100; const MIN_FOCUS_ELEMENT_WIDTH = 100;
const MIN_FOCUS_ELEMENT_TOP = 80; const MIN_FOCUS_ELEMENT_TOP = 40;
/** @type {Partial<Settings>} */ /** @type {Partial<Settings>} */
let userSettings = {}; let userSettings = {};
@@ -472,6 +472,8 @@ Promise.all([
}, URL_CHECK_INTERVAL); }, URL_CHECK_INTERVAL);
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
focusOnElement(true);
} }
function update() { function update() {
@@ -525,7 +527,7 @@ Promise.all([
// Update the bird's position // Update the bird's position
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) { if (focusedElement && !isWithinHorizontalBounds()) {
focusOnGround(); flySomewhere();
} }
birdY = getFocusedY(); birdY = getFocusedY();
} else if (currentState === States.FLYING) { } else if (currentState === States.FLYING) {
@@ -540,8 +542,8 @@ Promise.all([
// Adjust startY to account for scrolling // Adjust startY to account for scrolling
startY += targetY - oldTargetY; startY += targetY - oldTargetY;
if (targetY < 0 || targetY > window.innerHeight) { if (targetY < 0 || targetY > window.innerHeight) {
// Fly to ground if the focused element moves out of bounds // Fly to another element or the ground if the focused element moves out of bounds
focusOnGround(); flySomewhere();
} }
if (birb.draw(SPECIES[currentSpecies])) { if (birb.draw(SPECIES[currentSpecies])) {
@@ -876,15 +878,34 @@ Promise.all([
return document.documentElement.clientHeight; return document.documentElement.clientHeight;
} }
/**
* Fly to either an element or the ground
*/
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() { function focusOnGround() {
focusedElement = null; focusedElement = null;
focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() };
flyTo(Math.random() * window.innerWidth, 0); flyTo(Math.random() * window.innerWidth, 0);
} }
function focusOnElement() { /**
* Focus on an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found
*/
function focusOnElement(teleport = false) {
if (frozen) { if (frozen) {
return; return false;
} }
const elements = document.querySelectorAll("img, video, .birb-sticky-note"); const elements = document.querySelectorAll("img, video, .birb-sticky-note");
const inWindow = Array.from(elements).filter((img) => { const inWindow = Array.from(elements).filter((img) => {
@@ -894,20 +915,35 @@ Promise.all([
/** @type {HTMLElement[]} */ /** @type {HTMLElement[]} */
// @ts-expect-error // @ts-expect-error
const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
if (largeElements.length === 0) {
return;
}
// Ensure the bird doesn't land on fixed or sticky elements // Ensure the bird doesn't land on fixed or sticky elements
const nonFixedElements = largeElements.filter((el) => { const nonFixedElements = largeElements.filter((el) => {
const style = window.getComputedStyle(el); const style = window.getComputedStyle(el);
return style.position !== "fixed" && style.position !== "sticky"; return style.position !== "fixed" && style.position !== "sticky";
}); });
if (nonFixedElements.length === 0) {
return false;
}
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)];
focusedElement = randomElement; focusedElement = randomElement;
log("Focusing on element: ", focusedElement); log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY());
} else {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return randomElement !== null;
}
/**
* @param {number} x
* @param {number} y
*/
function teleportTo(x, y) {
birdX = x;
birdY = y;
setState(States.IDLE);
}
function updateFocusedElementBounds() { function updateFocusedElementBounds() {
if (focusedElement === null) { if (focusedElement === null) {
@@ -917,7 +953,23 @@ Promise.all([
} }
let { left, right, top } = focusedElement.getBoundingClientRect(); let { left, right, top } = focusedElement.getBoundingClientRect();
if (focusedElement.classList.contains("birb-sticky-note")) { if (focusedElement.classList.contains("birb-sticky-note")) {
top -= 4 * UI_CSS_SCALE; top -= 4.5 * UI_CSS_SCALE;
if (focusedBounds.left !== left) {
// Sticky note has moved
const oldWidth = focusedBounds.right - focusedBounds.left;
const newWidth = right - left;
if (oldWidth === newWidth) {
// Move bird along with note
if (currentState === States.IDLE) {
birdX += left - focusedBounds.left;
} else if (currentState === States.HOP) {
startX += left - focusedBounds.left;
startY += top - focusedBounds.top;
targetX += left - focusedBounds.left;
targetY += top - focusedBounds.top;
}
}
}
} }
focusedBounds = { left, right, top }; focusedBounds = { left, right, top };
} }
@@ -983,6 +1035,10 @@ Promise.all([
birb.setY(birdY); birb.setY(birdY);
} }
function coinFlip() {
return Math.random() < 0.5;
}
// Helper functions // Helper functions
/** /**