diff --git a/birb.js b/birb.js index 57aec66..bd28dd2 100644 --- a/birb.js +++ b/birb.js @@ -338,450 +338,617 @@ 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 layers = { - base: new Layer(getLayer(SPRITE_SHEET, 0)), - down: new Layer(getLayer(SPRITE_SHEET, 1)), - heartOne: new Layer(getLayer(SPRITE_SHEET, 2)), - heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), - heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), - heartFour: new Layer(getLayer(SPRITE_SHEET, 5)), - wingsUp: new Layer(getLayer(SPRITE_SHEET, 6)), - wingsDown: new Layer(getLayer(SPRITE_SHEET, 7)), - happyEye: new Layer(getLayer(SPRITE_SHEET, 8)), -}; - -const decorationLayers = { - mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)), -}; - -const birbFrames = { - base: new Frame([layers.base]), - headDown: new Frame([layers.down]), - wingsDown: new Frame([layers.base, layers.wingsDown]), - wingsUp: new Frame([layers.down, layers.wingsUp]), - heartOne: new Frame([layers.base, layers.happyEye, layers.heartOne]), - 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 = { - mac: new Frame([decorationLayers.mac]), -}; - -const Animations = { - STILL: new Anim([birbFrames.base], [1000]), - BOB: new Anim([ - birbFrames.base, - birbFrames.headDown - ], [ - 420, - 420 - ]), - FLYING: new Anim([ - birbFrames.base, - birbFrames.wingsUp, - birbFrames.headDown, - birbFrames.wingsDown, - ], [ - 40, - 80, - 40, - 80, - ]), - HEART: new Anim([ - birbFrames.heartOne, - birbFrames.heartTwo, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - birbFrames.heartThree, - birbFrames.heartFour, - ], [ - 60, - 80, - 250, - 250, - 250, - 250, - 250, - 250, - ], false), -}; - -const DECORATION_ANIMATIONS = { - mac: new Anim([ - decorationFrames.mac, - ], [ - 1000, - ]), -}; - -const styleElement = document.createElement("style"); -const canvas = document.createElement("canvas"); - -/** @type {CanvasRenderingContext2D} */ -// @ts-ignore -const ctx = canvas.getContext("2d"); - const Directions = { LEFT: -1, RIGHT: 1, }; -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 = []; - -function init() { - if (window !== window.top) { - // Skip installation if within an iframe - return; - } - - styleElement.innerHTML = styles; - document.head.appendChild(styleElement); - - canvas.id = "birb"; - canvas.width = birbFrames.base.pixels[0].length * CANVAS_PIXEL_SIZE; - canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; - document.body.appendChild(canvas); - - window.addEventListener("scroll", () => { - timeOfLastAction = Date.now(); - // Can't keep up with scrolling on mobile devices so fly down instead - if (isMobile()) { - focusOnGround(); - } - - }); - - document.addEventListener("click", (e) => { - timeOfLastAction = Date.now(); - if (e.target instanceof Node && !canvas.contains(e.target) && !document.querySelector(".birb-window")?.contains(e.target)) { - removeStartMenu(); - } - }); - - canvas.addEventListener("click", () => { - insertStartMenu(); - }); - - canvas.addEventListener("mouseover", () => { - timeOfLastAction = Date.now(); - if (currentState === States.IDLE) { - petStack.push(Date.now()); - if (petStack.length > 10) { - petStack.shift(); - } - const pets = petStack.filter((time) => Date.now() - time < 1000).length; - if (pets >= 4) { - setAnimation(Animations.HEART); - // Clear the stack - petStack = []; - } - } - }); - - setInterval(update, 1000 / 60); -} - -function update() { - ticks++; - if (currentState === States.IDLE) { - if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART && !isStartMenuOpen()) { - hop(); - } - } else if (currentState === States.HOP) { - if (updateParabolicPath(HOP_SPEED)) { - setState(States.IDLE); - } - } -} - -function draw() { - requestAnimationFrame(draw); - - // Update the bird's position - if (currentState === States.IDLE) { - if (focusedElement !== null) { - birdY = getFocusedElementY(); - } - } else if (currentState === States.FLYING) { - // Fly to target location (even if in the air) - if (updateParabolicPath(FLY_SPEED)) { - setState(States.IDLE); - } - } - - if (focusedElement === null) { - if (Date.now() - timeOfLastAction > AFK_TIME && !isStartMenuOpen()) { - // Fly to an element if the user is AFK - focusOnElement(); - timeOfLastAction = 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(); - } - } - - ctx.clearRect(0, 0, canvas.width, canvas.height); - if (currentAnimation.draw(ctx, direction, animStart)) { - setAnimation(Animations.STILL); - } - - // Update HTML element position - setX(birdX); - setY(birdY); -} - -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) { - const element = document.createElement("div"); - element.classList.add(className); - if (textContent) { - element.textContent = textContent; - } - if (id) { - element.id = id; - } - return element; -} - -function insertDecoration() { - // Create a canvas element for the decoration - const decorationCanvas = document.createElement("canvas"); - decorationCanvas.classList.add("birb-decoration"); - decorationCanvas.width = DECORATIONS_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - decorationCanvas.height = DECORATIONS_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; - const decorationCtx = decorationCanvas.getContext("2d"); - if (!decorationCtx) { - return; - } - // Draw the decoration - DECORATION_ANIMATIONS.mac.draw(decorationCtx, Directions.LEFT, Date.now()); - // Add the decoration to the page - document.body.appendChild(decorationCanvas); - makeDraggable(decorationCanvas, false); -} - -// insertDecoration(); - -/** - * Add the start menu to the page if it doesn't already exist - */ -function insertStartMenu() { - if (document.querySelector("#" + START_MENU_ID)) { - return; - } - let startMenu = makeElement("birb-window", undefined, START_MENU_ID); - let header = makeElement("birb-window-header"); - header.innerHTML = '
birbOS
'; - let content = makeElement("birb-window-content"); - let petButton = makeElement("birb-window-list-item", "Pet Birb"); - petButton.addEventListener("click", () => { - removeStartMenu(); - pet(); - }); - content.appendChild(petButton); - let fieldGuideButton = makeElement("birb-window-list-item", "Field Guide"); - content.appendChild(fieldGuideButton); - let decorationsButton = makeElement("birb-window-list-item", "Decorations"); - decorationsButton.addEventListener("click", () => { - removeStartMenu(); - insertDecoration(); - }); - content.appendChild(decorationsButton); - content.appendChild(makeElement("birb-window-list-item", "Programs")); - content.appendChild(makeElement("birb-window-separator")); - content.appendChild(makeElement("birb-window-list-item", "Settings")); - startMenu.appendChild(header); - startMenu.appendChild(content); - document.body.appendChild(startMenu); - makeDraggable(document.querySelector(".birb-window-header")); - - let x = birdX; - let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; - const offset = 20; - if (x < window.innerWidth / 2) { - // Left side - x += offset; - } else { - // Right side - x -= startMenu.offsetWidth + offset; - } - if (y > window.innerHeight / 2) { - // Top side - y -= startMenu.offsetHeight + offset + 10; - } else { - // Bottom side - y += offset; - } - startMenu.style.left = `${x}px`; - startMenu.style.top = `${y}px`; -} - -/** - * Remove the start menu from the page - */ -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() { - return document.querySelector("#" + START_MENU_ID) !== null; -} - -/** - * @param {HTMLElement|null} element - */ -function makeDraggable(element, parent = true) { - if (!element) { - return; - } - - let isMouseDown = false; - let offsetX = 0; - let offsetY = 0; - - if (parent) { - element = element.parentElement; - } - - if (!element) { - console.error("Birb: Parent element not found"); - return; - } - - element.addEventListener("mousedown", (e) => { - isMouseDown = true; - offsetX = e.clientX - element.offsetLeft; - offsetY = e.clientY - element.offsetTop; - }); - - document.addEventListener("mouseup", () => { - isMouseDown = false; - }); - - document.addEventListener("mousemove", (e) => { - if (isMouseDown) { - element.style.left = `${e.clientX - offsetX}px`; - element.style.top = `${e.clientY - offsetY}px`; - } - }); -} +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 {string[][]} + * @returns {Promise} */ -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; +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; } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; + 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); } - 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; + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); } -/** - * @param {string[][]} array - * @param {number} sprite - * @param {number} [width] - * @returns {string[][]} - */ -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)); +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)), + heartTwo: new Layer(getLayer(SPRITE_SHEET, 3)), + heartThree: new Layer(getLayer(SPRITE_SHEET, 4)), + heartFour: new Layer(getLayer(SPRITE_SHEET, 5)), + wingsUp: new Layer(getLayer(SPRITE_SHEET, 6)), + wingsDown: new Layer(getLayer(SPRITE_SHEET, 7)), + happyEye: new Layer(getLayer(SPRITE_SHEET, 8)), + }; + + const decorationLayers = { + mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)), + }; + + const birbFrames = { + base: new Frame([layers.base]), + headDown: new Frame([layers.down]), + wingsDown: new Frame([layers.base, layers.wingsDown]), + wingsUp: new Frame([layers.down, layers.wingsUp]), + heartOne: new Frame([layers.base, layers.happyEye, layers.heartOne]), + 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 = { + mac: new Frame([decorationLayers.mac]), + }; + + const Animations = { + STILL: new Anim([birbFrames.base], [1000]), + BOB: new Anim([ + birbFrames.base, + birbFrames.headDown + ], [ + 420, + 420 + ]), + FLYING: new Anim([ + birbFrames.base, + birbFrames.wingsUp, + birbFrames.headDown, + birbFrames.wingsDown, + ], [ + 40, + 80, + 40, + 80, + ]), + HEART: new Anim([ + birbFrames.heartOne, + birbFrames.heartTwo, + birbFrames.heartThree, + birbFrames.heartFour, + birbFrames.heartThree, + birbFrames.heartFour, + birbFrames.heartThree, + birbFrames.heartFour, + ], [ + 60, + 80, + 250, + 250, + 250, + 250, + 250, + 250, + ], false), + }; + + const DECORATION_ANIMATIONS = { + mac: new Anim([ + decorationFrames.mac, + ], [ + 1000, + ]), + }; + + const styleElement = document.createElement("style"); + const canvas = document.createElement("canvas"); + + /** @type {CanvasRenderingContext2D} */ + // @ts-ignore + const ctx = canvas.getContext("2d"); + + 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 = []; + + function init() { + if (window !== window.top) { + // Skip installation if within an iframe + return; + } + + styleElement.innerHTML = styles; + document.head.appendChild(styleElement); + + canvas.id = "birb"; + canvas.width = birbFrames.base.pixels[0].length * CANVAS_PIXEL_SIZE; + canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; + document.body.appendChild(canvas); + + window.addEventListener("scroll", () => { + timeOfLastAction = Date.now(); + // Can't keep up with scrolling on mobile devices so fly down instead + if (isMobile()) { + focusOnGround(); + } + + }); + + document.addEventListener("click", (e) => { + timeOfLastAction = Date.now(); + if (e.target instanceof Node && !canvas.contains(e.target) && !document.querySelector(".birb-window")?.contains(e.target)) { + removeStartMenu(); + } + }); + + canvas.addEventListener("click", () => { + insertStartMenu(); + }); + + canvas.addEventListener("mouseover", () => { + timeOfLastAction = Date.now(); + if (currentState === States.IDLE) { + petStack.push(Date.now()); + if (petStack.length > 10) { + petStack.shift(); + } + const pets = petStack.filter((time) => Date.now() - time < 1000).length; + if (pets >= 4) { + setAnimation(Animations.HEART); + // Clear the stack + petStack = []; + } + } + }); + + setInterval(update, 1000 / 60); } - return layer; -} + + function update() { + ticks++; + if (currentState === States.IDLE) { + if (Math.random() < 1 / (60 * 3) && currentAnimation !== Animations.HEART && !isStartMenuOpen()) { + hop(); + } + } else if (currentState === States.HOP) { + if (updateParabolicPath(HOP_SPEED)) { + setState(States.IDLE); + } + } + } + + function draw() { + requestAnimationFrame(draw); + + // Update the bird's position + if (currentState === States.IDLE) { + if (focusedElement !== null) { + birdY = getFocusedElementY(); + } + } else if (currentState === States.FLYING) { + // Fly to target location (even if in the air) + if (updateParabolicPath(FLY_SPEED)) { + setState(States.IDLE); + } + } + + if (focusedElement === null) { + if (Date.now() - timeOfLastAction > AFK_TIME && !isStartMenuOpen()) { + // Fly to an element if the user is AFK + focusOnElement(); + timeOfLastAction = 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(); + } + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + if (currentAnimation.draw(ctx, direction, animStart)) { + setAnimation(Animations.STILL); + } + + // Update HTML element position + setX(birdX); + setY(birdY); + } + + 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) { + const element = document.createElement("div"); + element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } + return element; + } + + function insertDecoration() { + // Create a canvas element for the decoration + const decorationCanvas = document.createElement("canvas"); + decorationCanvas.classList.add("birb-decoration"); + decorationCanvas.width = DECORATIONS_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + decorationCanvas.height = DECORATIONS_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + const decorationCtx = decorationCanvas.getContext("2d"); + if (!decorationCtx) { + return; + } + // Draw the decoration + DECORATION_ANIMATIONS.mac.draw(decorationCtx, Directions.LEFT, Date.now()); + // Add the decoration to the page + document.body.appendChild(decorationCanvas); + makeDraggable(decorationCanvas, false); + } + + // insertDecoration(); + + /** + * Add the start menu to the page if it doesn't already exist + */ + function insertStartMenu() { + if (document.querySelector("#" + START_MENU_ID)) { + return; + } + let startMenu = makeElement("birb-window", undefined, START_MENU_ID); + let header = makeElement("birb-window-header"); + header.innerHTML = '
birbOS
'; + let content = makeElement("birb-window-content"); + let petButton = makeElement("birb-window-list-item", "Pet Birb"); + petButton.addEventListener("click", () => { + removeStartMenu(); + pet(); + }); + content.appendChild(petButton); + let fieldGuideButton = makeElement("birb-window-list-item", "Field Guide"); + content.appendChild(fieldGuideButton); + let decorationsButton = makeElement("birb-window-list-item", "Decorations"); + decorationsButton.addEventListener("click", () => { + removeStartMenu(); + insertDecoration(); + }); + content.appendChild(decorationsButton); + content.appendChild(makeElement("birb-window-list-item", "Programs")); + content.appendChild(makeElement("birb-window-separator")); + content.appendChild(makeElement("birb-window-list-item", "Settings")); + startMenu.appendChild(header); + startMenu.appendChild(content); + document.body.appendChild(startMenu); + makeDraggable(document.querySelector(".birb-window-header")); + + let x = birdX; + let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10; + const offset = 20; + if (x < window.innerWidth / 2) { + // Left side + x += offset; + } else { + // Right side + x -= startMenu.offsetWidth + offset; + } + if (y > window.innerHeight / 2) { + // Top side + y -= startMenu.offsetHeight + offset + 10; + } else { + // Bottom side + y += offset; + } + startMenu.style.left = `${x}px`; + startMenu.style.top = `${y}px`; + } + + /** + * Remove the start menu from the page + */ + 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() { + return document.querySelector("#" + START_MENU_ID) !== null; + } + + /** + * @param {HTMLElement|null} element + */ + function makeDraggable(element, parent = true) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + + if (parent) { + element = element.parentElement; + } + + if (!element) { + console.error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - element.offsetLeft; + offsetY = e.clientY - element.offsetTop; + }); + + document.addEventListener("mouseup", () => { + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + if (isMouseDown) { + element.style.left = `${e.clientX - offsetX}px`; + element.style.top = `${e.clientY - offsetY}px`; + } + }); + } + + /** + * @param {string[][]} array + * @param {number} sprite + * @param {number} [width] + * @returns {string[][]} + */ + 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; + } + + /** + * 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) { + const dx = targetX - startX; + const dy = targetY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const time = Date.now() - stateStart; + if (distance > Math.max(window.innerWidth, window.innerHeight) / 2) { + speed *= 1.3; + } + const amount = Math.min(1, time / (distance / speed)); + const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity); + birdX = x; + birdY = y; + const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1; + if (complete) { + birdX = targetX; + birdY = targetY; + } else { + 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 @@ -793,34 +960,6 @@ 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) { - const dx = targetX - startX; - const dy = targetY - startY; - const distance = Math.sqrt(dx * dx + dy * dy); - const time = Date.now() - stateStart; - if (distance > Math.max(window.innerWidth, window.innerHeight) / 2) { - speed *= 1.3; - } - const amount = Math.min(1, time / (distance / speed)); - const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity); - birdX = x; - birdY = y; - const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1; - if (complete) { - birdX = targetX; - birdY = targetY; - } else { - direction = targetX > birdX ? Directions.RIGHT : Directions.LEFT; - } - return complete; -} - /** * @param {number} startX * @param {number} startY @@ -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 */ diff --git a/index.html b/index.html index 087096a..eda8e3c 100644 --- a/index.html +++ b/index.html @@ -33,6 +33,7 @@ Bird 3
+ \ No newline at end of file diff --git a/spritesheet-compiler.js b/spritesheet-compiler.js new file mode 100644 index 0000000..8226868 --- /dev/null +++ b/spritesheet-compiler.js @@ -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 {}; \ No newline at end of file