diff --git a/birb.js b/birb.js index c204cc6..64c706b 100644 --- a/birb.js +++ b/birb.js @@ -685,6 +685,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI let targetY = 0; /** @type {HTMLElement|null} */ let focusedElement = null; + let focusedBounds = { left: 0, right: 0, top: 0 }; let lastActionTimestamp = Date.now(); /** @type {number[]} */ let petStack = []; @@ -958,11 +959,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI window.addEventListener("scroll", () => { lastActionTimestamp = Date.now(); - // Can't keep up with scrolling on mobile devices so fly down instead - if (isMobile()) { - // focusOnGround(); - } - }); onClick(document, (e) => { @@ -1028,12 +1024,20 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI // Won't be restored on fullscreen exit } - if (currentState === States.IDLE) { - if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART && !isMenuOpen()) { + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) { hop(); - } else if (focusedElement !== null && Math.random() < 1 / (60 * 20) && Date.now() - lastActionTimestamp > AFK_TIME && !isMenuOpen()) { - focusOnElement(); - lastActionTimestamp = Date.now(); + } else if (Date.now() - lastActionTimestamp > AFK_TIME) { + // Idle for a while, do something + if (focusedElement === null) { + // Fly to an element + focusOnElement(); + lastActionTimestamp = Date.now(); + } else if (Math.random() < 1 / (60 * 20)) { + // Fly to another element if idle for a longer while + focusOnElement(); + lastActionTimestamp = Date.now(); + } } } else if (currentState === States.HOP) { if (updateParabolicPath(HOP_SPEED)) { @@ -1057,14 +1061,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI return; } + updateFocusedElementBounds(); + // Update the bird's position if (currentState === States.IDLE) { - if (focusedElement !== null) { - birdY = getFocusedElementY() - 0.5; - if (!isWithinHorizontalBounds()) { - focusOnGround(); - } + if (focusedElement && !isWithinHorizontalBounds()) { + focusOnGround(); } + birdY = getFocusedY(); } else if (currentState === States.FLYING) { // Fly to target location (even if in the air) if (updateParabolicPath(FLY_SPEED)) { @@ -1072,18 +1076,13 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } } - if (focusedElement === null) { - if (Date.now() - lastActionTimestamp > AFK_TIME && !isMenuOpen()) { - // Fly to an element if the user is AFK - focusOnElement(); - lastActionTimestamp = Date.now(); - } - } else if (focusedElement !== null) { - targetY = getFocusedElementY(); - if (targetY < 0 || targetY > window.innerHeight) { - // Fly to ground if the focused element moves out of bounds - focusOnGround(); - } + const oldTargetY = targetY; + targetY = getFocusedY(); + // Adjust startY to account for scrolling + startY += targetY - oldTargetY; + if (targetY < 0 || targetY > window.innerHeight) { + // Fly to ground if the focused element moves out of bounds + focusOnGround(); } ctx.clearRect(0, 0, canvas.width, canvas.height); @@ -1659,34 +1658,37 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } function getFocusedElementRandomX() { - if (focusedElement === null) { - return Math.random() * window.innerWidth; - } - const rect = focusedElement.getBoundingClientRect(); - return Math.random() * (rect.right - rect.left) + rect.left; + return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; } function isWithinHorizontalBounds() { - if (focusedElement === null) { - return true; - } - const rect = focusedElement.getBoundingClientRect(); - return birdX >= rect.left && birdX <= rect.right; + return birdX >= focusedBounds.left && birdX <= focusedBounds.right; } - function getFocusedElementY() { - if (focusedElement === null) { - return 0; - } - const rect = focusedElement.getBoundingClientRect(); - return window.innerHeight - rect.top; + function getFocusedY() { + return getFullWindowHeight() - focusedBounds.top; } + /** + * @returns The render-safe height of the inner browser window + */ + function getSafeWindowHeight() { + // Necessary because iOS 26 Safari is terrible and won't render + // fixed elements behind the address bar + return window.innerHeight; + } + + /** + * @returns The true height of the inner browser window + */ + function getFullWindowHeight() { + return document.documentElement.clientHeight; + } + function focusOnGround() { - if (focusedElement === null) { - return; - } + console.log("Focusing on ground"); focusedElement = null; + focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; flyTo(Math.random() * window.innerWidth, 0); } @@ -1708,7 +1710,23 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)]; focusedElement = randomElement; - flyTo(getFocusedElementRandomX(), getFocusedElementY()); + log("Focusing on element: ", focusedElement); + updateFocusedElementBounds(); + flyTo(getFocusedElementRandomX(), getFocusedY()); + } + + function updateFocusedElementBounds() { + if (focusedElement === null) { + // Update ground location to bottom of window + focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; + return; + } + const rect = focusedElement.getBoundingClientRect(); + focusedBounds = { + left: rect.left, + right: rect.right, + top: rect.top + }; } function getCanvasWidth() { @@ -1724,25 +1742,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI return; } if (currentState === States.IDLE) { - // Determine bounds for hopping - let minX = 0; - let maxX = window.innerWidth; - let y = 0; - if (focusedElement !== null) { - // Hop on the element - const rect = focusedElement.getBoundingClientRect(); - minX = rect.left; - maxX = rect.right; - y = window.innerHeight - rect.top; - } setState(States.HOP); setAnimation(Animations.FLYING); - if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > minX) || birdX + HOP_DISTANCE > maxX) { + if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { targetX = birdX - HOP_DISTANCE; } else { targetX = birdX + HOP_DISTANCE; } - targetY = y; + targetY = getFocusedY(); } } @@ -1769,6 +1776,13 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI setAnimation(Animations.FLYING); } + /** + * @returns {boolean} Whether the bird should be absolutely positioned + */ + function isAbsolute() { + return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); + } + /** * Set the current animation and reset the animation timer * @param {Anim} animation @@ -1790,6 +1804,12 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI if (state === States.IDLE) { setAnimation(Animations.BOB); } + if (isAbsolute()) { + canvas.classList.add("birb-absolute"); + } else { + canvas.classList.remove("birb-absolute"); + } + setY(birdY); } /** @@ -1804,7 +1824,15 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI * @param {number} y */ function setY(y) { - canvas.style.bottom = `${y}px`; + let bottom; + if (isAbsolute()) { + // Position is absolute, convert from fixed + bottom = y - window.scrollY; + } else { + // Position is fixed + bottom = y; + } + canvas.style.bottom = `${bottom}px`; } }); diff --git a/build.js b/build.js index 57688c4..d40d8b5 100644 --- a/build.js +++ b/build.js @@ -34,13 +34,13 @@ try { const userScriptHeader = `// ==UserScript== -// @name Browser Bird +// @name Pocket Bird // @namespace https://idreesinc.com // @version ${version} // @description birb // @author Idrees -// @downloadURL https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js -// @updateURL https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js +// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js +// @updateURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js // @match *://*/* // @grant GM_setValue // @grant GM_getValue diff --git a/dist/birb.js b/dist/birb.js index 0023209..d6825b6 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -74,6 +74,10 @@ const STYLESHEET = `:root { cursor: pointer; } +.birb-absolute { + position: absolute !important; +} + .birb-decoration { image-rendering: pixelated; position: fixed; @@ -1024,6 +1028,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI let targetY = 0; /** @type {HTMLElement|null} */ let focusedElement = null; + let focusedBounds = { left: 0, right: 0, top: 0 }; let lastActionTimestamp = Date.now(); /** @type {number[]} */ let petStack = []; @@ -1297,11 +1302,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI window.addEventListener("scroll", () => { lastActionTimestamp = Date.now(); - // Can't keep up with scrolling on mobile devices so fly down instead - if (isMobile()) { - // focusOnGround(); - } - }); onClick(document, (e) => { @@ -1367,12 +1367,20 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI // Won't be restored on fullscreen exit } - if (currentState === States.IDLE) { - if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART && !isMenuOpen()) { + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) { hop(); - } else if (focusedElement !== null && Math.random() < 1 / (60 * 20) && Date.now() - lastActionTimestamp > AFK_TIME && !isMenuOpen()) { - focusOnElement(); - lastActionTimestamp = Date.now(); + } else if (Date.now() - lastActionTimestamp > AFK_TIME) { + // Idle for a while, do something + if (focusedElement === null) { + // Fly to an element + focusOnElement(); + lastActionTimestamp = Date.now(); + } else if (Math.random() < 1 / (60 * 20)) { + // Fly to another element if idle for a longer while + focusOnElement(); + lastActionTimestamp = Date.now(); + } } } else if (currentState === States.HOP) { if (updateParabolicPath(HOP_SPEED)) { @@ -1396,14 +1404,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI return; } + updateFocusedElementBounds(); + // Update the bird's position if (currentState === States.IDLE) { - if (focusedElement !== null) { - birdY = getFocusedElementY() - 0.5; - if (!isWithinHorizontalBounds()) { - focusOnGround(); - } + if (focusedElement && !isWithinHorizontalBounds()) { + focusOnGround(); } + birdY = getFocusedY(); } else if (currentState === States.FLYING) { // Fly to target location (even if in the air) if (updateParabolicPath(FLY_SPEED)) { @@ -1411,18 +1419,13 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } } - if (focusedElement === null) { - if (Date.now() - lastActionTimestamp > AFK_TIME && !isMenuOpen()) { - // Fly to an element if the user is AFK - focusOnElement(); - lastActionTimestamp = Date.now(); - } - } else if (focusedElement !== null) { - targetY = getFocusedElementY(); - if (targetY < 0 || targetY > window.innerHeight) { - // Fly to ground if the focused element moves out of bounds - focusOnGround(); - } + const oldTargetY = targetY; + targetY = getFocusedY(); + // Adjust startY to account for scrolling + startY += targetY - oldTargetY; + if (targetY < 0 || targetY > window.innerHeight) { + // Fly to ground if the focused element moves out of bounds + focusOnGround(); } ctx.clearRect(0, 0, canvas.width, canvas.height); @@ -1998,34 +2001,37 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } function getFocusedElementRandomX() { - if (focusedElement === null) { - return Math.random() * window.innerWidth; - } - const rect = focusedElement.getBoundingClientRect(); - return Math.random() * (rect.right - rect.left) + rect.left; + return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; } function isWithinHorizontalBounds() { - if (focusedElement === null) { - return true; - } - const rect = focusedElement.getBoundingClientRect(); - return birdX >= rect.left && birdX <= rect.right; + return birdX >= focusedBounds.left && birdX <= focusedBounds.right; } - function getFocusedElementY() { - if (focusedElement === null) { - return 0; - } - const rect = focusedElement.getBoundingClientRect(); - return window.innerHeight - rect.top; + function getFocusedY() { + return getFullWindowHeight() - focusedBounds.top; } + /** + * @returns The render-safe height of the inner browser window + */ + function getSafeWindowHeight() { + // Necessary because iOS 26 Safari is terrible and won't render + // fixed elements behind the address bar + return window.innerHeight; + } + + /** + * @returns The true height of the inner browser window + */ + function getFullWindowHeight() { + return document.documentElement.clientHeight; + } + function focusOnGround() { - if (focusedElement === null) { - return; - } + console.log("Focusing on ground"); focusedElement = null; + focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; flyTo(Math.random() * window.innerWidth, 0); } @@ -2047,7 +2053,23 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)]; focusedElement = randomElement; - flyTo(getFocusedElementRandomX(), getFocusedElementY()); + log("Focusing on element: ", focusedElement); + updateFocusedElementBounds(); + flyTo(getFocusedElementRandomX(), getFocusedY()); + } + + function updateFocusedElementBounds() { + if (focusedElement === null) { + // Update ground location to bottom of window + focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; + return; + } + const rect = focusedElement.getBoundingClientRect(); + focusedBounds = { + left: rect.left, + right: rect.right, + top: rect.top + }; } function getCanvasWidth() { @@ -2063,25 +2085,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI return; } if (currentState === States.IDLE) { - // Determine bounds for hopping - let minX = 0; - let maxX = window.innerWidth; - let y = 0; - if (focusedElement !== null) { - // Hop on the element - const rect = focusedElement.getBoundingClientRect(); - minX = rect.left; - maxX = rect.right; - y = window.innerHeight - rect.top; - } setState(States.HOP); setAnimation(Animations.FLYING); - if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > minX) || birdX + HOP_DISTANCE > maxX) { + if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { targetX = birdX - HOP_DISTANCE; } else { targetX = birdX + HOP_DISTANCE; } - targetY = y; + targetY = getFocusedY(); } } @@ -2108,6 +2119,13 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI setAnimation(Animations.FLYING); } + /** + * @returns {boolean} Whether the bird should be absolutely positioned + */ + function isAbsolute() { + return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); + } + /** * Set the current animation and reset the animation timer * @param {Anim} animation @@ -2129,6 +2147,12 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI if (state === States.IDLE) { setAnimation(Animations.BOB); } + if (isAbsolute()) { + canvas.classList.add("birb-absolute"); + } else { + canvas.classList.remove("birb-absolute"); + } + setY(birdY); } /** @@ -2143,7 +2167,15 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI * @param {number} y */ function setY(y) { - canvas.style.bottom = `${y}px`; + let bottom; + if (isAbsolute()) { + // Position is absolute, convert from fixed + bottom = y - window.scrollY; + } else { + // Position is fixed + bottom = y; + } + canvas.style.bottom = `${bottom}px`; } }); diff --git a/dist/birb.user.js b/dist/birb.user.js index 96e37a8..51bcd82 100644 --- a/dist/birb.user.js +++ b/dist/birb.user.js @@ -1,11 +1,11 @@ // ==UserScript== -// @name Browser Bird +// @name Pocket Bird // @namespace https://idreesinc.com // @version 2025.9.16.1 // @description birb // @author Idrees -// @downloadURL https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js -// @updateURL https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js +// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js +// @updateURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js // @match *://*/* // @grant GM_setValue // @grant GM_getValue @@ -88,6 +88,10 @@ const STYLESHEET = `:root { cursor: pointer; } +.birb-absolute { + position: absolute !important; +} + .birb-decoration { image-rendering: pixelated; position: fixed; @@ -1038,6 +1042,7 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI let targetY = 0; /** @type {HTMLElement|null} */ let focusedElement = null; + let focusedBounds = { left: 0, right: 0, top: 0 }; let lastActionTimestamp = Date.now(); /** @type {number[]} */ let petStack = []; @@ -1311,11 +1316,6 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI window.addEventListener("scroll", () => { lastActionTimestamp = Date.now(); - // Can't keep up with scrolling on mobile devices so fly down instead - if (isMobile()) { - // focusOnGround(); - } - }); onClick(document, (e) => { @@ -1381,12 +1381,20 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI // Won't be restored on fullscreen exit } - if (currentState === States.IDLE) { - if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART && !isMenuOpen()) { + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART) { hop(); - } else if (focusedElement !== null && Math.random() < 1 / (60 * 20) && Date.now() - lastActionTimestamp > AFK_TIME && !isMenuOpen()) { - focusOnElement(); - lastActionTimestamp = Date.now(); + } else if (Date.now() - lastActionTimestamp > AFK_TIME) { + // Idle for a while, do something + if (focusedElement === null) { + // Fly to an element + focusOnElement(); + lastActionTimestamp = Date.now(); + } else if (Math.random() < 1 / (60 * 20)) { + // Fly to another element if idle for a longer while + focusOnElement(); + lastActionTimestamp = Date.now(); + } } } else if (currentState === States.HOP) { if (updateParabolicPath(HOP_SPEED)) { @@ -1410,14 +1418,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI return; } + updateFocusedElementBounds(); + // Update the bird's position if (currentState === States.IDLE) { - if (focusedElement !== null) { - birdY = getFocusedElementY() - 0.5; - if (!isWithinHorizontalBounds()) { - focusOnGround(); - } + if (focusedElement && !isWithinHorizontalBounds()) { + focusOnGround(); } + birdY = getFocusedY(); } else if (currentState === States.FLYING) { // Fly to target location (even if in the air) if (updateParabolicPath(FLY_SPEED)) { @@ -1425,18 +1433,13 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } } - if (focusedElement === null) { - if (Date.now() - lastActionTimestamp > AFK_TIME && !isMenuOpen()) { - // Fly to an element if the user is AFK - focusOnElement(); - lastActionTimestamp = Date.now(); - } - } else if (focusedElement !== null) { - targetY = getFocusedElementY(); - if (targetY < 0 || targetY > window.innerHeight) { - // Fly to ground if the focused element moves out of bounds - focusOnGround(); - } + const oldTargetY = targetY; + targetY = getFocusedY(); + // Adjust startY to account for scrolling + startY += targetY - oldTargetY; + if (targetY < 0 || targetY > window.innerHeight) { + // Fly to ground if the focused element moves out of bounds + focusOnGround(); } ctx.clearRect(0, 0, canvas.width, canvas.height); @@ -2012,34 +2015,37 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } function getFocusedElementRandomX() { - if (focusedElement === null) { - return Math.random() * window.innerWidth; - } - const rect = focusedElement.getBoundingClientRect(); - return Math.random() * (rect.right - rect.left) + rect.left; + return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; } function isWithinHorizontalBounds() { - if (focusedElement === null) { - return true; - } - const rect = focusedElement.getBoundingClientRect(); - return birdX >= rect.left && birdX <= rect.right; + return birdX >= focusedBounds.left && birdX <= focusedBounds.right; } - function getFocusedElementY() { - if (focusedElement === null) { - return 0; - } - const rect = focusedElement.getBoundingClientRect(); - return window.innerHeight - rect.top; + function getFocusedY() { + return getFullWindowHeight() - focusedBounds.top; } + /** + * @returns The render-safe height of the inner browser window + */ + function getSafeWindowHeight() { + // Necessary because iOS 26 Safari is terrible and won't render + // fixed elements behind the address bar + return window.innerHeight; + } + + /** + * @returns The true height of the inner browser window + */ + function getFullWindowHeight() { + return document.documentElement.clientHeight; + } + function focusOnGround() { - if (focusedElement === null) { - return; - } + console.log("Focusing on ground"); focusedElement = null; + focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() }; flyTo(Math.random() * window.innerWidth, 0); } @@ -2061,7 +2067,23 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI } const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)]; focusedElement = randomElement; - flyTo(getFocusedElementRandomX(), getFocusedElementY()); + log("Focusing on element: ", focusedElement); + updateFocusedElementBounds(); + flyTo(getFocusedElementRandomX(), getFocusedY()); + } + + function updateFocusedElementBounds() { + if (focusedElement === null) { + // Update ground location to bottom of window + focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() }; + return; + } + const rect = focusedElement.getBoundingClientRect(); + focusedBounds = { + left: rect.left, + right: rect.right, + top: rect.top + }; } function getCanvasWidth() { @@ -2077,25 +2099,14 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI return; } if (currentState === States.IDLE) { - // Determine bounds for hopping - let minX = 0; - let maxX = window.innerWidth; - let y = 0; - if (focusedElement !== null) { - // Hop on the element - const rect = focusedElement.getBoundingClientRect(); - minX = rect.left; - maxX = rect.right; - y = window.innerHeight - rect.top; - } setState(States.HOP); setAnimation(Animations.FLYING); - if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > minX) || birdX + HOP_DISTANCE > maxX) { + if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { targetX = birdX - HOP_DISTANCE; } else { targetX = birdX + HOP_DISTANCE; } - targetY = y; + targetY = getFocusedY(); } } @@ -2122,6 +2133,13 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI setAnimation(Animations.FLYING); } + /** + * @returns {boolean} Whether the bird should be absolutely positioned + */ + function isAbsolute() { + return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); + } + /** * Set the current animation and reset the animation timer * @param {Anim} animation @@ -2143,6 +2161,12 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI if (state === States.IDLE) { setAnimation(Animations.BOB); } + if (isAbsolute()) { + canvas.classList.add("birb-absolute"); + } else { + canvas.classList.remove("birb-absolute"); + } + setY(birdY); } /** @@ -2157,7 +2181,15 @@ Promise.all([loadSpriteSheetPixels(SPRITE_SHEET), loadSpriteSheetPixels(DECORATI * @param {number} y */ function setY(y) { - canvas.style.bottom = `${y}px`; + let bottom; + if (isAbsolute()) { + // Position is absolute, convert from fixed + bottom = y - window.scrollY; + } else { + // Position is fixed + bottom = y; + } + canvas.style.bottom = `${bottom}px`; } }); diff --git a/stylesheet.css b/stylesheet.css index f4de410..504506c 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -21,6 +21,10 @@ cursor: pointer; } +.birb-absolute { + position: absolute !important; +} + .birb-decoration { image-rendering: pixelated; position: fixed;