mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-26 04:07:24 +00:00
Add parabolic flying
This commit is contained in:
269
birb.js
269
birb.js
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
const CSS_SCALE = 0.5;
|
||||||
|
|
||||||
class Frame {
|
class Frame {
|
||||||
/**
|
/**
|
||||||
* @param {number[][]} pixels
|
* @param {number[][]} pixels
|
||||||
@@ -78,46 +80,46 @@ class Anim {
|
|||||||
|
|
||||||
const sharedFrames = {
|
const sharedFrames = {
|
||||||
base: new Frame([
|
base: new Frame([
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0],
|
[0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0],
|
||||||
[0, 2, 3, 1, 4, 4, 5, 5, 0, 0, 0, 0],
|
[0, 2, 3, 1, 4, 4, 5, 5, 0, 0, 0],
|
||||||
[0, 0, 3, 3, 4, 5, 5, 5, 5, 5, 0, 0],
|
[0, 0, 3, 3, 4, 5, 5, 5, 5, 5, 0],
|
||||||
[0, 0, 0, 3, 3, 2, 5, 5, 5, 0, 0, 0],
|
[0, 0, 0, 3, 3, 2, 5, 5, 5, 0, 0],
|
||||||
[0, 0, 0, 3, 3, 3, 2, 2, 2, 0, 0, 0],
|
[0, 0, 0, 3, 3, 3, 2, 2, 2, 0, 0],
|
||||||
[0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 0],
|
[0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0]
|
[0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0]
|
||||||
]),
|
]),
|
||||||
headDown: new Frame([
|
headDown: new Frame([
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 3, 4, 4, 4, 5, 5, 0, 0, 0, 0],
|
[0, 0, 3, 4, 4, 4, 5, 5, 0, 0, 0],
|
||||||
[0, 2, 3, 1, 4, 5, 5, 5, 5, 5, 0, 0],
|
[0, 2, 3, 1, 4, 5, 5, 5, 5, 5, 0],
|
||||||
[0, 0, 3, 3, 3, 2, 5, 5, 5, 0, 0, 0],
|
[0, 0, 3, 3, 3, 2, 5, 5, 5, 0, 0],
|
||||||
[0, 0, 0, 3, 3, 3, 2, 2, 2, 0, 0, 0],
|
[0, 0, 0, 3, 3, 3, 2, 2, 2, 0, 0],
|
||||||
[0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 0],
|
[0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0]
|
[0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0]
|
||||||
]),
|
]),
|
||||||
wingsUp: new Frame([
|
wingsUp: new Frame([
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
[0, 0, 0, 4, 4, 0, 0, 5, 5, 5, 0, 0],
|
[0, 0, 0, 4, 4, 0, 0, 5, 5, 5, 0],
|
||||||
[0, 0, 3, 4, 4, 4, 5, 5, 5, 5, 0, 0],
|
[0, 0, 3, 4, 4, 4, 5, 5, 5, 5, 0],
|
||||||
[0, 2, 3, 1, 4, 5, 5, 5, 5, 0, 0, 0],
|
[0, 2, 3, 1, 4, 5, 5, 5, 5, 0, 0],
|
||||||
[0, 0, 3, 3, 3, 2, 5, 5, 2, 0, 0, 0],
|
[0, 0, 3, 3, 3, 2, 5, 5, 2, 0, 0],
|
||||||
[0, 0, 0, 3, 3, 3, 2, 2, 3, 0, 0, 0],
|
[0, 0, 0, 3, 3, 3, 2, 2, 3, 0, 0],
|
||||||
[0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 0],
|
[0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0],
|
||||||
[0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0]
|
[0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0]
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,7 +153,7 @@ const colors = {
|
|||||||
|
|
||||||
// Number of pixels per unit
|
// Number of pixels per unit
|
||||||
const CANVAS_PIXEL_SIZE = 6;
|
const CANVAS_PIXEL_SIZE = 6;
|
||||||
const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE / 2;
|
const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * CSS_SCALE;
|
||||||
|
|
||||||
const styles = `
|
const styles = `
|
||||||
canvas {
|
canvas {
|
||||||
@@ -159,8 +161,9 @@ const styles = `
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 0.25));
|
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 0.25));
|
||||||
transform: scale(0.5);
|
transform: scale(${CSS_SCALE});
|
||||||
transform-origin: bottom;
|
transform-origin: bottom;
|
||||||
|
z-index: 999999999;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -186,59 +189,165 @@ const Directions = {
|
|||||||
const States = {
|
const States = {
|
||||||
IDLE: "idle",
|
IDLE: "idle",
|
||||||
HOP: "hop",
|
HOP: "hop",
|
||||||
|
FLYING: "flying",
|
||||||
};
|
};
|
||||||
|
|
||||||
const HOP_HEIGHT = CANVAS_PIXEL_SIZE * 3;
|
let stateStart = Date.now();
|
||||||
const MAX_HOP_TICKS = 24;
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
let direction = Directions.RIGHT;
|
let currentState = States.IDLE;
|
||||||
let state = States.IDLE;
|
|
||||||
let ticks = 0;
|
|
||||||
let hopTicks = 0;
|
|
||||||
let animStart = Date.now();
|
let animStart = Date.now();
|
||||||
let currentAnimation = Animations.IDLE;
|
let currentAnimation = Animations.IDLE;
|
||||||
let modY = 0;
|
let direction = Directions.RIGHT;
|
||||||
let modX = 0;
|
let ticks = 0;
|
||||||
|
// Bird's current position
|
||||||
|
let birdY = 0;
|
||||||
|
let birdX = 0;
|
||||||
|
// Bird's target position (when flying)
|
||||||
|
let targetX = 0;
|
||||||
|
let targetY = 0;
|
||||||
|
/** @type {HTMLElement|null} */
|
||||||
|
let focusedElement = null;
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
ticks++;
|
ticks++;
|
||||||
modY = 0;
|
if (currentState === States.IDLE) {
|
||||||
if (state === States.IDLE) {
|
if (Math.random() < 0.025) {
|
||||||
if (Math.random() < 0.0025) {
|
|
||||||
hop();
|
hop();
|
||||||
}
|
}
|
||||||
} else if (state === States.HOP) {
|
} else if (currentState === States.HOP) {
|
||||||
hopTicks++;
|
if (updateParabolicPath(0.05)) {
|
||||||
if (hopTicks >= MAX_HOP_TICKS) {
|
setState(States.IDLE);
|
||||||
state = States.IDLE;
|
|
||||||
hopTicks = 0;
|
|
||||||
setAnimation(Animations.IDLE);
|
|
||||||
}
|
}
|
||||||
modX += 1.4 * direction;
|
|
||||||
modY = Math.sin(hopTicks / MAX_HOP_TICKS * Math.PI) * HOP_HEIGHT;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(update, 1000 / 60);
|
setInterval(update, 1000 / 60);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} start
|
||||||
|
* @param {number} end
|
||||||
|
* @param {number} amount
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function linearLerp(start, end, amount) {
|
||||||
|
return start + (end - start) * amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the birds location from the start to the target location on a parabolic path
|
||||||
|
* @param {number} speed The speed of the bird along the path
|
||||||
|
* @param {number} [intensity] The intensity of the parabolic path
|
||||||
|
* @returns {boolean} Whether the bird has reached the target location
|
||||||
|
*/
|
||||||
|
function updateParabolicPath(speed, intensity = 3) {
|
||||||
|
const dx = targetX - startX;
|
||||||
|
const dy = targetY - startY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const time = Date.now() - stateStart;
|
||||||
|
const amount = Math.min(1, time / (distance / speed));
|
||||||
|
const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity);
|
||||||
|
birdX = x;
|
||||||
|
birdY = y;
|
||||||
|
direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT;
|
||||||
|
const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1;
|
||||||
|
if (complete) {
|
||||||
|
birdX = targetX;
|
||||||
|
birdY = targetY;
|
||||||
|
}
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} startX
|
||||||
|
* @param {number} startY
|
||||||
|
* @param {number} endX
|
||||||
|
* @param {number} endY
|
||||||
|
* @param {number} amount
|
||||||
|
* @param {number} [intensity]
|
||||||
|
* @returns {{x: number, y: number}}
|
||||||
|
*/
|
||||||
|
function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
|
||||||
|
const dx = endX - startX;
|
||||||
|
const dy = endY - startY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const angle = Math.atan2(dy, dx);
|
||||||
|
const midX = startX + Math.cos(angle) * distance / 2;
|
||||||
|
const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity;
|
||||||
|
const t = amount;
|
||||||
|
const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX;
|
||||||
|
const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY;
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
function locateTargets() {
|
||||||
|
// Find all images on the page
|
||||||
|
const images = document.querySelectorAll("img");
|
||||||
|
const MIN_SIZE = 100;
|
||||||
|
// Filter out images that are too small
|
||||||
|
const largeImages = Array.from(images).filter((img) => img.width >= MIN_SIZE && img.height >= MIN_SIZE);
|
||||||
|
// Pick a random image
|
||||||
|
const randomImage = largeImages[Math.floor(Math.random() * largeImages.length)];
|
||||||
|
// Get the top left coordinates of the image relative to the window
|
||||||
|
const rect = randomImage.getBoundingClientRect();
|
||||||
|
const x = rect.left;
|
||||||
|
const y = window.innerHeight - rect.top;
|
||||||
|
focusedElement = randomImage;
|
||||||
|
// Move the bird to the top left of the image
|
||||||
|
flyTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
requestAnimationFrame(draw);
|
requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
if (currentState === States.FLYING) {
|
||||||
|
// Fly to target location (even if in the air)
|
||||||
|
if (updateParabolicPath(0.3)) {
|
||||||
|
setState(States.IDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the canvas
|
// Clear the canvas
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
// Draw the bird
|
// Draw the bird
|
||||||
currentAnimation.draw(ctx, direction, animStart);
|
currentAnimation.draw(ctx, direction, animStart);
|
||||||
// Update position
|
// Update position
|
||||||
setX(modX);
|
setX(birdX);
|
||||||
setY(roundToPixel(modY));
|
setY(birdY);
|
||||||
}
|
}
|
||||||
|
|
||||||
draw();
|
draw();
|
||||||
|
|
||||||
|
function getCanvasWidth() {
|
||||||
|
return canvas.width * CSS_SCALE
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCanvasHeight() {
|
||||||
|
return canvas.height * CSS_SCALE
|
||||||
|
}
|
||||||
|
|
||||||
function hop() {
|
function hop() {
|
||||||
if (state === States.IDLE) {
|
if (currentState === States.IDLE) {
|
||||||
state = States.HOP;
|
// 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);
|
setAnimation(Animations.FLYING);
|
||||||
hopTicks = 0;
|
const HOP_DISTANCE = 60 * CSS_SCALE;
|
||||||
|
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > minX) || birdX + HOP_DISTANCE > maxX) {
|
||||||
|
targetX = birdX - HOP_DISTANCE;
|
||||||
|
} else {
|
||||||
|
targetX = birdX + HOP_DISTANCE;
|
||||||
|
}
|
||||||
|
targetY = y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +355,25 @@ canvas.addEventListener("click", () => {
|
|||||||
hop();
|
hop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} x
|
||||||
|
* @param {number} y
|
||||||
|
*/
|
||||||
|
function flyTo(x, y) {
|
||||||
|
targetX = x;
|
||||||
|
targetY = y;
|
||||||
|
setState(States.FLYING);
|
||||||
|
setAnimation(Animations.FLYING);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect any click on the page and print the coordinates
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
// const x = e.clientX;
|
||||||
|
// const y = window.innerHeight - e.clientY;
|
||||||
|
// flyTo(x, y);
|
||||||
|
locateTargets();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current animation and reset the animation timer
|
* Set the current animation and reset the animation timer
|
||||||
* @param {Anim} animation
|
* @param {Anim} animation
|
||||||
@@ -255,6 +383,20 @@ function setAnimation(animation) {
|
|||||||
animStart = Date.now();
|
animStart = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current state and reset the state timer
|
||||||
|
* @param {string} state
|
||||||
|
*/
|
||||||
|
function setState(state) {
|
||||||
|
stateStart = Date.now();
|
||||||
|
startX = birdX;
|
||||||
|
startY = birdY;
|
||||||
|
currentState = state;
|
||||||
|
if (state === States.IDLE) {
|
||||||
|
setAnimation(Animations.IDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} value
|
* @param {number} value
|
||||||
*/
|
*/
|
||||||
@@ -266,6 +408,7 @@ function roundToPixel(value) {
|
|||||||
* @param {number} x
|
* @param {number} x
|
||||||
*/
|
*/
|
||||||
function setX(x) {
|
function setX(x) {
|
||||||
|
x = x - getCanvasWidth() - WINDOW_PIXEL_SIZE / 2;
|
||||||
canvas.style.left = `${x}px`;
|
canvas.style.left = `${x}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
images/bird-1.jpg
Normal file
BIN
images/bird-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
images/bird-2.jpg
Normal file
BIN
images/bird-2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
images/bird-3.jpg
Normal file
BIN
images/bird-3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 862 KiB |
13
index.html
13
index.html
@@ -8,12 +8,23 @@
|
|||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
|
||||||
/* background-color: #363636; */
|
/* background-color: #363636; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#spacer {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div style="text-align: center; padding: 20px;">
|
||||||
|
<h1>This is an example header</h1>
|
||||||
|
<p>This is an example paragraph</p>
|
||||||
|
<img src="./images/bird-1.jpg" alt="Bird 1" style="width: 300px; height: auto; margin: 10px;">
|
||||||
|
<img src="./images/bird-2.jpg" alt="Bird 2" style="width: 300px; height: auto; margin: 10px;">
|
||||||
|
<img src="./images/bird-3.jpg" alt="Bird 3" style="width: 300px; height: auto; margin: 10px;">
|
||||||
|
</div>
|
||||||
|
<div id="spacer"></div>
|
||||||
<script src="birb.js"></script>
|
<script src="birb.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user