From 80d7cfc72c80b98d841c796c1a6a6350744cbd38 Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Mon, 30 Dec 2024 14:20:20 -0500 Subject: [PATCH] Add layer tags and tufted titmouse --- birb.js | 161 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 108 insertions(+), 53 deletions(-) diff --git a/birb.js b/birb.js index 503273d..c4461cc 100644 --- a/birb.js +++ b/birb.js @@ -182,32 +182,31 @@ const styles = ` } #${FIELD_GUIDE_ID} { - width: 230px; + width: 260px; } .birb-grid-content { + width: 100%; display: flex; flex-wrap: wrap; - justify-content: center; + justify-content: space-between; flex-direction: row; + padding-top: 4px; + padding-bottom: 4px; } .birb-grid-item { width: 64px; height: 64px; overflow: hidden; - margin: 6px; + margin-top: 6px; + margin-bottom: 6px; display: flex; justify-content: center; align-items: center; cursor: pointer; } - .birb-grid-item-locked { - filter: grayscale(100%); - cursor: auto; - } - .birb-grid-item canvas { image-rendering: pixelated; transform: scale(2); @@ -220,6 +219,15 @@ const styles = ` background: rgba(255, 221, 177, 0.5); } + .birb-grid-item-locked { + cursor: auto; + filter: grayscale(100%) sepia(30%); + } + + .birb-grid-item-locked canvas { + filter: contrast(90%); + } + .birb-field-guide-description { box-sizing: border-box; width: 100%; @@ -250,32 +258,51 @@ const styles = ` class Layer { /** * @param {string[][]} pixels + * @param {string} [tag] */ - constructor(pixels) { + constructor(pixels, tag="default") { this.pixels = pixels; + this.tag = tag; } } class Frame { + + #pixelsByTag = {}; + /** * @param {Layer[]} layers */ constructor(layers) { - let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0); - this.pixels = layers[0].pixels.map(row => row.slice()); - // Pad from top with transparent pixels - while (this.pixels.length < maxHeight) { - this.pixels.unshift(new Array(this.pixels[0].length).fill(TRANSPARENT)); + /** @type {Set} */ + let tags = new Set(); + for (let layer of layers) { + tags.add(layer.tag); } - // Combine layers - for (let i = 1; i < layers.length; i++) { - let layerPixels = layers[i].pixels; - let topMargin = maxHeight - layerPixels.length; - for (let y = 0; y < layerPixels.length; y++) { - for (let x = 0; x < layerPixels[y].length; x++) { - this.pixels[y + topMargin][x] = layerPixels[y][x] !== TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; + tags.add("default"); + for (let tag of tags) { + let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0); + if (layers[0].tag !== "default") { + throw new Error("First layer must have the 'default' tag"); + } + this.pixels = layers[0].pixels.map(row => row.slice()); + // Pad from top with transparent pixels + while (this.pixels.length < maxHeight) { + this.pixels.unshift(new Array(this.pixels[0].length).fill(TRANSPARENT)); + } + // Combine layers + for (let i = 1; i < layers.length; i++) { + if (layers[i].tag === "default" || layers[i].tag === tag) { + let layerPixels = layers[i].pixels; + let topMargin = maxHeight - layerPixels.length; + for (let y = 0; y < layerPixels.length; y++) { + for (let x = 0; x < layerPixels[y].length; x++) { + this.pixels[y + topMargin][x] = layerPixels[y][x] !== TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; + } + } } } + this.#pixelsByTag[tag] = this.pixels.map(row => row.slice()); } // Surround non-transparent pixels with border // for (let y = 0; y < this.pixels.length; y++) { @@ -287,30 +314,39 @@ class Frame { // } } - hasAdjacent(x, y) { - for (let i = -1; i <= 1; i++) { - for (let j = -1; j <= 1; j++) { - if (i === 0 && j === 0) { - continue; - } - if (this.pixels[y + i] && this.pixels[y + i][x + j] && this.pixels[y + i][x + j] !== TRANSPARENT && this.pixels[y + i][x + j] !== BORDER) { - return true; - } - } - } - return false + /** + * @param {string} [tag] + * @returns {string[][]} + */ + getPixels(tag="default") { + return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"]; } + // hasAdjacent(x, y) { + // for (let i = -1; i <= 1; i++) { + // for (let j = -1; j <= 1; j++) { + // if (i === 0 && j === 0) { + // continue; + // } + // if (this.#pixels[y + i] && this.#pixels[y + i][x + j] && this.#pixels[y + i][x + j] !== TRANSPARENT && this.#pixels[y + i][x + j] !== BORDER) { + // return true; + // } + // } + // } + // return false + // } + /** * @param {CanvasRenderingContext2D} ctx * @param {number} direction * @param {BirdType} [theme] */ draw(ctx, direction, theme) { - for (let y = 0; y < this.pixels.length; y++) { - const row = this.pixels[y]; - for (let x = 0; x < this.pixels[y].length; x++) { - const cell = direction === Directions.LEFT ? row[x] : row[this.pixels[y].length - x - 1]; + const pixels = this.getPixels(theme?.tags[0]); + for (let y = 0; y < pixels.length; y++) { + const row = pixels[y]; + for (let x = 0; x < pixels[y].length; x++) { + const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1]; ctx.fillStyle = theme?.colors[cell] ?? cell; ctx.fillRect(x * CANVAS_PIXEL_SIZE, y * CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE); }; @@ -400,8 +436,9 @@ class BirdType { * @param {string} name * @param {string} description * @param {Record} colors + * @param {string[]} [tags] */ - constructor(name, description, colors) { + constructor(name, description, colors, tags=[]) { this.name = name; this.description = description; const defaultColors = { @@ -414,6 +451,7 @@ class BirdType { [FEATHER_SPINE]: "#373737", }; this.colors = { ...defaultColors, ...colors }; + this.tags = tags; } } @@ -440,6 +478,17 @@ const species = { [WING]: "#e3cabd", [WING_EDGE]: "#9b8b82", }), + tuftedTitmouse: new BirdType("Tufted Titmouse", + "Native to the eastern United States, full of personality, and my wife's favorite bird.", { + [BEAK]: "#000000", + [FOOT]: "#af8e75", + [EYE]: "#000000", + [FACE]: "#c7cad7", + [BELLY]: "#e4e5eb", + [UNDERBELLY]: "#d7cfcb", + [WING]: "#b1b5c5", + [WING_EDGE]: "#9d9fa9", + }, ["tuft"]), }; @@ -451,7 +500,7 @@ const Directions = { const SPRITE_WIDTH = 32; const DECORATIONS_SPRITE_WIDTH = 48; const FEATHER_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_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAWAAAAAgCAYAAAAsTqKUAAAAAXNSR0IArs4c6QAABKFJREFUeJztnL9rHEccxd+cXRwx7o6AVmmNOhncXOVKqdycXKkKgQiBQSBXOYz/AGFUBGIIGIQCIdVVQo2rqHJjNQYXKYTb6AzhwIUVUGN9U9zN3uze7OzpvDuze/s+cOzsj7vv7O7M+76d3VuAEEIIIYQQQgghpHRU6AoQkoeISNY6pRTbMKktbLwkl5ACqGM/Xl0FABxfXCTKPupACCFBkAmbUSSbUTRTdolzUfE3o0jerq+L7O7OlMuOT0iZ3A5dATIfNqHx5fwer67iWaeD7sOHM2XtQn1w9uYNnnU6cZmQutMKXQGST/oyXE99u79QAnh8cYEXo1Fi2YvRyKv4E0IainkZbk59CLA5BPF2fT3++BqCsNXBZ2xCyoQOeE7EQoh6aAfqCz3MYbpQ0336GAZJ14E33whpGCIyvvljTH3GDu0AzTqESkChkx8hRUMHMSciImf378fz3ffvvTowcxw4lAM0ha+J7jMt/E08BoQEQTsv7X7pAJuFcdxFz4aqA9tAw7CNfzaxATR53wkAQLYPYxH2G7gCCYAUT+5NOH2ilVKJj7mubKqSAJSB79gkPNuH0xEY37F1s9N1qHIbrEp/rQPOP2KY4ms0PuhlImJuU0qDMOuQWg4RkSo3RLJcHO2EbWshE0AeaYG19VcyS+aJdImv5mgndsKYbFtow3DV4WhHJU4qhZiQcEzMUDy/dTCMy4N+BBFZmj66ckvJxy9SyL44HXDrTgT1zQp+fxoBAH76dYjXT1p49OoawPQgl+GG8xLA9qGYQyF0w4SEQ5RSCdFdVlZuKXn06hqvn7QKEeGvcsCXnz7G5UE/0t/LDnYDgRQRad2JEsvSCUDH1xl2kTiELMLWwVAG/YjtDBCX8Jq6wH45S6YDVkop18C5Kb5A0g3bWMSlXv83nEkAWnzTsdNu2PZ7bABkEa5ONqTdO1W6DADtXljxrUgCcIqvAbteBnO9De23b7/H7r9/xWUA+PHTn9Zt0yfkwz+fAUyHKealCgmAEABo907VVHhPvbefqiaAvG20+/UZ34hZi37urKQeWL862ZhZ1+6d4sHeOe59d9f6XS28716u6d8aB7zhMISOP5MAzu0JwFUPXgaRuhIyAdjIE+BBP8LWwRAluvRM9z0R4UocpzwWfh/w1ckG2r01YO/cuj4tvMDXXYdo4dUM+tFCCYCQOlIV4dX8sfZDpgnyIL4Axn08q//XhVwHDIwv69Mu+PLvSwBA5/lZ1nenQRYUXpcDB6Yu3EbRCYAQkmS035Xd28cAksMNPsQXgLgMWF1ccG4FbSKoxddG5/lZYc8Fh04AhBA3o/1u3NEMMfbV3zJF2Hwyqsr9fy4BBsYiONrvOrctUnzN+KESACGk8oi+Ck4LcR3+AJI7BqyfRpjsSKYIlyl8OjYAZxKg+BLSONS7l2vjTp8hxEuB8UINOf7l5/itTOZ8WS/cSL3QwxmfL/0gpLEIxo5YHuydL89b42ziZ5svW4BDxSeE1AOtC/qzFJqQFrusaZkCHDI+IaQeiIXQdXIx93PAehzWNS2T0PEJIdWnbvd+bvSvtNwfK3HnQ8cnhJCi+R9t4o1zEu5PTgAAAABJRU5ErkJggg=="; const DECORATIONS_SPRITE_SHEET_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAPNJREFUaIHtmTESgzAMBHWZDC+gp0vP/x9Bn44+L6BRmrhJA4csM05uGzfY1s1JxggzIYQQQgghxEnATnB3zwikAICKiXq4BE/uwaxvn/UPb3BnNwFg27Ky0w6vzRp8S4mkIbQD3wzzFJofdTMkYJgn89czFADGKSSiSgphfFBjTaoIKC4cHWvSxIFMmjiQSYoDLUlxoCVywOwHHWjpROop1IL/vsxty2oYO77M1QggSvcpJAFXE66BPfa+2C4v4j2yi7z7FJKAq6FrwN3TO3MMlAAAKO3F2sVZTiu2N9p9CnUv4FR7PbMG2BQ69SJL/kVA8QauAnHUj36BVwAAAABJRU5ErkJggg=="; const FEATHER_SPRITE_SHEET_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; @@ -523,9 +572,11 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO 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)), + tuftBase: new Layer(getLayer(SPRITE_SHEET, 6), "tuft"), + tuftDown: new Layer(getLayer(SPRITE_SHEET, 7), "tuft"), + wingsUp: new Layer(getLayer(SPRITE_SHEET, 8)), + wingsDown: new Layer(getLayer(SPRITE_SHEET, 9)), + happyEye: new Layer(getLayer(SPRITE_SHEET, 10)), }; const decorationLayers = { @@ -537,14 +588,14 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO }; 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]), + base: new Frame([layers.base, layers.tuftBase]), + headDown: new Frame([layers.down, layers.tuftDown]), + wingsDown: new Frame([layers.base, layers.tuftBase, layers.wingsDown]), + wingsUp: new Frame([layers.down, layers.tuftDown, layers.wingsUp]), + heartOne: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartOne]), + heartTwo: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartTwo]), + heartThree: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartThree]), + heartFour: new Frame([layers.base, layers.tuftBase, layers.happyEye, layers.heartFour]), }; const decorationFrames = { @@ -644,8 +695,8 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO let focusedElement = null; let timeOfLastAction = Date.now(); let petStack = []; - let currentTheme = "bluebird"; - let unlockedThemes = ["bluebird"]; + let currentTheme = "tuftedTitmouse"; + let unlockedThemes = ["tuftedTitmouse"]; function init() { if (window !== window.top) { @@ -657,7 +708,7 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO document.head.appendChild(styleElement); canvas.id = "birb"; - canvas.width = birbFrames.base.pixels[0].length * CANVAS_PIXEL_SIZE; + canvas.width = birbFrames.base.getPixels()[0].length * CANVAS_PIXEL_SIZE; canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; document.body.appendChild(canvas); @@ -830,6 +881,10 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO featherCanvas.addEventListener("click", () => { unlockBird(birdType); removeFeather(); + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + removeFieldGuide(); + insertFieldGuide(); + } }); }