diff --git a/aseprite/hats.aseprite b/aseprite/hats.aseprite new file mode 100644 index 0000000..0ddd6b0 Binary files /dev/null and b/aseprite/hats.aseprite differ diff --git a/build.js b/build.js index 49ed684..f5faec5 100644 --- a/build.js +++ b/build.js @@ -46,6 +46,10 @@ const spriteSheets = [ { key: "__FEATHER_SPRITE_SHEET__", path: SPRITES_DIR + "/feather.png" + }, + { + key: "__HATS_SPRITE_SHEET__", + path: SPRITES_DIR + "/hats.png" } ]; diff --git a/dist/extension.zip b/dist/extension.zip index e463bc2..9a98b92 100644 Binary files a/dist/extension.zip and b/dist/extension.zip differ diff --git a/dist/extension/birb.js b/dist/extension/birb.js index 1b60e97..423a1ee 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -632,8 +632,9 @@ * @param {string[][]} spriteSheet The loaded sprite sheet pixel data * @param {number} spriteWidth * @param {number} spriteHeight + * @param {string[][]} hatSpriteSheet The loaded hat sprite sheet pixel data */ - constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight, hatSpriteSheet) { this.birbCssScale = birbCssScale; this.canvasPixelSize = canvasPixelSize; this.windowPixelSize = canvasPixelSize * birbCssScale; @@ -654,12 +655,16 @@ happyEye: new Layer(getLayerPixels(spriteSheet, 9, this.spriteWidth)), }; + // Build hat layers + const hatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false); + const downHatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false, 1); + // Build frames from layers this.frames = { - base: new Frame([this.layers.base, this.layers.tuftBase]), - headDown: new Frame([this.layers.down, this.layers.tuftDown]), - wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), - wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + base: new Frame([this.layers.base, this.layers.tuftBase, hatLayer]), + headDown: new Frame([this.layers.down, this.layers.tuftDown, downHatLayer]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown, hatLayer]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp, downHatLayer]), heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), @@ -730,6 +735,74 @@ return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); } + buildHatLayer(spriteSheet, hatName, outlineBottom = false, yOffset = 0) { + const LEFT_PADDING = 6; + const RIGHT_PADDING = 14; + const TOP_PADDING = 4 + yOffset; + const BOTTOM_PADDING = Math.max(0, 16 - yOffset); + + const hatPixels = getLayerPixels(spriteSheet, 0, 12); + const paddedHatPixels = []; + + // Top padding + for (let y = 0; y < TOP_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + // Left and right padding + for (let y = 0; y < hatPixels.length; y++) { + const row = []; + for (let x = 0; x < LEFT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + for (let x = 0; x < hatPixels[y].length; x++) { + row.push(hatPixels[y][x]); + } + + for (let x = 0; x < RIGHT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + paddedHatPixels.push(row); + } + // Bottom padding + for (let y = 0; y < BOTTOM_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + + // Add outline + let neighborOffsets = [ + [-1, 0], + [1, 0], + [0, -1], + [-1, -1], + [1, -1], + ]; + if (outlineBottom) { + neighborOffsets.push([0, 1], [-1, 1], [1, 1]); + } + for (let y = 0; y < paddedHatPixels.length; y++) { + for (let x = 0; x < paddedHatPixels[y].length; x++) { + const pixel = paddedHatPixels[y][x]; + if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { + for (let [dx, dy] of neighborOffsets) { + const newX = x + dx; + const newY = y + dy; + if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { + paddedHatPixels[newY][newX] = PALETTE.BORDER; + } + } + } + } + } + return new Layer(paddedHatPixels); + } + + /** * @returns {AnimationType} The current animation key */ @@ -1763,6 +1836,7 @@ }`; const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg=="; const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; + const HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAC5JREFUKJFjYBgFgwEwYhH7j08NE6k2sKALnJCVReFbPH6M0zp0p6ADRgYGBgYAqu4FCZWdtIcAAAAASUVORK5CYII="; // Element IDs const FIELD_GUIDE_ID = "birb-field-guide"; @@ -1808,17 +1882,20 @@ log("Loading sprite sheets..."); const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); - startApplication(birbPixels, featherPixels); + const hatsPixels = await loadSpriteSheetPixels(HATS_SPRITE_SHEET); + startApplication(birbPixels, featherPixels, hatsPixels); } /** * @param {string[][]} birbPixels * @param {string[][]} featherPixels + * @param {string[][]} hatsPixels */ - function startApplication(birbPixels, featherPixels) { + function startApplication(birbPixels, featherPixels, hatsPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; + const HATS_SPRITE_SHEET = hatsPixels; const featherLayers = { feather: new Layer(getLayerPixels(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), @@ -2006,7 +2083,7 @@ styleElement.textContent = STYLESHEET; document.head.appendChild(styleElement); - birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT, HATS_SPRITE_SHEET); birb.setAnimation(Animations.BOB); window.addEventListener("scroll", () => { @@ -2688,8 +2765,9 @@ continue; } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(PALETTE.TRANSPARENT); + // Return the color as-is if not found in the map + row.push(hex); + continue; } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index e847f07..f02c753 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -637,8 +637,9 @@ module.exports = class PocketBird extends Plugin { * @param {string[][]} spriteSheet The loaded sprite sheet pixel data * @param {number} spriteWidth * @param {number} spriteHeight + * @param {string[][]} hatSpriteSheet The loaded hat sprite sheet pixel data */ - constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight, hatSpriteSheet) { this.birbCssScale = birbCssScale; this.canvasPixelSize = canvasPixelSize; this.windowPixelSize = canvasPixelSize * birbCssScale; @@ -659,12 +660,16 @@ module.exports = class PocketBird extends Plugin { happyEye: new Layer(getLayerPixels(spriteSheet, 9, this.spriteWidth)), }; + // Build hat layers + const hatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false); + const downHatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false, 1); + // Build frames from layers this.frames = { - base: new Frame([this.layers.base, this.layers.tuftBase]), - headDown: new Frame([this.layers.down, this.layers.tuftDown]), - wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), - wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + base: new Frame([this.layers.base, this.layers.tuftBase, hatLayer]), + headDown: new Frame([this.layers.down, this.layers.tuftDown, downHatLayer]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown, hatLayer]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp, downHatLayer]), heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), @@ -735,6 +740,74 @@ module.exports = class PocketBird extends Plugin { return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); } + buildHatLayer(spriteSheet, hatName, outlineBottom = false, yOffset = 0) { + const LEFT_PADDING = 6; + const RIGHT_PADDING = 14; + const TOP_PADDING = 4 + yOffset; + const BOTTOM_PADDING = Math.max(0, 16 - yOffset); + + const hatPixels = getLayerPixels(spriteSheet, 0, 12); + const paddedHatPixels = []; + + // Top padding + for (let y = 0; y < TOP_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + // Left and right padding + for (let y = 0; y < hatPixels.length; y++) { + const row = []; + for (let x = 0; x < LEFT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + for (let x = 0; x < hatPixels[y].length; x++) { + row.push(hatPixels[y][x]); + } + + for (let x = 0; x < RIGHT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + paddedHatPixels.push(row); + } + // Bottom padding + for (let y = 0; y < BOTTOM_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + + // Add outline + let neighborOffsets = [ + [-1, 0], + [1, 0], + [0, -1], + [-1, -1], + [1, -1], + ]; + if (outlineBottom) { + neighborOffsets.push([0, 1], [-1, 1], [1, 1]); + } + for (let y = 0; y < paddedHatPixels.length; y++) { + for (let x = 0; x < paddedHatPixels[y].length; x++) { + const pixel = paddedHatPixels[y][x]; + if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { + for (let [dx, dy] of neighborOffsets) { + const newX = x + dx; + const newY = y + dy; + if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { + paddedHatPixels[newY][newX] = PALETTE.BORDER; + } + } + } + } + } + return new Layer(paddedHatPixels); + } + + /** * @returns {AnimationType} The current animation key */ @@ -1806,6 +1879,7 @@ module.exports = class PocketBird extends Plugin { }`; const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg=="; const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; + const HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAC5JREFUKJFjYBgFgwEwYhH7j08NE6k2sKALnJCVReFbPH6M0zp0p6ADRgYGBgYAqu4FCZWdtIcAAAAASUVORK5CYII="; // Element IDs const FIELD_GUIDE_ID = "birb-field-guide"; @@ -1851,17 +1925,20 @@ module.exports = class PocketBird extends Plugin { log("Loading sprite sheets..."); const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); - startApplication(birbPixels, featherPixels); + const hatsPixels = await loadSpriteSheetPixels(HATS_SPRITE_SHEET); + startApplication(birbPixels, featherPixels, hatsPixels); } /** * @param {string[][]} birbPixels * @param {string[][]} featherPixels + * @param {string[][]} hatsPixels */ - function startApplication(birbPixels, featherPixels) { + function startApplication(birbPixels, featherPixels, hatsPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; + const HATS_SPRITE_SHEET = hatsPixels; const featherLayers = { feather: new Layer(getLayerPixels(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), @@ -2049,7 +2126,7 @@ module.exports = class PocketBird extends Plugin { styleElement.textContent = STYLESHEET; document.head.appendChild(styleElement); - birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT, HATS_SPRITE_SHEET); birb.setAnimation(Animations.BOB); window.addEventListener("scroll", () => { @@ -2731,8 +2808,9 @@ module.exports = class PocketBird extends Plugin { continue; } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(PALETTE.TRANSPARENT); + // Return the color as-is if not found in the map + row.push(hex); + continue; } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index c794adb..f2c01f1 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -646,8 +646,9 @@ * @param {string[][]} spriteSheet The loaded sprite sheet pixel data * @param {number} spriteWidth * @param {number} spriteHeight + * @param {string[][]} hatSpriteSheet The loaded hat sprite sheet pixel data */ - constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight, hatSpriteSheet) { this.birbCssScale = birbCssScale; this.canvasPixelSize = canvasPixelSize; this.windowPixelSize = canvasPixelSize * birbCssScale; @@ -668,12 +669,16 @@ happyEye: new Layer(getLayerPixels(spriteSheet, 9, this.spriteWidth)), }; + // Build hat layers + const hatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false); + const downHatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false, 1); + // Build frames from layers this.frames = { - base: new Frame([this.layers.base, this.layers.tuftBase]), - headDown: new Frame([this.layers.down, this.layers.tuftDown]), - wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), - wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + base: new Frame([this.layers.base, this.layers.tuftBase, hatLayer]), + headDown: new Frame([this.layers.down, this.layers.tuftDown, downHatLayer]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown, hatLayer]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp, downHatLayer]), heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), @@ -744,6 +749,74 @@ return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); } + buildHatLayer(spriteSheet, hatName, outlineBottom = false, yOffset = 0) { + const LEFT_PADDING = 6; + const RIGHT_PADDING = 14; + const TOP_PADDING = 4 + yOffset; + const BOTTOM_PADDING = Math.max(0, 16 - yOffset); + + const hatPixels = getLayerPixels(spriteSheet, 0, 12); + const paddedHatPixels = []; + + // Top padding + for (let y = 0; y < TOP_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + // Left and right padding + for (let y = 0; y < hatPixels.length; y++) { + const row = []; + for (let x = 0; x < LEFT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + for (let x = 0; x < hatPixels[y].length; x++) { + row.push(hatPixels[y][x]); + } + + for (let x = 0; x < RIGHT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + paddedHatPixels.push(row); + } + // Bottom padding + for (let y = 0; y < BOTTOM_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + + // Add outline + let neighborOffsets = [ + [-1, 0], + [1, 0], + [0, -1], + [-1, -1], + [1, -1], + ]; + if (outlineBottom) { + neighborOffsets.push([0, 1], [-1, 1], [1, 1]); + } + for (let y = 0; y < paddedHatPixels.length; y++) { + for (let x = 0; x < paddedHatPixels[y].length; x++) { + const pixel = paddedHatPixels[y][x]; + if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { + for (let [dx, dy] of neighborOffsets) { + const newX = x + dx; + const newY = y + dy; + if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { + paddedHatPixels[newY][newX] = PALETTE.BORDER; + } + } + } + } + } + return new Layer(paddedHatPixels); + } + + /** * @returns {AnimationType} The current animation key */ @@ -1768,6 +1841,7 @@ }`; const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg=="; const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; + const HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAC5JREFUKJFjYBgFgwEwYhH7j08NE6k2sKALnJCVReFbPH6M0zp0p6ADRgYGBgYAqu4FCZWdtIcAAAAASUVORK5CYII="; // Element IDs const FIELD_GUIDE_ID = "birb-field-guide"; @@ -1813,17 +1887,20 @@ log("Loading sprite sheets..."); const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); - startApplication(birbPixels, featherPixels); + const hatsPixels = await loadSpriteSheetPixels(HATS_SPRITE_SHEET); + startApplication(birbPixels, featherPixels, hatsPixels); } /** * @param {string[][]} birbPixels * @param {string[][]} featherPixels + * @param {string[][]} hatsPixels */ - function startApplication(birbPixels, featherPixels) { + function startApplication(birbPixels, featherPixels, hatsPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; + const HATS_SPRITE_SHEET = hatsPixels; const featherLayers = { feather: new Layer(getLayerPixels(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), @@ -2011,7 +2088,7 @@ styleElement.textContent = STYLESHEET; document.head.appendChild(styleElement); - birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT, HATS_SPRITE_SHEET); birb.setAnimation(Animations.BOB); window.addEventListener("scroll", () => { @@ -2693,8 +2770,9 @@ continue; } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(PALETTE.TRANSPARENT); + // Return the color as-is if not found in the map + row.push(hex); + continue; } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } diff --git a/dist/web/birb.embed.js b/dist/web/birb.embed.js index 0d3bcb5..2f4af0c 100644 --- a/dist/web/birb.embed.js +++ b/dist/web/birb.embed.js @@ -632,8 +632,9 @@ * @param {string[][]} spriteSheet The loaded sprite sheet pixel data * @param {number} spriteWidth * @param {number} spriteHeight + * @param {string[][]} hatSpriteSheet The loaded hat sprite sheet pixel data */ - constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight, hatSpriteSheet) { this.birbCssScale = birbCssScale; this.canvasPixelSize = canvasPixelSize; this.windowPixelSize = canvasPixelSize * birbCssScale; @@ -654,12 +655,16 @@ happyEye: new Layer(getLayerPixels(spriteSheet, 9, this.spriteWidth)), }; + // Build hat layers + const hatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false); + const downHatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false, 1); + // Build frames from layers this.frames = { - base: new Frame([this.layers.base, this.layers.tuftBase]), - headDown: new Frame([this.layers.down, this.layers.tuftDown]), - wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), - wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + base: new Frame([this.layers.base, this.layers.tuftBase, hatLayer]), + headDown: new Frame([this.layers.down, this.layers.tuftDown, downHatLayer]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown, hatLayer]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp, downHatLayer]), heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), @@ -730,6 +735,74 @@ return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); } + buildHatLayer(spriteSheet, hatName, outlineBottom = false, yOffset = 0) { + const LEFT_PADDING = 6; + const RIGHT_PADDING = 14; + const TOP_PADDING = 4 + yOffset; + const BOTTOM_PADDING = Math.max(0, 16 - yOffset); + + const hatPixels = getLayerPixels(spriteSheet, 0, 12); + const paddedHatPixels = []; + + // Top padding + for (let y = 0; y < TOP_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + // Left and right padding + for (let y = 0; y < hatPixels.length; y++) { + const row = []; + for (let x = 0; x < LEFT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + for (let x = 0; x < hatPixels[y].length; x++) { + row.push(hatPixels[y][x]); + } + + for (let x = 0; x < RIGHT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + paddedHatPixels.push(row); + } + // Bottom padding + for (let y = 0; y < BOTTOM_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + + // Add outline + let neighborOffsets = [ + [-1, 0], + [1, 0], + [0, -1], + [-1, -1], + [1, -1], + ]; + if (outlineBottom) { + neighborOffsets.push([0, 1], [-1, 1], [1, 1]); + } + for (let y = 0; y < paddedHatPixels.length; y++) { + for (let x = 0; x < paddedHatPixels[y].length; x++) { + const pixel = paddedHatPixels[y][x]; + if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { + for (let [dx, dy] of neighborOffsets) { + const newX = x + dx; + const newY = y + dy; + if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { + paddedHatPixels[newY][newX] = PALETTE.BORDER; + } + } + } + } + } + return new Layer(paddedHatPixels); + } + + /** * @returns {AnimationType} The current animation key */ @@ -1748,6 +1821,7 @@ }`; const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg=="; const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; + const HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAC5JREFUKJFjYBgFgwEwYhH7j08NE6k2sKALnJCVReFbPH6M0zp0p6ADRgYGBgYAqu4FCZWdtIcAAAAASUVORK5CYII="; // Element IDs const FIELD_GUIDE_ID = "birb-field-guide"; @@ -1793,17 +1867,20 @@ log("Loading sprite sheets..."); const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); - startApplication(birbPixels, featherPixels); + const hatsPixels = await loadSpriteSheetPixels(HATS_SPRITE_SHEET); + startApplication(birbPixels, featherPixels, hatsPixels); } /** * @param {string[][]} birbPixels * @param {string[][]} featherPixels + * @param {string[][]} hatsPixels */ - function startApplication(birbPixels, featherPixels) { + function startApplication(birbPixels, featherPixels, hatsPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; + const HATS_SPRITE_SHEET = hatsPixels; const featherLayers = { feather: new Layer(getLayerPixels(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), @@ -1991,7 +2068,7 @@ styleElement.textContent = STYLESHEET; document.head.appendChild(styleElement); - birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT, HATS_SPRITE_SHEET); birb.setAnimation(Animations.BOB); window.addEventListener("scroll", () => { @@ -2673,8 +2750,9 @@ continue; } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(PALETTE.TRANSPARENT); + // Return the color as-is if not found in the map + row.push(hex); + continue; } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } diff --git a/dist/web/birb.js b/dist/web/birb.js index 0d3bcb5..2f4af0c 100644 --- a/dist/web/birb.js +++ b/dist/web/birb.js @@ -632,8 +632,9 @@ * @param {string[][]} spriteSheet The loaded sprite sheet pixel data * @param {number} spriteWidth * @param {number} spriteHeight + * @param {string[][]} hatSpriteSheet The loaded hat sprite sheet pixel data */ - constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight, hatSpriteSheet) { this.birbCssScale = birbCssScale; this.canvasPixelSize = canvasPixelSize; this.windowPixelSize = canvasPixelSize * birbCssScale; @@ -654,12 +655,16 @@ happyEye: new Layer(getLayerPixels(spriteSheet, 9, this.spriteWidth)), }; + // Build hat layers + const hatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false); + const downHatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false, 1); + // Build frames from layers this.frames = { - base: new Frame([this.layers.base, this.layers.tuftBase]), - headDown: new Frame([this.layers.down, this.layers.tuftDown]), - wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), - wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + base: new Frame([this.layers.base, this.layers.tuftBase, hatLayer]), + headDown: new Frame([this.layers.down, this.layers.tuftDown, downHatLayer]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown, hatLayer]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp, downHatLayer]), heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), @@ -730,6 +735,74 @@ return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); } + buildHatLayer(spriteSheet, hatName, outlineBottom = false, yOffset = 0) { + const LEFT_PADDING = 6; + const RIGHT_PADDING = 14; + const TOP_PADDING = 4 + yOffset; + const BOTTOM_PADDING = Math.max(0, 16 - yOffset); + + const hatPixels = getLayerPixels(spriteSheet, 0, 12); + const paddedHatPixels = []; + + // Top padding + for (let y = 0; y < TOP_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + // Left and right padding + for (let y = 0; y < hatPixels.length; y++) { + const row = []; + for (let x = 0; x < LEFT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + for (let x = 0; x < hatPixels[y].length; x++) { + row.push(hatPixels[y][x]); + } + + for (let x = 0; x < RIGHT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + paddedHatPixels.push(row); + } + // Bottom padding + for (let y = 0; y < BOTTOM_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + + // Add outline + let neighborOffsets = [ + [-1, 0], + [1, 0], + [0, -1], + [-1, -1], + [1, -1], + ]; + if (outlineBottom) { + neighborOffsets.push([0, 1], [-1, 1], [1, 1]); + } + for (let y = 0; y < paddedHatPixels.length; y++) { + for (let x = 0; x < paddedHatPixels[y].length; x++) { + const pixel = paddedHatPixels[y][x]; + if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { + for (let [dx, dy] of neighborOffsets) { + const newX = x + dx; + const newY = y + dy; + if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { + paddedHatPixels[newY][newX] = PALETTE.BORDER; + } + } + } + } + } + return new Layer(paddedHatPixels); + } + + /** * @returns {AnimationType} The current animation key */ @@ -1748,6 +1821,7 @@ }`; const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg=="; const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; + const HATS_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAAC5JREFUKJFjYBgFgwEwYhH7j08NE6k2sKALnJCVReFbPH6M0zp0p6ADRgYGBgYAqu4FCZWdtIcAAAAASUVORK5CYII="; // Element IDs const FIELD_GUIDE_ID = "birb-field-guide"; @@ -1793,17 +1867,20 @@ log("Loading sprite sheets..."); const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); - startApplication(birbPixels, featherPixels); + const hatsPixels = await loadSpriteSheetPixels(HATS_SPRITE_SHEET); + startApplication(birbPixels, featherPixels, hatsPixels); } /** * @param {string[][]} birbPixels * @param {string[][]} featherPixels + * @param {string[][]} hatsPixels */ - function startApplication(birbPixels, featherPixels) { + function startApplication(birbPixels, featherPixels, hatsPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; + const HATS_SPRITE_SHEET = hatsPixels; const featherLayers = { feather: new Layer(getLayerPixels(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), @@ -1991,7 +2068,7 @@ styleElement.textContent = STYLESHEET; document.head.appendChild(styleElement); - birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT, HATS_SPRITE_SHEET); birb.setAnimation(Animations.BOB); window.addEventListener("scroll", () => { @@ -2673,8 +2750,9 @@ continue; } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(PALETTE.TRANSPARENT); + // Return the color as-is if not found in the map + row.push(hex); + continue; } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } diff --git a/sprites/hats.png b/sprites/hats.png new file mode 100644 index 0000000..1840ac9 Binary files /dev/null and b/sprites/hats.png differ diff --git a/src/application.js b/src/application.js index 42806a0..713e79b 100644 --- a/src/application.js +++ b/src/application.js @@ -78,6 +78,7 @@ const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; const STYLESHEET = `___STYLESHEET___`; const SPRITE_SHEET = "__SPRITE_SHEET__"; const FEATHER_SPRITE_SHEET = "__FEATHER_SPRITE_SHEET__"; +const HATS_SPRITE_SHEET = "__HATS_SPRITE_SHEET__"; // Element IDs const FIELD_GUIDE_ID = "birb-field-guide"; @@ -123,17 +124,20 @@ export async function initializeApplication(context) { log("Loading sprite sheets..."); const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); - startApplication(birbPixels, featherPixels); + const hatsPixels = await loadSpriteSheetPixels(HATS_SPRITE_SHEET); + startApplication(birbPixels, featherPixels, hatsPixels); } /** * @param {string[][]} birbPixels * @param {string[][]} featherPixels + * @param {string[][]} hatsPixels */ -function startApplication(birbPixels, featherPixels) { +function startApplication(birbPixels, featherPixels, hatsPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; + const HATS_SPRITE_SHEET = hatsPixels; const featherLayers = { feather: new Layer(getLayerPixels(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), @@ -321,7 +325,7 @@ function startApplication(birbPixels, featherPixels) { styleElement.textContent = STYLESHEET; document.head.appendChild(styleElement); - birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT, HATS_SPRITE_SHEET); birb.setAnimation(Animations.BOB); window.addEventListener("scroll", () => { @@ -1021,8 +1025,9 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) { continue; } if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(PALETTE.TRANSPARENT); + // Return the color as-is if not found in the map + row.push(hex); + continue; } row.push(SPRITE_SHEET_COLOR_MAP[hex]); } diff --git a/src/birb.js b/src/birb.js index bf12dff..55f56de 100644 --- a/src/birb.js +++ b/src/birb.js @@ -2,7 +2,7 @@ import { Directions, getLayerPixels, getWindowHeight, getFixedWindowHeight } fro import Layer from './animation/layer.js'; import Frame from './animation/frame.js'; import Anim from './animation/anim.js'; -import { BirdType } from './animation/sprites.js'; +import { BirdType, PALETTE } from './animation/sprites.js'; /** * @typedef {keyof typeof Animations} AnimationType @@ -31,8 +31,9 @@ export class Birb { * @param {string[][]} spriteSheet The loaded sprite sheet pixel data * @param {number} spriteWidth * @param {number} spriteHeight + * @param {string[][]} hatSpriteSheet The loaded hat sprite sheet pixel data */ - constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight, hatSpriteSheet) { this.birbCssScale = birbCssScale; this.canvasPixelSize = canvasPixelSize; this.windowPixelSize = canvasPixelSize * birbCssScale; @@ -53,12 +54,16 @@ export class Birb { happyEye: new Layer(getLayerPixels(spriteSheet, 9, this.spriteWidth)), }; + // Build hat layers + const hatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false); + const downHatLayer = this.buildHatLayer(hatSpriteSheet, "top-hat", false, 1); + // Build frames from layers this.frames = { - base: new Frame([this.layers.base, this.layers.tuftBase]), - headDown: new Frame([this.layers.down, this.layers.tuftDown]), - wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), - wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + base: new Frame([this.layers.base, this.layers.tuftBase, hatLayer]), + headDown: new Frame([this.layers.down, this.layers.tuftDown, downHatLayer]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown, hatLayer]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp, downHatLayer]), heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), @@ -129,6 +134,74 @@ export class Birb { return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); } + buildHatLayer(spriteSheet, hatName, outlineBottom = false, yOffset = 0) { + const LEFT_PADDING = 6; + const RIGHT_PADDING = 14; + const TOP_PADDING = 4 + yOffset; + const BOTTOM_PADDING = Math.max(0, 16 - yOffset); + + const hatPixels = getLayerPixels(spriteSheet, 0, 12); + const paddedHatPixels = []; + + // Top padding + for (let y = 0; y < TOP_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + // Left and right padding + for (let y = 0; y < hatPixels.length; y++) { + const row = []; + for (let x = 0; x < LEFT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + for (let x = 0; x < hatPixels[y].length; x++) { + row.push(hatPixels[y][x]); + } + + for (let x = 0; x < RIGHT_PADDING; x++) { + row.push(PALETTE.TRANSPARENT); + } + + paddedHatPixels.push(row); + } + // Bottom padding + for (let y = 0; y < BOTTOM_PADDING; y++) { + paddedHatPixels.push(Array(hatPixels[0].length + LEFT_PADDING + RIGHT_PADDING) + .fill(PALETTE.TRANSPARENT) + ); + } + + // Add outline + let neighborOffsets = [ + [-1, 0], + [1, 0], + [0, -1], + [-1, -1], + [1, -1], + ]; + if (outlineBottom) { + neighborOffsets.push([0, 1], [-1, 1], [1, 1]); + } + for (let y = 0; y < paddedHatPixels.length; y++) { + for (let x = 0; x < paddedHatPixels[y].length; x++) { + const pixel = paddedHatPixels[y][x]; + if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) { + for (let [dx, dy] of neighborOffsets) { + const newX = x + dx; + const newY = y + dy; + if (newY >= 0 && newY < paddedHatPixels.length && newX >= 0 && newX < paddedHatPixels[newY].length && paddedHatPixels[newY][newX] === PALETTE.TRANSPARENT) { + paddedHatPixels[newY][newX] = PALETTE.BORDER; + } + } + } + } + } + return new Layer(paddedHatPixels); + } + + /** * @returns {AnimationType} The current animation key */