mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-24 19:59:36 +00:00
Fix race condition
This commit is contained in:
545
birb.js
545
birb.js
@@ -338,14 +338,77 @@ const bluebirdColors = {
|
||||
[HEART_SHINE]: "#ff6b6b",
|
||||
};
|
||||
|
||||
const SPRITE_WIDTH = 32;
|
||||
const SPRITE_SHEET_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASAAAAAgCAYAAACy9KU0AAAAAXNSR0IArs4c6QAABBdJREFUeJztnL9LHEEUx79zWkgk3RFwTRvsDKS5KpWpbDSVVQgkjSCYKkfIHyDBIhAhIAiBkOoqsUmVVDbaBFKkkLTxhHBgEQM28aU4Z53d25lddXfm1vt+4Ni5X/vm9t77znfm9hYghBBCCCGEEOIJFboDhOQhImJ7TinFHK4x/PJILiEFQMd+PD0NANg+PEy0ffSBEBIIOWcximQxigbaLnEqK/5iFMne7KzIyspAu+r4pFrGQ3eAFCOr0HyN/I+np/Gq2UTr4cOBtnYhPtjf3cWrZjNuk/rTCN0Bkk96GqK3vkf/UAKwfXiIN71e4rE3vZ5X8SNkZDGnIebWhwCZU7C92dn45msKltUHn7FJtdABFUQyCNEP7UB8oad5pgsx3YePaWC6D1x8JiOHiPQXP42tz9ihHYDZh1ACHFr8SflwBCmIiMj+/fvx/db3715HYHMdKJQDMAuf7oMQj+iRV7sfOgBCrk+hUcyW8KM2CtIBEFIuuUWkiy5db7oWfRQiBZDUCeZrcZwnIpri83wreUyVUhAR8zWVHFyXAIqI8Eslw0BadGwDNkliFSCX+ADA8y2JD3JVYjAMAkhIUcwUXFrvxu1OOwrRncqYGlNy9E9KqTenA2pMRlC3pvDhRf8APnvXxeflBuY3zwBcHOQqxGAYBJCQgohSKiE6N5WpMSXzm2f4vNwoRYSsO8gTAAA4OT6K21rlXVbzMgIhItKYTI4caQHU8TvtKBGXQkQ8Ii7hMeuCeTmI1QEppZTr515TfICkG8riKi7l7G93QAC1+KRjp91Q1v6YAKRknOJjwNSzUOjf8O/vPMLK7y9xGwCeHn/KfG36C/n56w+Ai2laUYZBAAmxsbTezU3mKtd+bPGNmLXIc2cnzwsWpztzA89NLHzFg9UD3Lt7O/O9Wni+bczoffUDXnIapuMPCOBBtgC6+kEbTMoiT4A67QhL61102lFV+WZ1X+ciVIs8v/L1gE535jCxMAOsHmQ+nxYe4Ho+VAuPptOOriSAhJTBx5kn1kHQg/gA6Oe4Lf/rQq4DAvrTmrQLOvlxAgBovt63vfciyBWFx+XAgAsXlkXZAkhImt5aS1bGtwEkp1s+xAeAuAbgurigQmdCp0VAi08Wzdf7pZ0lHVoACcmjt9aKE80QI1/5ZhUh85fhYc7/S/0Vo7fWcr62TPEx44cSQEJqgOhZQFqItAgNcx3krgHpX6POP4hVhKosfB0bgFMEKT5kBFHfNmb6SW8RohuBcRkI2X77UgDEN32/qstEpC5D4YzPS1WQEUbQd0TyYPWg0pr0SlbxZ92vWoBCxSekLui60LcbURPpYrdtqxSgkPEJqQuSQeg+uSh8HpBeh3FtqyR0fELqQN3WPi91VnLuzir88KHjE0LK5z8tFuzphqiPOAAAAABJRU5ErkJggg==";
|
||||
const SPRITE_SHEET = dataUriTo2DArray(SPRITE_SHEET_URI);
|
||||
const DECORATIONS_SPRITE_WIDTH = 48;
|
||||
const DECORATIONS_SPRITE_SHEET_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg==";
|
||||
const DECORATIONS_SPRITE_SHEET = dataUriTo2DArray(DECORATIONS_SPRITE_SHEET_URI, false);
|
||||
const Directions = {
|
||||
LEFT: -1,
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
const layers = {
|
||||
const SPRITE_WIDTH = 32;
|
||||
const DECORATIONS_SPRITE_WIDTH = 48;
|
||||
const SPRITE_SHEET_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASAAAAAgCAYAAACy9KU0AAAAAXNSR0IArs4c6QAABBdJREFUeJztnL9LHEEUx79zWkgk3RFwTRvsDKS5KpWpbDSVVQgkjSCYKkfIHyDBIhAhIAiBkOoqsUmVVDbaBFKkkLTxhHBgEQM28aU4Z53d25lddXfm1vt+4Ni5X/vm9t77znfm9hYghBBCCCGEEOIJFboDhOQhImJ7TinFHK4x/PJILiEFQMd+PD0NANg+PEy0ffSBEBIIOWcximQxigbaLnEqK/5iFMne7KzIyspAu+r4pFrGQ3eAFCOr0HyN/I+np/Gq2UTr4cOBtnYhPtjf3cWrZjNuk/rTCN0Bkk96GqK3vkf/UAKwfXiIN71e4rE3vZ5X8SNkZDGnIebWhwCZU7C92dn45msKltUHn7FJtdABFUQyCNEP7UB8oad5pgsx3YePaWC6D1x8JiOHiPQXP42tz9ihHYDZh1ACHFr8SflwBCmIiMj+/fvx/db3715HYHMdKJQDMAuf7oMQj+iRV7sfOgBCrk+hUcyW8KM2CtIBEFIuuUWkiy5db7oWfRQiBZDUCeZrcZwnIpri83wreUyVUhAR8zWVHFyXAIqI8Eslw0BadGwDNkliFSCX+ADA8y2JD3JVYjAMAkhIUcwUXFrvxu1OOwrRncqYGlNy9E9KqTenA2pMRlC3pvDhRf8APnvXxeflBuY3zwBcHOQqxGAYBJCQgohSKiE6N5WpMSXzm2f4vNwoRYSsO8gTAAA4OT6K21rlXVbzMgIhItKYTI4caQHU8TvtKBGXQkQ8Ii7hMeuCeTmI1QEppZTr515TfICkG8riKi7l7G93QAC1+KRjp91Q1v6YAKRknOJjwNSzUOjf8O/vPMLK7y9xGwCeHn/KfG36C/n56w+Ai2laUYZBAAmxsbTezU3mKtd+bPGNmLXIc2cnzwsWpztzA89NLHzFg9UD3Lt7O/O9Wni+bczoffUDXnIapuMPCOBBtgC6+kEbTMoiT4A67QhL61102lFV+WZ1X+ciVIs8v/L1gE535jCxMAOsHmQ+nxYe4Ho+VAuPptOOriSAhJTBx5kn1kHQg/gA6Oe4Lf/rQq4DAvrTmrQLOvlxAgBovt63vfciyBWFx+XAgAsXlkXZAkhImt5aS1bGtwEkp1s+xAeAuAbgurigQmdCp0VAi08Wzdf7pZ0lHVoACcmjt9aKE80QI1/5ZhUh85fhYc7/S/0Vo7fWcr62TPEx44cSQEJqgOhZQFqItAgNcx3krgHpX6POP4hVhKosfB0bgFMEKT5kBFHfNmb6SW8RohuBcRkI2X77UgDEN32/qstEpC5D4YzPS1WQEUbQd0TyYPWg0pr0SlbxZ92vWoBCxSekLui60LcbURPpYrdtqxSgkPEJqQuSQeg+uSh8HpBeh3FtqyR0fELqQN3WPi91VnLuzir88KHjE0LK5z8tFuzphqiPOAAAAABJRU5ErkJggg==";
|
||||
const DECORATIONS_SPRITE_SHEET_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg==";
|
||||
|
||||
/**
|
||||
* Load the spritesheet and return the pixelmap template
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {Promise<string[][]>}
|
||||
*/
|
||||
function loadSpritesheetPixels(dataUri, templateColors = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.src = dataUri;
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error('Failed to get canvas context'));
|
||||
return;
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
const pixels = imageData.data;
|
||||
const hexArray = [];
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < img.width; x++) {
|
||||
const index = (y * img.width + x) * 4;
|
||||
const r = pixels[index];
|
||||
const g = pixels[index + 1];
|
||||
const b = pixels[index + 2];
|
||||
const a = pixels[index + 3];
|
||||
if (a === 0) {
|
||||
row.push(TRANSPARENT);
|
||||
continue;
|
||||
}
|
||||
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
if (!templateColors) {
|
||||
row.push(hex);
|
||||
continue;
|
||||
}
|
||||
if (SPRITESHEET_COLOR_MAP[hex] === undefined) {
|
||||
console.error(`Unknown color: ${hex}`);
|
||||
row.push(TRANSPARENT);
|
||||
}
|
||||
row.push(SPRITESHEET_COLOR_MAP[hex]);
|
||||
}
|
||||
hexArray.push(row);
|
||||
}
|
||||
resolve(hexArray);
|
||||
};
|
||||
img.onerror = (err) => {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECORATIONS_SPRITE_SHEET_URI, false)]).then(([birbPixels, decorationPixels]) => {
|
||||
const SPRITE_SHEET = birbPixels;
|
||||
const DECORATIONS_SPRITE_SHEET = decorationPixels;
|
||||
|
||||
const layers = {
|
||||
base: new Layer(getLayer(SPRITE_SHEET, 0)),
|
||||
down: new Layer(getLayer(SPRITE_SHEET, 1)),
|
||||
heartOne: new Layer(getLayer(SPRITE_SHEET, 2)),
|
||||
@@ -355,13 +418,13 @@ const layers = {
|
||||
wingsUp: new Layer(getLayer(SPRITE_SHEET, 6)),
|
||||
wingsDown: new Layer(getLayer(SPRITE_SHEET, 7)),
|
||||
happyEye: new Layer(getLayer(SPRITE_SHEET, 8)),
|
||||
};
|
||||
};
|
||||
|
||||
const decorationLayers = {
|
||||
const decorationLayers = {
|
||||
mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)),
|
||||
};
|
||||
};
|
||||
|
||||
const birbFrames = {
|
||||
const birbFrames = {
|
||||
base: new Frame([layers.base]),
|
||||
headDown: new Frame([layers.down]),
|
||||
wingsDown: new Frame([layers.base, layers.wingsDown]),
|
||||
@@ -370,13 +433,13 @@ const birbFrames = {
|
||||
heartTwo: new Frame([layers.base, layers.happyEye, layers.heartTwo]),
|
||||
heartThree: new Frame([layers.base, layers.happyEye, layers.heartThree]),
|
||||
heartFour: new Frame([layers.base, layers.happyEye, layers.heartFour]),
|
||||
};
|
||||
};
|
||||
|
||||
const decorationFrames = {
|
||||
const decorationFrames = {
|
||||
mac: new Frame([decorationLayers.mac]),
|
||||
};
|
||||
};
|
||||
|
||||
const Animations = {
|
||||
const Animations = {
|
||||
STILL: new Anim([birbFrames.base], [1000]),
|
||||
BOB: new Anim([
|
||||
birbFrames.base,
|
||||
@@ -415,57 +478,52 @@ const Animations = {
|
||||
250,
|
||||
250,
|
||||
], false),
|
||||
};
|
||||
};
|
||||
|
||||
const DECORATION_ANIMATIONS = {
|
||||
const DECORATION_ANIMATIONS = {
|
||||
mac: new Anim([
|
||||
decorationFrames.mac,
|
||||
], [
|
||||
1000,
|
||||
]),
|
||||
};
|
||||
};
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
const canvas = document.createElement("canvas");
|
||||
const styleElement = document.createElement("style");
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
/** @type {CanvasRenderingContext2D} */
|
||||
// @ts-ignore
|
||||
const ctx = canvas.getContext("2d");
|
||||
/** @type {CanvasRenderingContext2D} */
|
||||
// @ts-ignore
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const Directions = {
|
||||
LEFT: -1,
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
const States = {
|
||||
const States = {
|
||||
IDLE: "idle",
|
||||
HOP: "hop",
|
||||
FLYING: "flying",
|
||||
};
|
||||
};
|
||||
|
||||
let stateStart = Date.now();
|
||||
let currentState = States.IDLE;
|
||||
let animStart = Date.now();
|
||||
let currentAnimation = Animations.BOB;
|
||||
let direction = Directions.RIGHT;
|
||||
let ticks = 0;
|
||||
// Bird's current position
|
||||
let birdY = 0;
|
||||
let birdX = 40;
|
||||
// Bird's starting position (when flying)
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
// Bird's target position (when flying)
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
/** @type {HTMLElement|null} */
|
||||
let focusedElement = null;
|
||||
// Time of the user's last action on the page
|
||||
let timeOfLastAction = Date.now();
|
||||
// Stack of timestamps for each mouseover, max length of 10
|
||||
let petStack = [];
|
||||
let stateStart = Date.now();
|
||||
let currentState = States.IDLE;
|
||||
let animStart = Date.now();
|
||||
let currentAnimation = Animations.BOB;
|
||||
let direction = Directions.RIGHT;
|
||||
let ticks = 0;
|
||||
// Bird's current position
|
||||
let birdY = 0;
|
||||
let birdX = 40;
|
||||
// Bird's starting position (when flying)
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
// Bird's target position (when flying)
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
/** @type {HTMLElement|null} */
|
||||
let focusedElement = null;
|
||||
// Time of the user's last action on the page
|
||||
let timeOfLastAction = Date.now();
|
||||
// Stack of timestamps for each mouseover, max length of 10
|
||||
let petStack = [];
|
||||
|
||||
function init() {
|
||||
function init() {
|
||||
if (window !== window.top) {
|
||||
// Skip installation if within an iframe
|
||||
return;
|
||||
@@ -516,9 +574,9 @@ function init() {
|
||||
});
|
||||
|
||||
setInterval(update, 1000 / 60);
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
function update() {
|
||||
ticks++;
|
||||
if (currentState === States.IDLE) {
|
||||
if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART && !isStartMenuOpen()) {
|
||||
@@ -529,9 +587,9 @@ function update() {
|
||||
setState(States.IDLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
function draw() {
|
||||
requestAnimationFrame(draw);
|
||||
|
||||
// Update the bird's position
|
||||
@@ -568,19 +626,19 @@ function draw() {
|
||||
// Update HTML element position
|
||||
setX(birdX);
|
||||
setY(birdY);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
draw();
|
||||
init();
|
||||
draw();
|
||||
|
||||
/**
|
||||
/**
|
||||
* Create an HTML element with the specified parameters
|
||||
* @param {string} className
|
||||
* @param {string} [textContent]
|
||||
* @param {string} [id]
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
function makeElement(className, textContent, id) {
|
||||
function makeElement(className, textContent, id) {
|
||||
const element = document.createElement("div");
|
||||
element.classList.add(className);
|
||||
if (textContent) {
|
||||
@@ -590,9 +648,9 @@ function makeElement(className, textContent, id) {
|
||||
element.id = id;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
function insertDecoration() {
|
||||
function insertDecoration() {
|
||||
// Create a canvas element for the decoration
|
||||
const decorationCanvas = document.createElement("canvas");
|
||||
decorationCanvas.classList.add("birb-decoration");
|
||||
@@ -607,14 +665,14 @@ function insertDecoration() {
|
||||
// Add the decoration to the page
|
||||
document.body.appendChild(decorationCanvas);
|
||||
makeDraggable(decorationCanvas, false);
|
||||
}
|
||||
}
|
||||
|
||||
// insertDecoration();
|
||||
// insertDecoration();
|
||||
|
||||
/**
|
||||
/**
|
||||
* Add the start menu to the page if it doesn't already exist
|
||||
*/
|
||||
function insertStartMenu() {
|
||||
function insertStartMenu() {
|
||||
if (document.querySelector("#" + START_MENU_ID)) {
|
||||
return;
|
||||
}
|
||||
@@ -663,29 +721,29 @@ function insertStartMenu() {
|
||||
}
|
||||
startMenu.style.left = `${x}px`;
|
||||
startMenu.style.top = `${y}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Remove the start menu from the page
|
||||
*/
|
||||
function removeStartMenu() {
|
||||
function removeStartMenu() {
|
||||
const startMenu = document.querySelector("#" + START_MENU_ID);
|
||||
if (startMenu) {
|
||||
startMenu.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @returns {boolean} Whether the start menu element is on the page
|
||||
*/
|
||||
function isStartMenuOpen() {
|
||||
function isStartMenuOpen() {
|
||||
return document.querySelector("#" + START_MENU_ID) !== null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @param {HTMLElement|null} element
|
||||
*/
|
||||
function makeDraggable(element, parent = true) {
|
||||
function makeDraggable(element, parent = true) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
@@ -719,87 +777,30 @@ function makeDraggable(element, parent = true) {
|
||||
element.style.top = `${e.clientY - offsetY}px`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {string[][]}
|
||||
*/
|
||||
function dataUriTo2DArray(dataUri, templateColors = true) {
|
||||
const img = new Image();
|
||||
img.src = dataUri;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return [];
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
const pixels = imageData.data;
|
||||
const hexArray = [];
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < img.width; x++) {
|
||||
const index = (y * img.width + x) * 4;
|
||||
const r = pixels[index];
|
||||
const g = pixels[index + 1];
|
||||
const b = pixels[index + 2];
|
||||
const a = pixels[index + 3];
|
||||
if (a === 0) {
|
||||
row.push(TRANSPARENT);
|
||||
continue;
|
||||
}
|
||||
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
if (!templateColors) {
|
||||
row.push(hex);
|
||||
continue;
|
||||
}
|
||||
if (SPRITESHEET_COLOR_MAP[hex] === undefined) {
|
||||
console.error(`Unknown color: ${hex}`);
|
||||
row.push(TRANSPARENT);
|
||||
}
|
||||
row.push(SPRITESHEET_COLOR_MAP[hex]);
|
||||
}
|
||||
hexArray.push(row);
|
||||
}
|
||||
return hexArray;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @param {string[][]} array
|
||||
* @param {number} sprite
|
||||
* @param {number} [width]
|
||||
* @returns {string[][]}
|
||||
*/
|
||||
function getLayer(array, sprite, width = SPRITE_WIDTH) {
|
||||
function getLayer(array, sprite, width = SPRITE_WIDTH) {
|
||||
// From an array of a horizontal sprite sheet, get the layer for a specific sprite
|
||||
const layer = [];
|
||||
for (let y = 0; y < width; y++) {
|
||||
layer.push(array[y].slice(sprite * width, (sprite + 1) * width));
|
||||
}
|
||||
return layer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = 2.5) {
|
||||
function updateParabolicPath(speed, intensity = 2.5) {
|
||||
const dx = targetX - startX;
|
||||
const dy = targetY - startY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
@@ -819,6 +820,144 @@ function updateParabolicPath(speed, intensity = 2.5) {
|
||||
direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT;
|
||||
}
|
||||
return complete;
|
||||
}
|
||||
|
||||
function getFocusedElementRandomX() {
|
||||
if (focusedElement === null) {
|
||||
return Math.random() * window.innerWidth;
|
||||
}
|
||||
const rect = focusedElement.getBoundingClientRect();
|
||||
return Math.random() * (rect.right - rect.left) + rect.left;
|
||||
}
|
||||
|
||||
function getFocusedElementY() {
|
||||
if (focusedElement === null) {
|
||||
return 0;
|
||||
}
|
||||
const rect = focusedElement.getBoundingClientRect();
|
||||
return window.innerHeight - rect.top;
|
||||
}
|
||||
|
||||
function focusOnGround() {
|
||||
if (focusedElement === null) {
|
||||
return;
|
||||
}
|
||||
focusedElement = null;
|
||||
flyTo(Math.random() * window.innerWidth, 0);
|
||||
}
|
||||
|
||||
function focusOnElement() {
|
||||
const images = document.querySelectorAll("img");
|
||||
const inWindow = Array.from(images).filter((img) => {
|
||||
const rect = img.getBoundingClientRect();
|
||||
return rect.left >= 0 && rect.top >= 0 + 100 && rect.right <= window.innerWidth && rect.top <= window.innerHeight;
|
||||
});
|
||||
const MIN_SIZE = 100;
|
||||
const largeImages = Array.from(inWindow).filter((img) => img !== focusedElement && img.width >= MIN_SIZE && img.height >= MIN_SIZE);
|
||||
if (largeImages.length === 0) {
|
||||
return;
|
||||
}
|
||||
const randomImage = largeImages[Math.floor(Math.random() * largeImages.length)];
|
||||
focusedElement = randomImage;
|
||||
flyTo(getFocusedElementRandomX(), getFocusedElementY());
|
||||
}
|
||||
|
||||
function getCanvasWidth() {
|
||||
return canvas.width * CSS_SCALE
|
||||
}
|
||||
|
||||
function getCanvasHeight() {
|
||||
return canvas.height * CSS_SCALE
|
||||
}
|
||||
|
||||
function hop() {
|
||||
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) {
|
||||
targetX = birdX - HOP_DISTANCE;
|
||||
} else {
|
||||
targetX = birdX + HOP_DISTANCE;
|
||||
}
|
||||
targetY = y;
|
||||
}
|
||||
}
|
||||
|
||||
function pet() {
|
||||
if (currentState === States.IDLE) {
|
||||
setAnimation(Animations.HEART);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
function flyTo(x, y) {
|
||||
targetX = x;
|
||||
targetY = y;
|
||||
setState(States.FLYING);
|
||||
setAnimation(Animations.FLYING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current animation and reset the animation timer
|
||||
* @param {Anim} animation
|
||||
*/
|
||||
function setAnimation(animation) {
|
||||
currentAnimation = animation;
|
||||
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.BOB);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
*/
|
||||
function setX(x) {
|
||||
let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2));
|
||||
canvas.style.left = `${x + mod}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} y
|
||||
*/
|
||||
function setY(y) {
|
||||
canvas.style.bottom = `${y}px`;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
* @param {number} amount
|
||||
* @returns {number}
|
||||
*/
|
||||
function linearLerp(start, end, amount) {
|
||||
return start + (end - start) * amount;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -843,118 +982,6 @@ function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function getFocusedElementRandomX() {
|
||||
if (focusedElement === null) {
|
||||
return Math.random() * window.innerWidth;
|
||||
}
|
||||
const rect = focusedElement.getBoundingClientRect();
|
||||
return Math.random() * (rect.right - rect.left) + rect.left;
|
||||
}
|
||||
|
||||
function getFocusedElementY() {
|
||||
if (focusedElement === null) {
|
||||
return 0;
|
||||
}
|
||||
const rect = focusedElement.getBoundingClientRect();
|
||||
return window.innerHeight - rect.top;
|
||||
}
|
||||
|
||||
function focusOnGround() {
|
||||
if (focusedElement === null) {
|
||||
return;
|
||||
}
|
||||
focusedElement = null;
|
||||
flyTo(Math.random() * window.innerWidth, 0);
|
||||
}
|
||||
|
||||
function focusOnElement() {
|
||||
const images = document.querySelectorAll("img");
|
||||
const inWindow = Array.from(images).filter((img) => {
|
||||
const rect = img.getBoundingClientRect();
|
||||
return rect.left >= 0 && rect.top >= 0 + 100 && rect.right <= window.innerWidth && rect.top <= window.innerHeight;
|
||||
});
|
||||
const MIN_SIZE = 100;
|
||||
const largeImages = Array.from(inWindow).filter((img) => img !== focusedElement && img.width >= MIN_SIZE && img.height >= MIN_SIZE);
|
||||
if (largeImages.length === 0) {
|
||||
return;
|
||||
}
|
||||
const randomImage = largeImages[Math.floor(Math.random() * largeImages.length)];
|
||||
focusedElement = randomImage;
|
||||
flyTo(getFocusedElementRandomX(), getFocusedElementY());
|
||||
}
|
||||
|
||||
function getCanvasWidth() {
|
||||
return canvas.width * CSS_SCALE
|
||||
}
|
||||
|
||||
function getCanvasHeight() {
|
||||
return canvas.height * CSS_SCALE
|
||||
}
|
||||
|
||||
function hop() {
|
||||
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) {
|
||||
targetX = birdX - HOP_DISTANCE;
|
||||
} else {
|
||||
targetX = birdX + HOP_DISTANCE;
|
||||
}
|
||||
targetY = y;
|
||||
}
|
||||
}
|
||||
|
||||
function pet() {
|
||||
if (currentState === States.IDLE) {
|
||||
setAnimation(Animations.HEART);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
function flyTo(x, y) {
|
||||
targetX = x;
|
||||
targetY = y;
|
||||
setState(States.FLYING);
|
||||
setAnimation(Animations.FLYING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current animation and reset the animation timer
|
||||
* @param {Anim} animation
|
||||
*/
|
||||
function setAnimation(animation) {
|
||||
currentAnimation = animation;
|
||||
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.BOB);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
@@ -962,22 +989,6 @@ function roundToPixel(value) {
|
||||
return Math.round(value / WINDOW_PIXEL_SIZE) * WINDOW_PIXEL_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
*/
|
||||
function setX(x) {
|
||||
let mod = getCanvasWidth() / -2 - (WINDOW_PIXEL_SIZE * (direction === Directions.RIGHT ? 2 : -2));
|
||||
canvas.style.left = `${x + mod}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} y
|
||||
*/
|
||||
function setY(y) {
|
||||
canvas.style.bottom = `${y}px`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether the user is on a mobile device
|
||||
*/
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<img src="./images/bird-3.jpg" alt="Bird 3" style="width: 300px; height: auto; margin: 10px;">
|
||||
</div>
|
||||
<div id="spacer"></div>
|
||||
<!-- <script type="module" src="spritesheet-compiler.js"></script> -->
|
||||
<script src="birb.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
121
spritesheet-compiler.js
Normal file
121
spritesheet-compiler.js
Normal file
@@ -0,0 +1,121 @@
|
||||
|
||||
// @ts-check
|
||||
|
||||
const TRANSPARENT = 0;
|
||||
const OUTLINE = 1;
|
||||
const BORDER = 2;
|
||||
const FOOT = 3;
|
||||
const BEAK = 4;
|
||||
const EYE = 5;
|
||||
const FACE = 6;
|
||||
const BELLY = 7;
|
||||
const UNDERBELLY = 8;
|
||||
const WING = 9;
|
||||
const WING_EDGE = 10;
|
||||
const HEART = 11;
|
||||
const HEART_BORDER = 12;
|
||||
const HEART_SHINE = 13;
|
||||
|
||||
const SPRITESHEET_COLOR_MAP = {
|
||||
"transparent": TRANSPARENT,
|
||||
"#ffffff": BORDER,
|
||||
"#000000": OUTLINE,
|
||||
"#010a19": BEAK,
|
||||
"#190301": EYE,
|
||||
"#af8e75": FOOT,
|
||||
"#639bff": FACE,
|
||||
"#f8b143": BELLY,
|
||||
"#ec8637": UNDERBELLY,
|
||||
"#578ae6": WING,
|
||||
"#326ed9": WING_EDGE,
|
||||
"#c82e2e": HEART,
|
||||
"#501a1a": HEART_BORDER,
|
||||
"#ff6b6b": HEART_SHINE
|
||||
};
|
||||
|
||||
const SPRITE_SHEET_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASAAAAAgCAYAAACy9KU0AAAAAXNSR0IArs4c6QAABBdJREFUeJztnL9LHEEUx79zWkgk3RFwTRvsDKS5KpWpbDSVVQgkjSCYKkfIHyDBIhAhIAiBkOoqsUmVVDbaBFKkkLTxhHBgEQM28aU4Z53d25lddXfm1vt+4Ni5X/vm9t77znfm9hYghBBCCCGEEOIJFboDhOQhImJ7TinFHK4x/PJILiEFQMd+PD0NANg+PEy0ffSBEBIIOWcximQxigbaLnEqK/5iFMne7KzIyspAu+r4pFrGQ3eAFCOr0HyN/I+np/Gq2UTr4cOBtnYhPtjf3cWrZjNuk/rTCN0Bkk96GqK3vkf/UAKwfXiIN71e4rE3vZ5X8SNkZDGnIebWhwCZU7C92dn45msKltUHn7FJtdABFUQyCNEP7UB8oad5pgsx3YePaWC6D1x8JiOHiPQXP42tz9ihHYDZh1ACHFr8SflwBCmIiMj+/fvx/db3715HYHMdKJQDMAuf7oMQj+iRV7sfOgBCrk+hUcyW8KM2CtIBEFIuuUWkiy5db7oWfRQiBZDUCeZrcZwnIpri83wreUyVUhAR8zWVHFyXAIqI8Eslw0BadGwDNkliFSCX+ADA8y2JD3JVYjAMAkhIUcwUXFrvxu1OOwrRncqYGlNy9E9KqTenA2pMRlC3pvDhRf8APnvXxeflBuY3zwBcHOQqxGAYBJCQgohSKiE6N5WpMSXzm2f4vNwoRYSsO8gTAAA4OT6K21rlXVbzMgIhItKYTI4caQHU8TvtKBGXQkQ8Ii7hMeuCeTmI1QEppZTr515TfICkG8riKi7l7G93QAC1+KRjp91Q1v6YAKRknOJjwNSzUOjf8O/vPMLK7y9xGwCeHn/KfG36C/n56w+Ai2laUYZBAAmxsbTezU3mKtd+bPGNmLXIc2cnzwsWpztzA89NLHzFg9UD3Lt7O/O9Wni+bczoffUDXnIapuMPCOBBtgC6+kEbTMoiT4A67QhL61102lFV+WZ1X+ciVIs8v/L1gE535jCxMAOsHmQ+nxYe4Ho+VAuPptOOriSAhJTBx5kn1kHQg/gA6Oe4Lf/rQq4DAvrTmrQLOvlxAgBovt63vfciyBWFx+XAgAsXlkXZAkhImt5aS1bGtwEkp1s+xAeAuAbgurigQmdCp0VAi08Wzdf7pZ0lHVoACcmjt9aKE80QI1/5ZhUh85fhYc7/S/0Vo7fWcr62TPEx44cSQEJqgOhZQFqItAgNcx3krgHpX6POP4hVhKosfB0bgFMEKT5kBFHfNmb6SW8RohuBcRkI2X77UgDEN32/qstEpC5D4YzPS1WQEUbQd0TyYPWg0pr0SlbxZ92vWoBCxSekLui60LcbURPpYrdtqxSgkPEJqQuSQeg+uSh8HpBeh3FtqyR0fELqQN3WPi91VnLuzir88KHjE0LK5z8tFuzphqiPOAAAAABJRU5ErkJggg==";
|
||||
console.log(stringifyPixels(compress(loadSpritesheetPixels(SPRITE_SHEET_URI))))
|
||||
|
||||
function compress(pixels) {
|
||||
let counts = [];
|
||||
let rowCounts = [];
|
||||
let count = null;
|
||||
for (let row of pixels) {
|
||||
console.log("Row length: " + row.length);
|
||||
for (let pixel of row) {
|
||||
if (count === null) {
|
||||
count = [pixel, 1];
|
||||
} else if (pixel === count[0]) {
|
||||
count[1] = count[1] + 1;
|
||||
} else {
|
||||
rowCounts.push(count);
|
||||
count = [pixel, 1];
|
||||
}
|
||||
}
|
||||
rowCounts.push(count);
|
||||
counts.push([...rowCounts]);
|
||||
rowCounts = [];
|
||||
count = null;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function stringifyPixels(pixels) {
|
||||
// Add newlines between every row
|
||||
let str = "";
|
||||
for (let row of pixels) {
|
||||
str += JSON.stringify(row) + ",\n";
|
||||
}
|
||||
str = str.slice(0, -2);
|
||||
return "[" + str + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the spritesheet and return the pixelmap template
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {string[][]}
|
||||
*/
|
||||
function loadSpritesheetPixels(dataUri, templateColors = true) {
|
||||
const img = new Image();
|
||||
img.src = dataUri;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return [];
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
const pixels = imageData.data;
|
||||
const hexArray = [];
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < img.width; x++) {
|
||||
const index = (y * img.width + x) * 4;
|
||||
const r = pixels[index];
|
||||
const g = pixels[index + 1];
|
||||
const b = pixels[index + 2];
|
||||
const a = pixels[index + 3];
|
||||
if (a === 0) {
|
||||
row.push(TRANSPARENT);
|
||||
continue;
|
||||
}
|
||||
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
if (!templateColors) {
|
||||
row.push(hex);
|
||||
continue;
|
||||
}
|
||||
if (SPRITESHEET_COLOR_MAP[hex] === undefined) {
|
||||
console.error(`Unknown color: ${hex}`);
|
||||
row.push(TRANSPARENT);
|
||||
}
|
||||
row.push(SPRITESHEET_COLOR_MAP[hex]);
|
||||
}
|
||||
hexArray.push(row);
|
||||
}
|
||||
return hexArray;
|
||||
}
|
||||
|
||||
export {};
|
||||
Reference in New Issue
Block a user