Add decoration rendering

This commit is contained in:
Idrees Hassan
2024-12-26 17:30:06 -05:00
parent 2120e802ec
commit 480814ec68

152
birb.js
View File

@@ -36,7 +36,7 @@ const FLY_SPEED = settings.flySpeed;
const HOP_DISTANCE = settings.hopDistance; const HOP_DISTANCE = settings.hopDistance;
// Time in milliseconds until the user is considered AFK // Time in milliseconds until the user is considered AFK
const AFK_TIME = 1000 * 30; const AFK_TIME = 1000 * 30;
const MAX_HEIGHT = 32; const SPRITE_HEIGHT = 32;
const START_MENU_ID = "birb-start-menu"; const START_MENU_ID = "birb-start-menu";
const styles = ` const styles = `
@@ -50,6 +50,15 @@ const styles = `
cursor: pointer; cursor: pointer;
} }
.birb-decoration {
image-rendering: pixelated;
position: fixed;
bottom: 0;
transform: scale(${CSS_SCALE});
transform-origin: bottom;
z-index: 999999990;
}
.birb-window { .birb-window {
font-family: "Monocraft"; font-family: "Monocraft";
z-index: 1000; z-index: 1000;
@@ -73,6 +82,13 @@ const styles = `
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
animation: pop-in 0.08s;
transition-timing-function: ease-in;
}
@keyframes pop-in {
0% { opacity: 1; transform: scale(0.1); }
100% { opacity: 1; transform: scale(1); }
} }
.birb-window-header { .birb-window-header {
@@ -192,13 +208,13 @@ class Frame {
} }
} }
// Surround non-transparent pixels with border // Surround non-transparent pixels with border
for (let y = 0; y < this.pixels.length; y++) { // for (let y = 0; y < this.pixels.length; y++) {
for (let x = 0; x < this.pixels[y].length; x++) { // for (let x = 0; x < this.pixels[y].length; x++) {
if (this.pixels[y][x] === TRANSPARENT && this.hasAdjacent(x, y)) { // if (this.pixels[y][x] === TRANSPARENT && this.hasAdjacent(x, y)) {
this.pixels[y][x] = BORDER; // this.pixels[y][x] = BORDER;
} // }
} // }
} // }
} }
hasAdjacent(x, y) { hasAdjacent(x, y) {
@@ -223,10 +239,9 @@ class Frame {
for (let y = 0; y < this.pixels.length; y++) { for (let y = 0; y < this.pixels.length; y++) {
const row = this.pixels[y]; const row = this.pixels[y];
for (let x = 0; x < this.pixels[y].length; x++) { for (let x = 0; x < this.pixels[y].length; x++) {
let topMargin = MAX_HEIGHT - this.pixels.length;
const cell = direction === Directions.LEFT ? row[x] : row[this.pixels[y].length - x - 1]; const cell = direction === Directions.LEFT ? row[x] : row[this.pixels[y].length - x - 1];
ctx.fillStyle = bluebirdColors[cell]; ctx.fillStyle = bluebirdColors[cell] ?? cell;
ctx.fillRect(x * CANVAS_PIXEL_SIZE, (y + topMargin) * CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE); ctx.fillRect(x * CANVAS_PIXEL_SIZE, y * CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE, CANVAS_PIXEL_SIZE);
}; };
}; };
} }
@@ -291,7 +306,7 @@ const HEART_SHINE = "heart-shine";
const SPRITESHEET_COLOR_MAP = { const SPRITESHEET_COLOR_MAP = {
"transparent": TRANSPARENT, "transparent": TRANSPARENT,
"#ffffff": TRANSPARENT, "#ffffff": BORDER,
"#000000": OUTLINE, "#000000": OUTLINE,
"#010a19": BEAK, "#010a19": BEAK,
"#190301": EYE, "#190301": EYE,
@@ -326,6 +341,9 @@ const bluebirdColors = {
const SPRITE_WIDTH = 32; 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_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 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 = { const layers = {
base: new Layer(getLayer(SPRITE_SHEET, 0)), base: new Layer(getLayer(SPRITE_SHEET, 0)),
@@ -339,7 +357,11 @@ const layers = {
happyEye: new Layer(getLayer(SPRITE_SHEET, 8)), happyEye: new Layer(getLayer(SPRITE_SHEET, 8)),
}; };
const sharedFrames = { const decorationLayers = {
mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)),
};
const birbFrames = {
base: new Frame([layers.base]), base: new Frame([layers.base]),
headDown: new Frame([layers.down]), headDown: new Frame([layers.down]),
wingsDown: new Frame([layers.base, layers.wingsDown]), wingsDown: new Frame([layers.base, layers.wingsDown]),
@@ -350,21 +372,24 @@ const sharedFrames = {
heartFour: new Frame([layers.base, layers.happyEye, layers.heartFour]), heartFour: new Frame([layers.base, layers.happyEye, layers.heartFour]),
}; };
const decorationFrames = {
mac: new Frame([decorationLayers.mac]),
};
const Animations = { const Animations = {
STILL: new Anim([sharedFrames.base], [1000]), STILL: new Anim([birbFrames.base], [1000]),
BOB: new Anim([ BOB: new Anim([
sharedFrames.base, birbFrames.base,
sharedFrames.headDown birbFrames.headDown
], [ ], [
420, 420,
420 420
]), ]),
FLYING: new Anim([ FLYING: new Anim([
sharedFrames.base, birbFrames.base,
sharedFrames.wingsUp, birbFrames.wingsUp,
sharedFrames.headDown, birbFrames.headDown,
sharedFrames.wingsDown, birbFrames.wingsDown,
], [ ], [
40, 40,
80, 80,
@@ -372,14 +397,14 @@ const Animations = {
80, 80,
]), ]),
HEART: new Anim([ HEART: new Anim([
sharedFrames.heartOne, birbFrames.heartOne,
sharedFrames.heartTwo, birbFrames.heartTwo,
sharedFrames.heartThree, birbFrames.heartThree,
sharedFrames.heartFour, birbFrames.heartFour,
sharedFrames.heartThree, birbFrames.heartThree,
sharedFrames.heartFour, birbFrames.heartFour,
sharedFrames.heartThree, birbFrames.heartThree,
sharedFrames.heartFour, birbFrames.heartFour,
], [ ], [
60, 60,
80, 80,
@@ -392,6 +417,14 @@ const Animations = {
], false), ], false),
}; };
const DECORATION_ANIMATIONS = {
mac: new Anim([
decorationFrames.mac,
], [
1000,
]),
};
const styleElement = document.createElement("style"); const styleElement = document.createElement("style");
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
@@ -442,8 +475,8 @@ function init() {
document.head.appendChild(styleElement); document.head.appendChild(styleElement);
canvas.id = "birb"; canvas.id = "birb";
canvas.width = sharedFrames.base.pixels[0].length * CANVAS_PIXEL_SIZE; canvas.width = birbFrames.base.pixels[0].length * CANVAS_PIXEL_SIZE;
canvas.height = MAX_HEIGHT * CANVAS_PIXEL_SIZE; canvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE;
document.body.appendChild(canvas); document.body.appendChild(canvas);
window.addEventListener("scroll", () => { window.addEventListener("scroll", () => {
@@ -567,6 +600,25 @@ function makeElement(className, textContent, id) {
return element; 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 * Add the start menu to the page if it doesn't already exist
*/ */
@@ -633,11 +685,10 @@ function isStartMenuOpen() {
} }
/** /**
* Make the given HTML element draggable by the window header * @param {HTMLElement|null} element
* @param {HTMLElement|null} windowHeader
*/ */
function makeDraggable(windowHeader) { function makeDraggable(element, parent = true) {
if (!windowHeader) { if (!element) {
return; return;
} }
@@ -645,18 +696,19 @@ function makeDraggable(windowHeader) {
let offsetX = 0; let offsetX = 0;
let offsetY = 0; let offsetY = 0;
// Get the parent window element if (parent) {
const windowElement = windowHeader.parentElement; element = element.parentElement;
}
if (!windowElement) { if (!element) {
console.error("Birb: Window element not found"); console.error("Birb: Parent element not found");
return; return;
} }
windowHeader.addEventListener("mousedown", (e) => { element.addEventListener("mousedown", (e) => {
isMouseDown = true; isMouseDown = true;
offsetX = e.clientX - windowElement.offsetLeft; offsetX = e.clientX - element.offsetLeft;
offsetY = e.clientY - windowElement.offsetTop; offsetY = e.clientY - element.offsetTop;
}); });
document.addEventListener("mouseup", () => { document.addEventListener("mouseup", () => {
@@ -665,17 +717,18 @@ function makeDraggable(windowHeader) {
document.addEventListener("mousemove", (e) => { document.addEventListener("mousemove", (e) => {
if (isMouseDown) { if (isMouseDown) {
windowElement.style.left = `${e.clientX - offsetX}px`; element.style.left = `${e.clientX - offsetX}px`;
windowElement.style.top = `${e.clientY - offsetY}px`; element.style.top = `${e.clientY - offsetY}px`;
} }
}); });
} }
/** /**
* @param {string} dataUri * @param {string} dataUri
* @param {boolean} [templateColors]
* @returns {string[][]} * @returns {string[][]}
*/ */
function dataUriTo2DArray(dataUri) { function dataUriTo2DArray(dataUri, templateColors = true) {
const img = new Image(); const img = new Image();
img.src = dataUri; img.src = dataUri;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@@ -702,6 +755,10 @@ function dataUriTo2DArray(dataUri) {
continue; continue;
} }
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; 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) { if (SPRITESHEET_COLOR_MAP[hex] === undefined) {
console.error(`Unknown color: ${hex}`); console.error(`Unknown color: ${hex}`);
row.push(TRANSPARENT); row.push(TRANSPARENT);
@@ -716,13 +773,14 @@ function dataUriTo2DArray(dataUri) {
/** /**
* @param {string[][]} array * @param {string[][]} array
* @param {number} sprite * @param {number} sprite
* @param {number} [width]
* @returns {string[][]} * @returns {string[][]}
*/ */
function getLayer(array, sprite) { function getLayer(array, sprite, width = SPRITE_WIDTH) {
// From an array of a horizontal sprite sheet, get the layer for a specific sprite // From an array of a horizontal sprite sheet, get the layer for a specific sprite
const layer = []; const layer = [];
for (let y = 0; y < SPRITE_WIDTH; y++) { for (let y = 0; y < width; y++) {
layer.push(array[y].slice(sprite * SPRITE_WIDTH, (sprite + 1) * SPRITE_WIDTH)); layer.push(array[y].slice(sprite * width, (sprite + 1) * width));
} }
return layer; return layer;
} }