mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-24 19:59:36 +00:00
Persist and restore bird position across sessions
Add saving and restoring of bird positions so the bird can persist its location between navigations and reloads. Introduces SavedBirdPosition and birdPositions in save data, constants for save intervals and thresholds, and tracking of position deltas to mark dirty state. Implements sanitizeSavedBirdPositions, normalizePath, trimming logic, and a per-tab session key (using window.name) to avoid cross-tab restores. Saves periodically and on pagehide/beforeunload, restores on startup (with heuristics to snap to a nearby perchable element), and avoids overwriting restored Y when appropriate.
This commit is contained in:
BIN
dist/extension.zip
vendored
BIN
dist/extension.zip
vendored
Binary file not shown.
241
dist/extension/birb.js
vendored
241
dist/extension/birb.js
vendored
@@ -2111,6 +2111,13 @@
|
||||
* @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SavedBirdPosition
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} updatedAt
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BirbSaveData
|
||||
* @property {string[]} unlockedSpecies
|
||||
@@ -2119,6 +2126,7 @@
|
||||
* @property {string} currentHat
|
||||
* @property {Partial<Settings>} settings
|
||||
* @property {SavedStickyNote[]} [stickyNotes]
|
||||
* @property {Record<string, SavedBirdPosition>} [birdPositions]
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -2600,11 +2608,16 @@
|
||||
|
||||
// Petting boosts
|
||||
const PET_BOOST_DURATION = 1000 * 60 * 5; // 5 minutes
|
||||
const PET_FEATHER_BOOST = 2;
|
||||
const PET_HAT_BOOST = 1.5;
|
||||
const PET_FEATHER_BOOST = 2; // Multiplier for feather effect
|
||||
const PET_HAT_BOOST = 1.5; // Multiplier for hat effect
|
||||
|
||||
// Focus element constraints
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100; // Minimum width (in px) for an element to be considered a valid perch target
|
||||
const BIRD_POSITION_SAVE_INTERVAL = 2000; // How often (ms) we attempt to persist position in normal flow
|
||||
const BIRD_POSITION_SAVE_MIN_DELTA = 6; // Minimum movement (px) compared to last saved position required before writing again
|
||||
const BIRD_POSITION_TRACKING_DELTA = 0.5; // Minimum movement (px) in runtime tracking to mark position as "dirty"
|
||||
const MAX_SAVED_BIRD_POSITIONS = 200; // Maximum number of saved bird positions to keep
|
||||
const TAB_SESSION_MARKER = "__pocket_bird_tab_session__="; // Marker used in localStorage to identify which tab session saved bird positions belong to, to prevent restoring positions from a different tab
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
@@ -2741,7 +2754,7 @@
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
|
||||
new MenuItem("Build 2026.4.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.4.4"); }, undefined, false),
|
||||
new MenuItem("Build 2026.4.6", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.4.6"); }, undefined, false),
|
||||
];
|
||||
|
||||
/** @type {Birb} */
|
||||
@@ -2780,6 +2793,13 @@
|
||||
let currentHat = DEFAULT_HAT;
|
||||
// let visible = true;
|
||||
let lastPetTimestamp = 0;
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
let savedBirdPositions = {};
|
||||
let holdRestoredYPosition = false;
|
||||
let birdPositionDirty = false;
|
||||
let lastTrackedBirdX = birdX;
|
||||
let lastTrackedBirdY = birdY;
|
||||
let birdSessionKey = "";
|
||||
/** @type {StickyNote[]} */
|
||||
let stickyNotes = [];
|
||||
|
||||
@@ -2798,6 +2818,7 @@
|
||||
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
|
||||
unlockedHats = saveData.unlockedHats ?? [DEFAULT_HAT];
|
||||
currentHat = saveData.currentHat ?? DEFAULT_HAT;
|
||||
savedBirdPositions = sanitizeSavedBirdPositions(saveData.birdPositions);
|
||||
stickyNotes = [];
|
||||
|
||||
if (saveData.stickyNotes) {
|
||||
@@ -2832,6 +2853,9 @@
|
||||
left: note.left
|
||||
}));
|
||||
}
|
||||
if (Object.keys(savedBirdPositions).length > 0) {
|
||||
saveData.birdPositions = savedBirdPositions;
|
||||
}
|
||||
|
||||
getContext().putSaveData(saveData);
|
||||
}
|
||||
@@ -2919,19 +2943,26 @@
|
||||
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
|
||||
let lastPath = getContext().getPath().split("?")[0];
|
||||
let lastPath = normalizePath(getContext().getPath());
|
||||
setInterval(() => {
|
||||
const currentPath = getContext().getPath().split("?")[0];
|
||||
const currentPath = normalizePath(getContext().getPath());
|
||||
if (currentPath !== lastPath) {
|
||||
log("Path changed from '" + lastPath + "' to '" + currentPath + "'");
|
||||
saveBirdPosition(true);
|
||||
lastPath = currentPath;
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
restoreBirdPosition();
|
||||
}
|
||||
}, URL_CHECK_INTERVAL);
|
||||
|
||||
setInterval(update, UPDATE_INTERVAL);
|
||||
setInterval(() => saveBirdPosition(), BIRD_POSITION_SAVE_INTERVAL);
|
||||
window.addEventListener("pagehide", () => saveBirdPosition(true));
|
||||
window.addEventListener("beforeunload", () => saveBirdPosition(true));
|
||||
|
||||
flyToElement(true);
|
||||
if (!restoreBirdPosition()) {
|
||||
flyToElement(true);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
@@ -2992,7 +3023,9 @@
|
||||
if (focusedElement && !isWithinHorizontalBounds()) {
|
||||
flyToElement();
|
||||
}
|
||||
birdY = getFocusedY();
|
||||
if (focusedElement || !holdRestoredYPosition) {
|
||||
birdY = getFocusedY();
|
||||
}
|
||||
} else if (currentState === States.FLYING) {
|
||||
// Fly to target location (even if in the air)
|
||||
if (updateParabolicPath(FLY_SPEED, 2)) {
|
||||
@@ -3022,6 +3055,13 @@
|
||||
// Update HTML element position
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
const movedX = Math.abs(birdX - lastTrackedBirdX);
|
||||
const movedY = Math.abs(birdY - lastTrackedBirdY);
|
||||
if (movedX >= BIRD_POSITION_TRACKING_DELTA || movedY >= BIRD_POSITION_TRACKING_DELTA) {
|
||||
birdPositionDirty = true;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3578,6 +3618,7 @@
|
||||
if (frozen) {
|
||||
return false;
|
||||
}
|
||||
holdRestoredYPosition = false;
|
||||
const previousElement = focusedElement;
|
||||
focusedElement = getRandomValidElement();
|
||||
updateFocusedElementBounds();
|
||||
@@ -3594,6 +3635,7 @@
|
||||
* @param {number} y
|
||||
*/
|
||||
function teleportTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
birdX = x;
|
||||
birdY = y;
|
||||
setState(States.IDLE);
|
||||
@@ -3628,11 +3670,16 @@
|
||||
focusedBounds = { left, right, top };
|
||||
}
|
||||
|
||||
function getCanvasWidth() {
|
||||
return birb.getElementWidth();
|
||||
}
|
||||
|
||||
function hop() {
|
||||
if (frozen) {
|
||||
return;
|
||||
}
|
||||
if (currentState === States.IDLE) {
|
||||
holdRestoredYPosition = false;
|
||||
setState(States.HOP);
|
||||
birb.setAnimation(Animations.FLYING);
|
||||
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
|
||||
@@ -3663,6 +3710,7 @@
|
||||
* @param {number} y
|
||||
*/
|
||||
function flyTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
targetX = x;
|
||||
targetY = y;
|
||||
setState(States.FLYING);
|
||||
@@ -3692,6 +3740,183 @@
|
||||
birb.setY(birdY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {Record<string, SavedBirdPosition>}
|
||||
*/
|
||||
function sanitizeSavedBirdPositions(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
const result = {};
|
||||
for (const [key, position] of Object.entries(value)) {
|
||||
if (!position || typeof position !== "object" || Array.isArray(position)) {
|
||||
continue;
|
||||
}
|
||||
// @ts-expect-error
|
||||
const x = Number(position.x);
|
||||
// @ts-expect-error
|
||||
const y = Number(position.y);
|
||||
// @ts-expect-error
|
||||
const updatedAt = Number(position.updatedAt ?? 0);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||
continue;
|
||||
}
|
||||
result[key] = { x, y, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizePath(path) {
|
||||
return path.split("?")[0].split("#")[0];
|
||||
}
|
||||
|
||||
function trimSavedBirdPositions() {
|
||||
const entries = Object.entries(savedBirdPositions);
|
||||
if (entries.length <= MAX_SAVED_BIRD_POSITIONS) {
|
||||
return;
|
||||
}
|
||||
entries.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
||||
for (let i = 0; i < entries.length - MAX_SAVED_BIRD_POSITIONS; i++) {
|
||||
delete savedBirdPositions[entries[i][0]];
|
||||
}
|
||||
}
|
||||
|
||||
function getBirdPositionScopeKey() {
|
||||
if (birdSessionKey) {
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const existingWindowName = typeof window.name === "string" ? window.name : "";
|
||||
const markerIndex = existingWindowName.indexOf(TAB_SESSION_MARKER);
|
||||
if (markerIndex >= 0) {
|
||||
const end = existingWindowName.indexOf("|", markerIndex);
|
||||
birdSessionKey = end >= 0
|
||||
? existingWindowName.slice(markerIndex, end)
|
||||
: existingWindowName.slice(markerIndex);
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const sessionToken = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
birdSessionKey = `${TAB_SESSION_MARKER}${sessionToken}`;
|
||||
|
||||
try {
|
||||
window.name = existingWindowName
|
||||
? `${existingWindowName}|${birdSessionKey}`
|
||||
: birdSessionKey;
|
||||
} catch {
|
||||
// Ignore if the page blocks changing window.name.
|
||||
}
|
||||
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} [force]
|
||||
*/
|
||||
function saveBirdPosition(force = false) {
|
||||
if (!Number.isFinite(birdX) || !Number.isFinite(birdY)) {
|
||||
return;
|
||||
}
|
||||
if (!force && !birdPositionDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const previous = savedBirdPositions[scopeKey];
|
||||
if (!force && previous) {
|
||||
const movedX = Math.abs(previous.x - birdX);
|
||||
const movedY = Math.abs(previous.y - birdY);
|
||||
if (movedX < BIRD_POSITION_SAVE_MIN_DELTA && movedY < BIRD_POSITION_SAVE_MIN_DELTA) {
|
||||
birdPositionDirty = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
savedBirdPositions[scopeKey] = {
|
||||
x: birdX,
|
||||
y: birdY,
|
||||
updatedAt: now
|
||||
};
|
||||
trimSavedBirdPositions();
|
||||
birdPositionDirty = false;
|
||||
save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function restoreBirdPosition() {
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const saved = savedBirdPositions[scopeKey];
|
||||
if (!saved) {
|
||||
holdRestoredYPosition = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxX = Math.max(0, window.innerWidth - getCanvasWidth());
|
||||
const maxY = getWindowHeight() * 1.5;
|
||||
birdX = Math.min(Math.max(saved.x, 0), maxX);
|
||||
birdY = Math.min(Math.max(saved.y, 0), maxY);
|
||||
|
||||
// Attempt to keep the bird perched if an element still exists near the saved position.
|
||||
focusedElement = getElementAtPosition(birdX, birdY);
|
||||
updateFocusedElementBounds();
|
||||
|
||||
holdRestoredYPosition = focusedElement === null;
|
||||
birdPositionDirty = false;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
|
||||
setState(States.IDLE);
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
function getElementAtPosition(x, y) {
|
||||
const desiredTop = getWindowHeight() - y;
|
||||
let bestElement = null;
|
||||
let bestScore = Number.POSITIVE_INFINITY;
|
||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||
for (const element of elements) {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
if (element.offsetWidth < MIN_FOCUS_ELEMENT_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
const xDistance = Math.abs((rect.left + rect.right) / 2 - x);
|
||||
const yDistance = Math.abs(rect.top - desiredTop);
|
||||
const score = xDistance + yDistance * 1.5;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestElement = element;
|
||||
}
|
||||
}
|
||||
if (bestScore > Math.max(window.innerWidth, getWindowHeight()) * 0.75) {
|
||||
return null;
|
||||
}
|
||||
return bestElement;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
/**
|
||||
|
||||
2
dist/extension/manifest.json
vendored
2
dist/extension/manifest.json
vendored
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Pocket Bird",
|
||||
"description": "It's a pet bird in your browser, what more could you want?",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.6",
|
||||
"homepage_url": "https://idreesinc.com",
|
||||
"icons": {
|
||||
"48": "images/icons/transparent/48x48x1.png",
|
||||
|
||||
243
dist/obsidian/main.js
vendored
243
dist/obsidian/main.js
vendored
@@ -1,7 +1,7 @@
|
||||
const { Plugin, Notice } = require('obsidian');
|
||||
module.exports = class PocketBird extends Plugin {
|
||||
onload() {
|
||||
console.log("Loading Pocket Bird version 2026.4.4...");
|
||||
console.log("Loading Pocket Bird version 2026.4.6...");
|
||||
const OBSIDIAN_PLUGIN = this;
|
||||
(function () {
|
||||
'use strict';
|
||||
@@ -2144,6 +2144,13 @@ module.exports = class PocketBird extends Plugin {
|
||||
* @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SavedBirdPosition
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} updatedAt
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BirbSaveData
|
||||
* @property {string[]} unlockedSpecies
|
||||
@@ -2152,6 +2159,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
* @property {string} currentHat
|
||||
* @property {Partial<Settings>} settings
|
||||
* @property {SavedStickyNote[]} [stickyNotes]
|
||||
* @property {Record<string, SavedBirdPosition>} [birdPositions]
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -2633,11 +2641,16 @@ module.exports = class PocketBird extends Plugin {
|
||||
|
||||
// Petting boosts
|
||||
const PET_BOOST_DURATION = 1000 * 60 * 5; // 5 minutes
|
||||
const PET_FEATHER_BOOST = 2;
|
||||
const PET_HAT_BOOST = 1.5;
|
||||
const PET_FEATHER_BOOST = 2; // Multiplier for feather effect
|
||||
const PET_HAT_BOOST = 1.5; // Multiplier for hat effect
|
||||
|
||||
// Focus element constraints
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100; // Minimum width (in px) for an element to be considered a valid perch target
|
||||
const BIRD_POSITION_SAVE_INTERVAL = 2000; // How often (ms) we attempt to persist position in normal flow
|
||||
const BIRD_POSITION_SAVE_MIN_DELTA = 6; // Minimum movement (px) compared to last saved position required before writing again
|
||||
const BIRD_POSITION_TRACKING_DELTA = 0.5; // Minimum movement (px) in runtime tracking to mark position as "dirty"
|
||||
const MAX_SAVED_BIRD_POSITIONS = 200; // Maximum number of saved bird positions to keep
|
||||
const TAB_SESSION_MARKER = "__pocket_bird_tab_session__="; // Marker used in localStorage to identify which tab session saved bird positions belong to, to prevent restoring positions from a different tab
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
@@ -2774,7 +2787,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
|
||||
new MenuItem("Build 2026.4.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.4.4"); }, undefined, false),
|
||||
new MenuItem("Build 2026.4.6", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.4.6"); }, undefined, false),
|
||||
];
|
||||
|
||||
/** @type {Birb} */
|
||||
@@ -2813,6 +2826,13 @@ module.exports = class PocketBird extends Plugin {
|
||||
let currentHat = DEFAULT_HAT;
|
||||
// let visible = true;
|
||||
let lastPetTimestamp = 0;
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
let savedBirdPositions = {};
|
||||
let holdRestoredYPosition = false;
|
||||
let birdPositionDirty = false;
|
||||
let lastTrackedBirdX = birdX;
|
||||
let lastTrackedBirdY = birdY;
|
||||
let birdSessionKey = "";
|
||||
/** @type {StickyNote[]} */
|
||||
let stickyNotes = [];
|
||||
|
||||
@@ -2831,6 +2851,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
|
||||
unlockedHats = saveData.unlockedHats ?? [DEFAULT_HAT];
|
||||
currentHat = saveData.currentHat ?? DEFAULT_HAT;
|
||||
savedBirdPositions = sanitizeSavedBirdPositions(saveData.birdPositions);
|
||||
stickyNotes = [];
|
||||
|
||||
if (saveData.stickyNotes) {
|
||||
@@ -2865,6 +2886,9 @@ module.exports = class PocketBird extends Plugin {
|
||||
left: note.left
|
||||
}));
|
||||
}
|
||||
if (Object.keys(savedBirdPositions).length > 0) {
|
||||
saveData.birdPositions = savedBirdPositions;
|
||||
}
|
||||
|
||||
getContext().putSaveData(saveData);
|
||||
}
|
||||
@@ -2952,19 +2976,26 @@ module.exports = class PocketBird extends Plugin {
|
||||
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
|
||||
let lastPath = getContext().getPath().split("?")[0];
|
||||
let lastPath = normalizePath(getContext().getPath());
|
||||
setInterval(() => {
|
||||
const currentPath = getContext().getPath().split("?")[0];
|
||||
const currentPath = normalizePath(getContext().getPath());
|
||||
if (currentPath !== lastPath) {
|
||||
log("Path changed from '" + lastPath + "' to '" + currentPath + "'");
|
||||
saveBirdPosition(true);
|
||||
lastPath = currentPath;
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
restoreBirdPosition();
|
||||
}
|
||||
}, URL_CHECK_INTERVAL);
|
||||
|
||||
setInterval(update, UPDATE_INTERVAL);
|
||||
setInterval(() => saveBirdPosition(), BIRD_POSITION_SAVE_INTERVAL);
|
||||
window.addEventListener("pagehide", () => saveBirdPosition(true));
|
||||
window.addEventListener("beforeunload", () => saveBirdPosition(true));
|
||||
|
||||
flyToElement(true);
|
||||
if (!restoreBirdPosition()) {
|
||||
flyToElement(true);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
@@ -3025,7 +3056,9 @@ module.exports = class PocketBird extends Plugin {
|
||||
if (focusedElement && !isWithinHorizontalBounds()) {
|
||||
flyToElement();
|
||||
}
|
||||
birdY = getFocusedY();
|
||||
if (focusedElement || !holdRestoredYPosition) {
|
||||
birdY = getFocusedY();
|
||||
}
|
||||
} else if (currentState === States.FLYING) {
|
||||
// Fly to target location (even if in the air)
|
||||
if (updateParabolicPath(FLY_SPEED, 2)) {
|
||||
@@ -3055,6 +3088,13 @@ module.exports = class PocketBird extends Plugin {
|
||||
// Update HTML element position
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
const movedX = Math.abs(birdX - lastTrackedBirdX);
|
||||
const movedY = Math.abs(birdY - lastTrackedBirdY);
|
||||
if (movedX >= BIRD_POSITION_TRACKING_DELTA || movedY >= BIRD_POSITION_TRACKING_DELTA) {
|
||||
birdPositionDirty = true;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3611,6 +3651,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
if (frozen) {
|
||||
return false;
|
||||
}
|
||||
holdRestoredYPosition = false;
|
||||
const previousElement = focusedElement;
|
||||
focusedElement = getRandomValidElement();
|
||||
updateFocusedElementBounds();
|
||||
@@ -3627,6 +3668,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
* @param {number} y
|
||||
*/
|
||||
function teleportTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
birdX = x;
|
||||
birdY = y;
|
||||
setState(States.IDLE);
|
||||
@@ -3661,11 +3703,16 @@ module.exports = class PocketBird extends Plugin {
|
||||
focusedBounds = { left, right, top };
|
||||
}
|
||||
|
||||
function getCanvasWidth() {
|
||||
return birb.getElementWidth();
|
||||
}
|
||||
|
||||
function hop() {
|
||||
if (frozen) {
|
||||
return;
|
||||
}
|
||||
if (currentState === States.IDLE) {
|
||||
holdRestoredYPosition = false;
|
||||
setState(States.HOP);
|
||||
birb.setAnimation(Animations.FLYING);
|
||||
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
|
||||
@@ -3696,6 +3743,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
* @param {number} y
|
||||
*/
|
||||
function flyTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
targetX = x;
|
||||
targetY = y;
|
||||
setState(States.FLYING);
|
||||
@@ -3725,6 +3773,183 @@ module.exports = class PocketBird extends Plugin {
|
||||
birb.setY(birdY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {Record<string, SavedBirdPosition>}
|
||||
*/
|
||||
function sanitizeSavedBirdPositions(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
const result = {};
|
||||
for (const [key, position] of Object.entries(value)) {
|
||||
if (!position || typeof position !== "object" || Array.isArray(position)) {
|
||||
continue;
|
||||
}
|
||||
// @ts-expect-error
|
||||
const x = Number(position.x);
|
||||
// @ts-expect-error
|
||||
const y = Number(position.y);
|
||||
// @ts-expect-error
|
||||
const updatedAt = Number(position.updatedAt ?? 0);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||
continue;
|
||||
}
|
||||
result[key] = { x, y, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizePath(path) {
|
||||
return path.split("?")[0].split("#")[0];
|
||||
}
|
||||
|
||||
function trimSavedBirdPositions() {
|
||||
const entries = Object.entries(savedBirdPositions);
|
||||
if (entries.length <= MAX_SAVED_BIRD_POSITIONS) {
|
||||
return;
|
||||
}
|
||||
entries.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
||||
for (let i = 0; i < entries.length - MAX_SAVED_BIRD_POSITIONS; i++) {
|
||||
delete savedBirdPositions[entries[i][0]];
|
||||
}
|
||||
}
|
||||
|
||||
function getBirdPositionScopeKey() {
|
||||
if (birdSessionKey) {
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const existingWindowName = typeof window.name === "string" ? window.name : "";
|
||||
const markerIndex = existingWindowName.indexOf(TAB_SESSION_MARKER);
|
||||
if (markerIndex >= 0) {
|
||||
const end = existingWindowName.indexOf("|", markerIndex);
|
||||
birdSessionKey = end >= 0
|
||||
? existingWindowName.slice(markerIndex, end)
|
||||
: existingWindowName.slice(markerIndex);
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const sessionToken = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
birdSessionKey = `${TAB_SESSION_MARKER}${sessionToken}`;
|
||||
|
||||
try {
|
||||
window.name = existingWindowName
|
||||
? `${existingWindowName}|${birdSessionKey}`
|
||||
: birdSessionKey;
|
||||
} catch {
|
||||
// Ignore if the page blocks changing window.name.
|
||||
}
|
||||
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} [force]
|
||||
*/
|
||||
function saveBirdPosition(force = false) {
|
||||
if (!Number.isFinite(birdX) || !Number.isFinite(birdY)) {
|
||||
return;
|
||||
}
|
||||
if (!force && !birdPositionDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const previous = savedBirdPositions[scopeKey];
|
||||
if (!force && previous) {
|
||||
const movedX = Math.abs(previous.x - birdX);
|
||||
const movedY = Math.abs(previous.y - birdY);
|
||||
if (movedX < BIRD_POSITION_SAVE_MIN_DELTA && movedY < BIRD_POSITION_SAVE_MIN_DELTA) {
|
||||
birdPositionDirty = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
savedBirdPositions[scopeKey] = {
|
||||
x: birdX,
|
||||
y: birdY,
|
||||
updatedAt: now
|
||||
};
|
||||
trimSavedBirdPositions();
|
||||
birdPositionDirty = false;
|
||||
save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function restoreBirdPosition() {
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const saved = savedBirdPositions[scopeKey];
|
||||
if (!saved) {
|
||||
holdRestoredYPosition = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxX = Math.max(0, window.innerWidth - getCanvasWidth());
|
||||
const maxY = getWindowHeight() * 1.5;
|
||||
birdX = Math.min(Math.max(saved.x, 0), maxX);
|
||||
birdY = Math.min(Math.max(saved.y, 0), maxY);
|
||||
|
||||
// Attempt to keep the bird perched if an element still exists near the saved position.
|
||||
focusedElement = getElementAtPosition(birdX, birdY);
|
||||
updateFocusedElementBounds();
|
||||
|
||||
holdRestoredYPosition = focusedElement === null;
|
||||
birdPositionDirty = false;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
|
||||
setState(States.IDLE);
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
function getElementAtPosition(x, y) {
|
||||
const desiredTop = getWindowHeight() - y;
|
||||
let bestElement = null;
|
||||
let bestScore = Number.POSITIVE_INFINITY;
|
||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||
for (const element of elements) {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
if (element.offsetWidth < MIN_FOCUS_ELEMENT_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
const xDistance = Math.abs((rect.left + rect.right) / 2 - x);
|
||||
const yDistance = Math.abs(rect.top - desiredTop);
|
||||
const score = xDistance + yDistance * 1.5;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestElement = element;
|
||||
}
|
||||
}
|
||||
if (bestScore > Math.max(window.innerWidth, getWindowHeight()) * 0.75) {
|
||||
return null;
|
||||
}
|
||||
return bestElement;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
/**
|
||||
|
||||
2
dist/obsidian/manifest.json
vendored
2
dist/obsidian/manifest.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "pocket-bird",
|
||||
"name": "Pocket Bird",
|
||||
"version": "2026.4.4",
|
||||
"version": "2026.4.6",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Add a pet bird to fly around your notes and keep you company!",
|
||||
"author": "Idrees Hassan",
|
||||
|
||||
243
dist/userscript/birb.user.js
vendored
243
dist/userscript/birb.user.js
vendored
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name Pocket Bird
|
||||
// @namespace https://idreesinc.com
|
||||
// @version 2026.4.4
|
||||
// @version 2026.4.6
|
||||
// @description It's a pet bird in your browser, what more could you want?
|
||||
// @author Idrees
|
||||
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js
|
||||
@@ -2106,6 +2106,13 @@
|
||||
* @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SavedBirdPosition
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} updatedAt
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BirbSaveData
|
||||
* @property {string[]} unlockedSpecies
|
||||
@@ -2114,6 +2121,7 @@
|
||||
* @property {string} currentHat
|
||||
* @property {Partial<Settings>} settings
|
||||
* @property {SavedStickyNote[]} [stickyNotes]
|
||||
* @property {Record<string, SavedBirdPosition>} [birdPositions]
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -2595,11 +2603,16 @@
|
||||
|
||||
// Petting boosts
|
||||
const PET_BOOST_DURATION = 1000 * 60 * 5; // 5 minutes
|
||||
const PET_FEATHER_BOOST = 2;
|
||||
const PET_HAT_BOOST = 1.5;
|
||||
const PET_FEATHER_BOOST = 2; // Multiplier for feather effect
|
||||
const PET_HAT_BOOST = 1.5; // Multiplier for hat effect
|
||||
|
||||
// Focus element constraints
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100; // Minimum width (in px) for an element to be considered a valid perch target
|
||||
const BIRD_POSITION_SAVE_INTERVAL = 2000; // How often (ms) we attempt to persist position in normal flow
|
||||
const BIRD_POSITION_SAVE_MIN_DELTA = 6; // Minimum movement (px) compared to last saved position required before writing again
|
||||
const BIRD_POSITION_TRACKING_DELTA = 0.5; // Minimum movement (px) in runtime tracking to mark position as "dirty"
|
||||
const MAX_SAVED_BIRD_POSITIONS = 200; // Maximum number of saved bird positions to keep
|
||||
const TAB_SESSION_MARKER = "__pocket_bird_tab_session__="; // Marker used in localStorage to identify which tab session saved bird positions belong to, to prevent restoring positions from a different tab
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
@@ -2736,7 +2749,7 @@
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
|
||||
new MenuItem("Build 2026.4.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.4.4"); }, undefined, false),
|
||||
new MenuItem("Build 2026.4.6", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.4.6"); }, undefined, false),
|
||||
];
|
||||
|
||||
/** @type {Birb} */
|
||||
@@ -2775,6 +2788,13 @@
|
||||
let currentHat = DEFAULT_HAT;
|
||||
// let visible = true;
|
||||
let lastPetTimestamp = 0;
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
let savedBirdPositions = {};
|
||||
let holdRestoredYPosition = false;
|
||||
let birdPositionDirty = false;
|
||||
let lastTrackedBirdX = birdX;
|
||||
let lastTrackedBirdY = birdY;
|
||||
let birdSessionKey = "";
|
||||
/** @type {StickyNote[]} */
|
||||
let stickyNotes = [];
|
||||
|
||||
@@ -2793,6 +2813,7 @@
|
||||
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
|
||||
unlockedHats = saveData.unlockedHats ?? [DEFAULT_HAT];
|
||||
currentHat = saveData.currentHat ?? DEFAULT_HAT;
|
||||
savedBirdPositions = sanitizeSavedBirdPositions(saveData.birdPositions);
|
||||
stickyNotes = [];
|
||||
|
||||
if (saveData.stickyNotes) {
|
||||
@@ -2827,6 +2848,9 @@
|
||||
left: note.left
|
||||
}));
|
||||
}
|
||||
if (Object.keys(savedBirdPositions).length > 0) {
|
||||
saveData.birdPositions = savedBirdPositions;
|
||||
}
|
||||
|
||||
getContext().putSaveData(saveData);
|
||||
}
|
||||
@@ -2914,19 +2938,26 @@
|
||||
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
|
||||
let lastPath = getContext().getPath().split("?")[0];
|
||||
let lastPath = normalizePath(getContext().getPath());
|
||||
setInterval(() => {
|
||||
const currentPath = getContext().getPath().split("?")[0];
|
||||
const currentPath = normalizePath(getContext().getPath());
|
||||
if (currentPath !== lastPath) {
|
||||
log("Path changed from '" + lastPath + "' to '" + currentPath + "'");
|
||||
saveBirdPosition(true);
|
||||
lastPath = currentPath;
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
restoreBirdPosition();
|
||||
}
|
||||
}, URL_CHECK_INTERVAL);
|
||||
|
||||
setInterval(update, UPDATE_INTERVAL);
|
||||
setInterval(() => saveBirdPosition(), BIRD_POSITION_SAVE_INTERVAL);
|
||||
window.addEventListener("pagehide", () => saveBirdPosition(true));
|
||||
window.addEventListener("beforeunload", () => saveBirdPosition(true));
|
||||
|
||||
flyToElement(true);
|
||||
if (!restoreBirdPosition()) {
|
||||
flyToElement(true);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
@@ -2987,7 +3018,9 @@
|
||||
if (focusedElement && !isWithinHorizontalBounds()) {
|
||||
flyToElement();
|
||||
}
|
||||
birdY = getFocusedY();
|
||||
if (focusedElement || !holdRestoredYPosition) {
|
||||
birdY = getFocusedY();
|
||||
}
|
||||
} else if (currentState === States.FLYING) {
|
||||
// Fly to target location (even if in the air)
|
||||
if (updateParabolicPath(FLY_SPEED, 2)) {
|
||||
@@ -3017,6 +3050,13 @@
|
||||
// Update HTML element position
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
const movedX = Math.abs(birdX - lastTrackedBirdX);
|
||||
const movedY = Math.abs(birdY - lastTrackedBirdY);
|
||||
if (movedX >= BIRD_POSITION_TRACKING_DELTA || movedY >= BIRD_POSITION_TRACKING_DELTA) {
|
||||
birdPositionDirty = true;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3573,6 +3613,7 @@
|
||||
if (frozen) {
|
||||
return false;
|
||||
}
|
||||
holdRestoredYPosition = false;
|
||||
const previousElement = focusedElement;
|
||||
focusedElement = getRandomValidElement();
|
||||
updateFocusedElementBounds();
|
||||
@@ -3589,6 +3630,7 @@
|
||||
* @param {number} y
|
||||
*/
|
||||
function teleportTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
birdX = x;
|
||||
birdY = y;
|
||||
setState(States.IDLE);
|
||||
@@ -3623,11 +3665,16 @@
|
||||
focusedBounds = { left, right, top };
|
||||
}
|
||||
|
||||
function getCanvasWidth() {
|
||||
return birb.getElementWidth();
|
||||
}
|
||||
|
||||
function hop() {
|
||||
if (frozen) {
|
||||
return;
|
||||
}
|
||||
if (currentState === States.IDLE) {
|
||||
holdRestoredYPosition = false;
|
||||
setState(States.HOP);
|
||||
birb.setAnimation(Animations.FLYING);
|
||||
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
|
||||
@@ -3658,6 +3705,7 @@
|
||||
* @param {number} y
|
||||
*/
|
||||
function flyTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
targetX = x;
|
||||
targetY = y;
|
||||
setState(States.FLYING);
|
||||
@@ -3687,6 +3735,183 @@
|
||||
birb.setY(birdY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {Record<string, SavedBirdPosition>}
|
||||
*/
|
||||
function sanitizeSavedBirdPositions(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
const result = {};
|
||||
for (const [key, position] of Object.entries(value)) {
|
||||
if (!position || typeof position !== "object" || Array.isArray(position)) {
|
||||
continue;
|
||||
}
|
||||
// @ts-expect-error
|
||||
const x = Number(position.x);
|
||||
// @ts-expect-error
|
||||
const y = Number(position.y);
|
||||
// @ts-expect-error
|
||||
const updatedAt = Number(position.updatedAt ?? 0);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||
continue;
|
||||
}
|
||||
result[key] = { x, y, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizePath(path) {
|
||||
return path.split("?")[0].split("#")[0];
|
||||
}
|
||||
|
||||
function trimSavedBirdPositions() {
|
||||
const entries = Object.entries(savedBirdPositions);
|
||||
if (entries.length <= MAX_SAVED_BIRD_POSITIONS) {
|
||||
return;
|
||||
}
|
||||
entries.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
||||
for (let i = 0; i < entries.length - MAX_SAVED_BIRD_POSITIONS; i++) {
|
||||
delete savedBirdPositions[entries[i][0]];
|
||||
}
|
||||
}
|
||||
|
||||
function getBirdPositionScopeKey() {
|
||||
if (birdSessionKey) {
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const existingWindowName = typeof window.name === "string" ? window.name : "";
|
||||
const markerIndex = existingWindowName.indexOf(TAB_SESSION_MARKER);
|
||||
if (markerIndex >= 0) {
|
||||
const end = existingWindowName.indexOf("|", markerIndex);
|
||||
birdSessionKey = end >= 0
|
||||
? existingWindowName.slice(markerIndex, end)
|
||||
: existingWindowName.slice(markerIndex);
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const sessionToken = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
birdSessionKey = `${TAB_SESSION_MARKER}${sessionToken}`;
|
||||
|
||||
try {
|
||||
window.name = existingWindowName
|
||||
? `${existingWindowName}|${birdSessionKey}`
|
||||
: birdSessionKey;
|
||||
} catch {
|
||||
// Ignore if the page blocks changing window.name.
|
||||
}
|
||||
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} [force]
|
||||
*/
|
||||
function saveBirdPosition(force = false) {
|
||||
if (!Number.isFinite(birdX) || !Number.isFinite(birdY)) {
|
||||
return;
|
||||
}
|
||||
if (!force && !birdPositionDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const previous = savedBirdPositions[scopeKey];
|
||||
if (!force && previous) {
|
||||
const movedX = Math.abs(previous.x - birdX);
|
||||
const movedY = Math.abs(previous.y - birdY);
|
||||
if (movedX < BIRD_POSITION_SAVE_MIN_DELTA && movedY < BIRD_POSITION_SAVE_MIN_DELTA) {
|
||||
birdPositionDirty = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
savedBirdPositions[scopeKey] = {
|
||||
x: birdX,
|
||||
y: birdY,
|
||||
updatedAt: now
|
||||
};
|
||||
trimSavedBirdPositions();
|
||||
birdPositionDirty = false;
|
||||
save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function restoreBirdPosition() {
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const saved = savedBirdPositions[scopeKey];
|
||||
if (!saved) {
|
||||
holdRestoredYPosition = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxX = Math.max(0, window.innerWidth - getCanvasWidth());
|
||||
const maxY = getWindowHeight() * 1.5;
|
||||
birdX = Math.min(Math.max(saved.x, 0), maxX);
|
||||
birdY = Math.min(Math.max(saved.y, 0), maxY);
|
||||
|
||||
// Attempt to keep the bird perched if an element still exists near the saved position.
|
||||
focusedElement = getElementAtPosition(birdX, birdY);
|
||||
updateFocusedElementBounds();
|
||||
|
||||
holdRestoredYPosition = focusedElement === null;
|
||||
birdPositionDirty = false;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
|
||||
setState(States.IDLE);
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
function getElementAtPosition(x, y) {
|
||||
const desiredTop = getWindowHeight() - y;
|
||||
let bestElement = null;
|
||||
let bestScore = Number.POSITIVE_INFINITY;
|
||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||
for (const element of elements) {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
if (element.offsetWidth < MIN_FOCUS_ELEMENT_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
const xDistance = Math.abs((rect.left + rect.right) / 2 - x);
|
||||
const yDistance = Math.abs(rect.top - desiredTop);
|
||||
const score = xDistance + yDistance * 1.5;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestElement = element;
|
||||
}
|
||||
}
|
||||
if (bestScore > Math.max(window.innerWidth, getWindowHeight()) * 0.75) {
|
||||
return null;
|
||||
}
|
||||
return bestElement;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
/**
|
||||
|
||||
241
dist/web/birb.embed.js
vendored
241
dist/web/birb.embed.js
vendored
@@ -2086,6 +2086,13 @@
|
||||
* @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SavedBirdPosition
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} updatedAt
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BirbSaveData
|
||||
* @property {string[]} unlockedSpecies
|
||||
@@ -2094,6 +2101,7 @@
|
||||
* @property {string} currentHat
|
||||
* @property {Partial<Settings>} settings
|
||||
* @property {SavedStickyNote[]} [stickyNotes]
|
||||
* @property {Record<string, SavedBirdPosition>} [birdPositions]
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -2575,11 +2583,16 @@
|
||||
|
||||
// Petting boosts
|
||||
const PET_BOOST_DURATION = 1000 * 60 * 5; // 5 minutes
|
||||
const PET_FEATHER_BOOST = 2;
|
||||
const PET_HAT_BOOST = 1.5;
|
||||
const PET_FEATHER_BOOST = 2; // Multiplier for feather effect
|
||||
const PET_HAT_BOOST = 1.5; // Multiplier for hat effect
|
||||
|
||||
// Focus element constraints
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100; // Minimum width (in px) for an element to be considered a valid perch target
|
||||
const BIRD_POSITION_SAVE_INTERVAL = 2000; // How often (ms) we attempt to persist position in normal flow
|
||||
const BIRD_POSITION_SAVE_MIN_DELTA = 6; // Minimum movement (px) compared to last saved position required before writing again
|
||||
const BIRD_POSITION_TRACKING_DELTA = 0.5; // Minimum movement (px) in runtime tracking to mark position as "dirty"
|
||||
const MAX_SAVED_BIRD_POSITIONS = 200; // Maximum number of saved bird positions to keep
|
||||
const TAB_SESSION_MARKER = "__pocket_bird_tab_session__="; // Marker used in localStorage to identify which tab session saved bird positions belong to, to prevent restoring positions from a different tab
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
@@ -2716,7 +2729,7 @@
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
|
||||
new MenuItem("Build 2026.4.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.4.4"); }, undefined, false),
|
||||
new MenuItem("Build 2026.4.6", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.4.6"); }, undefined, false),
|
||||
];
|
||||
|
||||
/** @type {Birb} */
|
||||
@@ -2755,6 +2768,13 @@
|
||||
let currentHat = DEFAULT_HAT;
|
||||
// let visible = true;
|
||||
let lastPetTimestamp = 0;
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
let savedBirdPositions = {};
|
||||
let holdRestoredYPosition = false;
|
||||
let birdPositionDirty = false;
|
||||
let lastTrackedBirdX = birdX;
|
||||
let lastTrackedBirdY = birdY;
|
||||
let birdSessionKey = "";
|
||||
/** @type {StickyNote[]} */
|
||||
let stickyNotes = [];
|
||||
|
||||
@@ -2773,6 +2793,7 @@
|
||||
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
|
||||
unlockedHats = saveData.unlockedHats ?? [DEFAULT_HAT];
|
||||
currentHat = saveData.currentHat ?? DEFAULT_HAT;
|
||||
savedBirdPositions = sanitizeSavedBirdPositions(saveData.birdPositions);
|
||||
stickyNotes = [];
|
||||
|
||||
if (saveData.stickyNotes) {
|
||||
@@ -2807,6 +2828,9 @@
|
||||
left: note.left
|
||||
}));
|
||||
}
|
||||
if (Object.keys(savedBirdPositions).length > 0) {
|
||||
saveData.birdPositions = savedBirdPositions;
|
||||
}
|
||||
|
||||
getContext().putSaveData(saveData);
|
||||
}
|
||||
@@ -2894,19 +2918,26 @@
|
||||
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
|
||||
let lastPath = getContext().getPath().split("?")[0];
|
||||
let lastPath = normalizePath(getContext().getPath());
|
||||
setInterval(() => {
|
||||
const currentPath = getContext().getPath().split("?")[0];
|
||||
const currentPath = normalizePath(getContext().getPath());
|
||||
if (currentPath !== lastPath) {
|
||||
log("Path changed from '" + lastPath + "' to '" + currentPath + "'");
|
||||
saveBirdPosition(true);
|
||||
lastPath = currentPath;
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
restoreBirdPosition();
|
||||
}
|
||||
}, URL_CHECK_INTERVAL);
|
||||
|
||||
setInterval(update, UPDATE_INTERVAL);
|
||||
setInterval(() => saveBirdPosition(), BIRD_POSITION_SAVE_INTERVAL);
|
||||
window.addEventListener("pagehide", () => saveBirdPosition(true));
|
||||
window.addEventListener("beforeunload", () => saveBirdPosition(true));
|
||||
|
||||
flyToElement(true);
|
||||
if (!restoreBirdPosition()) {
|
||||
flyToElement(true);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
@@ -2967,7 +2998,9 @@
|
||||
if (focusedElement && !isWithinHorizontalBounds()) {
|
||||
flyToElement();
|
||||
}
|
||||
birdY = getFocusedY();
|
||||
if (focusedElement || !holdRestoredYPosition) {
|
||||
birdY = getFocusedY();
|
||||
}
|
||||
} else if (currentState === States.FLYING) {
|
||||
// Fly to target location (even if in the air)
|
||||
if (updateParabolicPath(FLY_SPEED, 2)) {
|
||||
@@ -2997,6 +3030,13 @@
|
||||
// Update HTML element position
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
const movedX = Math.abs(birdX - lastTrackedBirdX);
|
||||
const movedY = Math.abs(birdY - lastTrackedBirdY);
|
||||
if (movedX >= BIRD_POSITION_TRACKING_DELTA || movedY >= BIRD_POSITION_TRACKING_DELTA) {
|
||||
birdPositionDirty = true;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3553,6 +3593,7 @@
|
||||
if (frozen) {
|
||||
return false;
|
||||
}
|
||||
holdRestoredYPosition = false;
|
||||
const previousElement = focusedElement;
|
||||
focusedElement = getRandomValidElement();
|
||||
updateFocusedElementBounds();
|
||||
@@ -3569,6 +3610,7 @@
|
||||
* @param {number} y
|
||||
*/
|
||||
function teleportTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
birdX = x;
|
||||
birdY = y;
|
||||
setState(States.IDLE);
|
||||
@@ -3603,11 +3645,16 @@
|
||||
focusedBounds = { left, right, top };
|
||||
}
|
||||
|
||||
function getCanvasWidth() {
|
||||
return birb.getElementWidth();
|
||||
}
|
||||
|
||||
function hop() {
|
||||
if (frozen) {
|
||||
return;
|
||||
}
|
||||
if (currentState === States.IDLE) {
|
||||
holdRestoredYPosition = false;
|
||||
setState(States.HOP);
|
||||
birb.setAnimation(Animations.FLYING);
|
||||
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
|
||||
@@ -3638,6 +3685,7 @@
|
||||
* @param {number} y
|
||||
*/
|
||||
function flyTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
targetX = x;
|
||||
targetY = y;
|
||||
setState(States.FLYING);
|
||||
@@ -3667,6 +3715,183 @@
|
||||
birb.setY(birdY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {Record<string, SavedBirdPosition>}
|
||||
*/
|
||||
function sanitizeSavedBirdPositions(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
const result = {};
|
||||
for (const [key, position] of Object.entries(value)) {
|
||||
if (!position || typeof position !== "object" || Array.isArray(position)) {
|
||||
continue;
|
||||
}
|
||||
// @ts-expect-error
|
||||
const x = Number(position.x);
|
||||
// @ts-expect-error
|
||||
const y = Number(position.y);
|
||||
// @ts-expect-error
|
||||
const updatedAt = Number(position.updatedAt ?? 0);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||
continue;
|
||||
}
|
||||
result[key] = { x, y, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizePath(path) {
|
||||
return path.split("?")[0].split("#")[0];
|
||||
}
|
||||
|
||||
function trimSavedBirdPositions() {
|
||||
const entries = Object.entries(savedBirdPositions);
|
||||
if (entries.length <= MAX_SAVED_BIRD_POSITIONS) {
|
||||
return;
|
||||
}
|
||||
entries.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
||||
for (let i = 0; i < entries.length - MAX_SAVED_BIRD_POSITIONS; i++) {
|
||||
delete savedBirdPositions[entries[i][0]];
|
||||
}
|
||||
}
|
||||
|
||||
function getBirdPositionScopeKey() {
|
||||
if (birdSessionKey) {
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const existingWindowName = typeof window.name === "string" ? window.name : "";
|
||||
const markerIndex = existingWindowName.indexOf(TAB_SESSION_MARKER);
|
||||
if (markerIndex >= 0) {
|
||||
const end = existingWindowName.indexOf("|", markerIndex);
|
||||
birdSessionKey = end >= 0
|
||||
? existingWindowName.slice(markerIndex, end)
|
||||
: existingWindowName.slice(markerIndex);
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const sessionToken = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
birdSessionKey = `${TAB_SESSION_MARKER}${sessionToken}`;
|
||||
|
||||
try {
|
||||
window.name = existingWindowName
|
||||
? `${existingWindowName}|${birdSessionKey}`
|
||||
: birdSessionKey;
|
||||
} catch {
|
||||
// Ignore if the page blocks changing window.name.
|
||||
}
|
||||
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} [force]
|
||||
*/
|
||||
function saveBirdPosition(force = false) {
|
||||
if (!Number.isFinite(birdX) || !Number.isFinite(birdY)) {
|
||||
return;
|
||||
}
|
||||
if (!force && !birdPositionDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const previous = savedBirdPositions[scopeKey];
|
||||
if (!force && previous) {
|
||||
const movedX = Math.abs(previous.x - birdX);
|
||||
const movedY = Math.abs(previous.y - birdY);
|
||||
if (movedX < BIRD_POSITION_SAVE_MIN_DELTA && movedY < BIRD_POSITION_SAVE_MIN_DELTA) {
|
||||
birdPositionDirty = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
savedBirdPositions[scopeKey] = {
|
||||
x: birdX,
|
||||
y: birdY,
|
||||
updatedAt: now
|
||||
};
|
||||
trimSavedBirdPositions();
|
||||
birdPositionDirty = false;
|
||||
save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function restoreBirdPosition() {
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const saved = savedBirdPositions[scopeKey];
|
||||
if (!saved) {
|
||||
holdRestoredYPosition = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxX = Math.max(0, window.innerWidth - getCanvasWidth());
|
||||
const maxY = getWindowHeight() * 1.5;
|
||||
birdX = Math.min(Math.max(saved.x, 0), maxX);
|
||||
birdY = Math.min(Math.max(saved.y, 0), maxY);
|
||||
|
||||
// Attempt to keep the bird perched if an element still exists near the saved position.
|
||||
focusedElement = getElementAtPosition(birdX, birdY);
|
||||
updateFocusedElementBounds();
|
||||
|
||||
holdRestoredYPosition = focusedElement === null;
|
||||
birdPositionDirty = false;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
|
||||
setState(States.IDLE);
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
function getElementAtPosition(x, y) {
|
||||
const desiredTop = getWindowHeight() - y;
|
||||
let bestElement = null;
|
||||
let bestScore = Number.POSITIVE_INFINITY;
|
||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||
for (const element of elements) {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
if (element.offsetWidth < MIN_FOCUS_ELEMENT_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
const xDistance = Math.abs((rect.left + rect.right) / 2 - x);
|
||||
const yDistance = Math.abs(rect.top - desiredTop);
|
||||
const score = xDistance + yDistance * 1.5;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestElement = element;
|
||||
}
|
||||
}
|
||||
if (bestScore > Math.max(window.innerWidth, getWindowHeight()) * 0.75) {
|
||||
return null;
|
||||
}
|
||||
return bestElement;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
/**
|
||||
|
||||
241
dist/web/birb.js
vendored
241
dist/web/birb.js
vendored
@@ -2086,6 +2086,13 @@
|
||||
* @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SavedBirdPosition
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} updatedAt
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BirbSaveData
|
||||
* @property {string[]} unlockedSpecies
|
||||
@@ -2094,6 +2101,7 @@
|
||||
* @property {string} currentHat
|
||||
* @property {Partial<Settings>} settings
|
||||
* @property {SavedStickyNote[]} [stickyNotes]
|
||||
* @property {Record<string, SavedBirdPosition>} [birdPositions]
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -2575,11 +2583,16 @@
|
||||
|
||||
// Petting boosts
|
||||
const PET_BOOST_DURATION = 1000 * 60 * 5; // 5 minutes
|
||||
const PET_FEATHER_BOOST = 2;
|
||||
const PET_HAT_BOOST = 1.5;
|
||||
const PET_FEATHER_BOOST = 2; // Multiplier for feather effect
|
||||
const PET_HAT_BOOST = 1.5; // Multiplier for hat effect
|
||||
|
||||
// Focus element constraints
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100; // Minimum width (in px) for an element to be considered a valid perch target
|
||||
const BIRD_POSITION_SAVE_INTERVAL = 2000; // How often (ms) we attempt to persist position in normal flow
|
||||
const BIRD_POSITION_SAVE_MIN_DELTA = 6; // Minimum movement (px) compared to last saved position required before writing again
|
||||
const BIRD_POSITION_TRACKING_DELTA = 0.5; // Minimum movement (px) in runtime tracking to mark position as "dirty"
|
||||
const MAX_SAVED_BIRD_POSITIONS = 200; // Maximum number of saved bird positions to keep
|
||||
const TAB_SESSION_MARKER = "__pocket_bird_tab_session__="; // Marker used in localStorage to identify which tab session saved bird positions belong to, to prevent restoring positions from a different tab
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
@@ -2716,7 +2729,7 @@
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
|
||||
new MenuItem("Build 2026.4.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.4.4"); }, undefined, false),
|
||||
new MenuItem("Build 2026.4.6", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.4.6"); }, undefined, false),
|
||||
];
|
||||
|
||||
/** @type {Birb} */
|
||||
@@ -2755,6 +2768,13 @@
|
||||
let currentHat = DEFAULT_HAT;
|
||||
// let visible = true;
|
||||
let lastPetTimestamp = 0;
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
let savedBirdPositions = {};
|
||||
let holdRestoredYPosition = false;
|
||||
let birdPositionDirty = false;
|
||||
let lastTrackedBirdX = birdX;
|
||||
let lastTrackedBirdY = birdY;
|
||||
let birdSessionKey = "";
|
||||
/** @type {StickyNote[]} */
|
||||
let stickyNotes = [];
|
||||
|
||||
@@ -2773,6 +2793,7 @@
|
||||
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
|
||||
unlockedHats = saveData.unlockedHats ?? [DEFAULT_HAT];
|
||||
currentHat = saveData.currentHat ?? DEFAULT_HAT;
|
||||
savedBirdPositions = sanitizeSavedBirdPositions(saveData.birdPositions);
|
||||
stickyNotes = [];
|
||||
|
||||
if (saveData.stickyNotes) {
|
||||
@@ -2807,6 +2828,9 @@
|
||||
left: note.left
|
||||
}));
|
||||
}
|
||||
if (Object.keys(savedBirdPositions).length > 0) {
|
||||
saveData.birdPositions = savedBirdPositions;
|
||||
}
|
||||
|
||||
getContext().putSaveData(saveData);
|
||||
}
|
||||
@@ -2894,19 +2918,26 @@
|
||||
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
|
||||
let lastPath = getContext().getPath().split("?")[0];
|
||||
let lastPath = normalizePath(getContext().getPath());
|
||||
setInterval(() => {
|
||||
const currentPath = getContext().getPath().split("?")[0];
|
||||
const currentPath = normalizePath(getContext().getPath());
|
||||
if (currentPath !== lastPath) {
|
||||
log("Path changed from '" + lastPath + "' to '" + currentPath + "'");
|
||||
saveBirdPosition(true);
|
||||
lastPath = currentPath;
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
restoreBirdPosition();
|
||||
}
|
||||
}, URL_CHECK_INTERVAL);
|
||||
|
||||
setInterval(update, UPDATE_INTERVAL);
|
||||
setInterval(() => saveBirdPosition(), BIRD_POSITION_SAVE_INTERVAL);
|
||||
window.addEventListener("pagehide", () => saveBirdPosition(true));
|
||||
window.addEventListener("beforeunload", () => saveBirdPosition(true));
|
||||
|
||||
flyToElement(true);
|
||||
if (!restoreBirdPosition()) {
|
||||
flyToElement(true);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
@@ -2967,7 +2998,9 @@
|
||||
if (focusedElement && !isWithinHorizontalBounds()) {
|
||||
flyToElement();
|
||||
}
|
||||
birdY = getFocusedY();
|
||||
if (focusedElement || !holdRestoredYPosition) {
|
||||
birdY = getFocusedY();
|
||||
}
|
||||
} else if (currentState === States.FLYING) {
|
||||
// Fly to target location (even if in the air)
|
||||
if (updateParabolicPath(FLY_SPEED, 2)) {
|
||||
@@ -2997,6 +3030,13 @@
|
||||
// Update HTML element position
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
const movedX = Math.abs(birdX - lastTrackedBirdX);
|
||||
const movedY = Math.abs(birdY - lastTrackedBirdY);
|
||||
if (movedX >= BIRD_POSITION_TRACKING_DELTA || movedY >= BIRD_POSITION_TRACKING_DELTA) {
|
||||
birdPositionDirty = true;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3553,6 +3593,7 @@
|
||||
if (frozen) {
|
||||
return false;
|
||||
}
|
||||
holdRestoredYPosition = false;
|
||||
const previousElement = focusedElement;
|
||||
focusedElement = getRandomValidElement();
|
||||
updateFocusedElementBounds();
|
||||
@@ -3569,6 +3610,7 @@
|
||||
* @param {number} y
|
||||
*/
|
||||
function teleportTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
birdX = x;
|
||||
birdY = y;
|
||||
setState(States.IDLE);
|
||||
@@ -3603,11 +3645,16 @@
|
||||
focusedBounds = { left, right, top };
|
||||
}
|
||||
|
||||
function getCanvasWidth() {
|
||||
return birb.getElementWidth();
|
||||
}
|
||||
|
||||
function hop() {
|
||||
if (frozen) {
|
||||
return;
|
||||
}
|
||||
if (currentState === States.IDLE) {
|
||||
holdRestoredYPosition = false;
|
||||
setState(States.HOP);
|
||||
birb.setAnimation(Animations.FLYING);
|
||||
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
|
||||
@@ -3638,6 +3685,7 @@
|
||||
* @param {number} y
|
||||
*/
|
||||
function flyTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
targetX = x;
|
||||
targetY = y;
|
||||
setState(States.FLYING);
|
||||
@@ -3667,6 +3715,183 @@
|
||||
birb.setY(birdY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {Record<string, SavedBirdPosition>}
|
||||
*/
|
||||
function sanitizeSavedBirdPositions(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
const result = {};
|
||||
for (const [key, position] of Object.entries(value)) {
|
||||
if (!position || typeof position !== "object" || Array.isArray(position)) {
|
||||
continue;
|
||||
}
|
||||
// @ts-expect-error
|
||||
const x = Number(position.x);
|
||||
// @ts-expect-error
|
||||
const y = Number(position.y);
|
||||
// @ts-expect-error
|
||||
const updatedAt = Number(position.updatedAt ?? 0);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||
continue;
|
||||
}
|
||||
result[key] = { x, y, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizePath(path) {
|
||||
return path.split("?")[0].split("#")[0];
|
||||
}
|
||||
|
||||
function trimSavedBirdPositions() {
|
||||
const entries = Object.entries(savedBirdPositions);
|
||||
if (entries.length <= MAX_SAVED_BIRD_POSITIONS) {
|
||||
return;
|
||||
}
|
||||
entries.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
||||
for (let i = 0; i < entries.length - MAX_SAVED_BIRD_POSITIONS; i++) {
|
||||
delete savedBirdPositions[entries[i][0]];
|
||||
}
|
||||
}
|
||||
|
||||
function getBirdPositionScopeKey() {
|
||||
if (birdSessionKey) {
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const existingWindowName = typeof window.name === "string" ? window.name : "";
|
||||
const markerIndex = existingWindowName.indexOf(TAB_SESSION_MARKER);
|
||||
if (markerIndex >= 0) {
|
||||
const end = existingWindowName.indexOf("|", markerIndex);
|
||||
birdSessionKey = end >= 0
|
||||
? existingWindowName.slice(markerIndex, end)
|
||||
: existingWindowName.slice(markerIndex);
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const sessionToken = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
birdSessionKey = `${TAB_SESSION_MARKER}${sessionToken}`;
|
||||
|
||||
try {
|
||||
window.name = existingWindowName
|
||||
? `${existingWindowName}|${birdSessionKey}`
|
||||
: birdSessionKey;
|
||||
} catch {
|
||||
// Ignore if the page blocks changing window.name.
|
||||
}
|
||||
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} [force]
|
||||
*/
|
||||
function saveBirdPosition(force = false) {
|
||||
if (!Number.isFinite(birdX) || !Number.isFinite(birdY)) {
|
||||
return;
|
||||
}
|
||||
if (!force && !birdPositionDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const previous = savedBirdPositions[scopeKey];
|
||||
if (!force && previous) {
|
||||
const movedX = Math.abs(previous.x - birdX);
|
||||
const movedY = Math.abs(previous.y - birdY);
|
||||
if (movedX < BIRD_POSITION_SAVE_MIN_DELTA && movedY < BIRD_POSITION_SAVE_MIN_DELTA) {
|
||||
birdPositionDirty = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
savedBirdPositions[scopeKey] = {
|
||||
x: birdX,
|
||||
y: birdY,
|
||||
updatedAt: now
|
||||
};
|
||||
trimSavedBirdPositions();
|
||||
birdPositionDirty = false;
|
||||
save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function restoreBirdPosition() {
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const saved = savedBirdPositions[scopeKey];
|
||||
if (!saved) {
|
||||
holdRestoredYPosition = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxX = Math.max(0, window.innerWidth - getCanvasWidth());
|
||||
const maxY = getWindowHeight() * 1.5;
|
||||
birdX = Math.min(Math.max(saved.x, 0), maxX);
|
||||
birdY = Math.min(Math.max(saved.y, 0), maxY);
|
||||
|
||||
// Attempt to keep the bird perched if an element still exists near the saved position.
|
||||
focusedElement = getElementAtPosition(birdX, birdY);
|
||||
updateFocusedElementBounds();
|
||||
|
||||
holdRestoredYPosition = focusedElement === null;
|
||||
birdPositionDirty = false;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
|
||||
setState(States.IDLE);
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
function getElementAtPosition(x, y) {
|
||||
const desiredTop = getWindowHeight() - y;
|
||||
let bestElement = null;
|
||||
let bestScore = Number.POSITIVE_INFINITY;
|
||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||
for (const element of elements) {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
if (element.offsetWidth < MIN_FOCUS_ELEMENT_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
const xDistance = Math.abs((rect.left + rect.right) / 2 - x);
|
||||
const yDistance = Math.abs(rect.top - desiredTop);
|
||||
const score = xDistance + yDistance * 1.5;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestElement = element;
|
||||
}
|
||||
}
|
||||
if (bestScore > Math.max(window.innerWidth, getWindowHeight()) * 0.75) {
|
||||
return null;
|
||||
}
|
||||
return bestElement;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,6 +51,13 @@ import { HAT, HAT_METADATA, createHatItemAnimation } from './hats.js';
|
||||
* @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SavedBirdPosition
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
* @property {number} updatedAt
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BirbSaveData
|
||||
* @property {string[]} unlockedSpecies
|
||||
@@ -59,6 +66,7 @@ import { HAT, HAT_METADATA, createHatItemAnimation } from './hats.js';
|
||||
* @property {string} currentHat
|
||||
* @property {Partial<Settings>} settings
|
||||
* @property {SavedStickyNote[]} [stickyNotes]
|
||||
* @property {Record<string, SavedBirdPosition>} [birdPositions]
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -118,11 +126,16 @@ const FEATHER_FALL_SPEED = 1;
|
||||
|
||||
// Petting boosts
|
||||
const PET_BOOST_DURATION = 1000 * 60 * 5; // 5 minutes
|
||||
const PET_FEATHER_BOOST = 2;
|
||||
const PET_HAT_BOOST = 1.5;
|
||||
const PET_FEATHER_BOOST = 2; // Multiplier for feather effect
|
||||
const PET_HAT_BOOST = 1.5; // Multiplier for hat effect
|
||||
|
||||
// Focus element constraints
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100; // Minimum width (in px) for an element to be considered a valid perch target
|
||||
const BIRD_POSITION_SAVE_INTERVAL = 2000; // How often (ms) we attempt to persist position in normal flow
|
||||
const BIRD_POSITION_SAVE_MIN_DELTA = 6; // Minimum movement (px) compared to last saved position required before writing again
|
||||
const BIRD_POSITION_TRACKING_DELTA = 0.5; // Minimum movement (px) in runtime tracking to mark position as "dirty"
|
||||
const MAX_SAVED_BIRD_POSITIONS = 200; // Maximum number of saved bird positions to keep
|
||||
const TAB_SESSION_MARKER = "__pocket_bird_tab_session__="; // Marker used in localStorage to identify which tab session saved bird positions belong to, to prevent restoring positions from a different tab
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
@@ -298,6 +311,13 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
let currentHat = DEFAULT_HAT;
|
||||
// let visible = true;
|
||||
let lastPetTimestamp = 0;
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
let savedBirdPositions = {};
|
||||
let holdRestoredYPosition = false;
|
||||
let birdPositionDirty = false;
|
||||
let lastTrackedBirdX = birdX;
|
||||
let lastTrackedBirdY = birdY;
|
||||
let birdSessionKey = "";
|
||||
/** @type {StickyNote[]} */
|
||||
let stickyNotes = [];
|
||||
|
||||
@@ -316,6 +336,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
|
||||
unlockedHats = saveData.unlockedHats ?? [DEFAULT_HAT];
|
||||
currentHat = saveData.currentHat ?? DEFAULT_HAT;
|
||||
savedBirdPositions = sanitizeSavedBirdPositions(saveData.birdPositions);
|
||||
stickyNotes = [];
|
||||
|
||||
if (saveData.stickyNotes) {
|
||||
@@ -350,6 +371,9 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
left: note.left
|
||||
}));
|
||||
}
|
||||
if (Object.keys(savedBirdPositions).length > 0) {
|
||||
saveData.birdPositions = savedBirdPositions;
|
||||
}
|
||||
|
||||
getContext().putSaveData(saveData);
|
||||
}
|
||||
@@ -437,19 +461,26 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
|
||||
let lastPath = getContext().getPath().split("?")[0];
|
||||
let lastPath = normalizePath(getContext().getPath());
|
||||
setInterval(() => {
|
||||
const currentPath = getContext().getPath().split("?")[0];
|
||||
const currentPath = normalizePath(getContext().getPath());
|
||||
if (currentPath !== lastPath) {
|
||||
log("Path changed from '" + lastPath + "' to '" + currentPath + "'");
|
||||
saveBirdPosition(true);
|
||||
lastPath = currentPath;
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
restoreBirdPosition();
|
||||
}
|
||||
}, URL_CHECK_INTERVAL);
|
||||
|
||||
setInterval(update, UPDATE_INTERVAL);
|
||||
setInterval(() => saveBirdPosition(), BIRD_POSITION_SAVE_INTERVAL);
|
||||
window.addEventListener("pagehide", () => saveBirdPosition(true));
|
||||
window.addEventListener("beforeunload", () => saveBirdPosition(true));
|
||||
|
||||
flyToElement(true);
|
||||
if (!restoreBirdPosition()) {
|
||||
flyToElement(true);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
@@ -510,7 +541,9 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
if (focusedElement && !isWithinHorizontalBounds()) {
|
||||
flyToElement();
|
||||
}
|
||||
birdY = getFocusedY();
|
||||
if (focusedElement || !holdRestoredYPosition) {
|
||||
birdY = getFocusedY();
|
||||
}
|
||||
} else if (currentState === States.FLYING) {
|
||||
// Fly to target location (even if in the air)
|
||||
if (updateParabolicPath(FLY_SPEED, 2)) {
|
||||
@@ -540,6 +573,13 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
// Update HTML element position
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
const movedX = Math.abs(birdX - lastTrackedBirdX);
|
||||
const movedY = Math.abs(birdY - lastTrackedBirdY);
|
||||
if (movedX >= BIRD_POSITION_TRACKING_DELTA || movedY >= BIRD_POSITION_TRACKING_DELTA) {
|
||||
birdPositionDirty = true;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1106,6 +1146,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
if (frozen) {
|
||||
return false;
|
||||
}
|
||||
holdRestoredYPosition = false;
|
||||
const previousElement = focusedElement;
|
||||
focusedElement = getRandomValidElement();
|
||||
updateFocusedElementBounds();
|
||||
@@ -1122,6 +1163,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
* @param {number} y
|
||||
*/
|
||||
function teleportTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
birdX = x;
|
||||
birdY = y;
|
||||
setState(States.IDLE);
|
||||
@@ -1165,6 +1207,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
return;
|
||||
}
|
||||
if (currentState === States.IDLE) {
|
||||
holdRestoredYPosition = false;
|
||||
setState(States.HOP);
|
||||
birb.setAnimation(Animations.FLYING);
|
||||
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
|
||||
@@ -1195,6 +1238,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
* @param {number} y
|
||||
*/
|
||||
function flyTo(x, y) {
|
||||
holdRestoredYPosition = false;
|
||||
targetX = x;
|
||||
targetY = y;
|
||||
setState(States.FLYING);
|
||||
@@ -1228,6 +1272,183 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
return Math.random() < 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {Record<string, SavedBirdPosition>}
|
||||
*/
|
||||
function sanitizeSavedBirdPositions(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
/** @type {Record<string, SavedBirdPosition>} */
|
||||
const result = {};
|
||||
for (const [key, position] of Object.entries(value)) {
|
||||
if (!position || typeof position !== "object" || Array.isArray(position)) {
|
||||
continue;
|
||||
}
|
||||
// @ts-expect-error
|
||||
const x = Number(position.x);
|
||||
// @ts-expect-error
|
||||
const y = Number(position.y);
|
||||
// @ts-expect-error
|
||||
const updatedAt = Number(position.updatedAt ?? 0);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||
continue;
|
||||
}
|
||||
result[key] = { x, y, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizePath(path) {
|
||||
return path.split("?")[0].split("#")[0];
|
||||
}
|
||||
|
||||
function trimSavedBirdPositions() {
|
||||
const entries = Object.entries(savedBirdPositions);
|
||||
if (entries.length <= MAX_SAVED_BIRD_POSITIONS) {
|
||||
return;
|
||||
}
|
||||
entries.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
||||
for (let i = 0; i < entries.length - MAX_SAVED_BIRD_POSITIONS; i++) {
|
||||
delete savedBirdPositions[entries[i][0]];
|
||||
}
|
||||
}
|
||||
|
||||
function getBirdPositionScopeKey() {
|
||||
if (birdSessionKey) {
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const existingWindowName = typeof window.name === "string" ? window.name : "";
|
||||
const markerIndex = existingWindowName.indexOf(TAB_SESSION_MARKER);
|
||||
if (markerIndex >= 0) {
|
||||
const end = existingWindowName.indexOf("|", markerIndex);
|
||||
birdSessionKey = end >= 0
|
||||
? existingWindowName.slice(markerIndex, end)
|
||||
: existingWindowName.slice(markerIndex);
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
const sessionToken = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
birdSessionKey = `${TAB_SESSION_MARKER}${sessionToken}`;
|
||||
|
||||
try {
|
||||
window.name = existingWindowName
|
||||
? `${existingWindowName}|${birdSessionKey}`
|
||||
: birdSessionKey;
|
||||
} catch {
|
||||
// Ignore if the page blocks changing window.name.
|
||||
}
|
||||
|
||||
return birdSessionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} [force]
|
||||
*/
|
||||
function saveBirdPosition(force = false) {
|
||||
if (!Number.isFinite(birdX) || !Number.isFinite(birdY)) {
|
||||
return;
|
||||
}
|
||||
if (!force && !birdPositionDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const previous = savedBirdPositions[scopeKey];
|
||||
if (!force && previous) {
|
||||
const movedX = Math.abs(previous.x - birdX);
|
||||
const movedY = Math.abs(previous.y - birdY);
|
||||
if (movedX < BIRD_POSITION_SAVE_MIN_DELTA && movedY < BIRD_POSITION_SAVE_MIN_DELTA) {
|
||||
birdPositionDirty = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
savedBirdPositions[scopeKey] = {
|
||||
x: birdX,
|
||||
y: birdY,
|
||||
updatedAt: now
|
||||
};
|
||||
trimSavedBirdPositions();
|
||||
birdPositionDirty = false;
|
||||
save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function restoreBirdPosition() {
|
||||
const scopeKey = getBirdPositionScopeKey();
|
||||
const saved = savedBirdPositions[scopeKey];
|
||||
if (!saved) {
|
||||
holdRestoredYPosition = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxX = Math.max(0, window.innerWidth - getCanvasWidth());
|
||||
const maxY = getWindowHeight() * 1.5;
|
||||
birdX = Math.min(Math.max(saved.x, 0), maxX);
|
||||
birdY = Math.min(Math.max(saved.y, 0), maxY);
|
||||
|
||||
// Attempt to keep the bird perched if an element still exists near the saved position.
|
||||
focusedElement = getElementAtPosition(birdX, birdY);
|
||||
updateFocusedElementBounds();
|
||||
|
||||
holdRestoredYPosition = focusedElement === null;
|
||||
birdPositionDirty = false;
|
||||
lastTrackedBirdX = birdX;
|
||||
lastTrackedBirdY = birdY;
|
||||
|
||||
setState(States.IDLE);
|
||||
birb.setX(birdX);
|
||||
birb.setY(birdY);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
function getElementAtPosition(x, y) {
|
||||
const desiredTop = getWindowHeight() - y;
|
||||
let bestElement = null;
|
||||
let bestScore = Number.POSITIVE_INFINITY;
|
||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||
for (const element of elements) {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
continue;
|
||||
}
|
||||
if (element.offsetWidth < MIN_FOCUS_ELEMENT_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
const xDistance = Math.abs((rect.left + rect.right) / 2 - x);
|
||||
const yDistance = Math.abs(rect.top - desiredTop);
|
||||
const score = xDistance + yDistance * 1.5;
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
bestElement = element;
|
||||
}
|
||||
}
|
||||
if (bestScore > Math.max(window.innerWidth, getWindowHeight()) * 0.75) {
|
||||
return null;
|
||||
}
|
||||
return bestElement;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
/**
|
||||
@@ -1256,4 +1477,3 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
||||
init();
|
||||
draw();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user