Add falling feather

This commit is contained in:
Idrees Hassan
2024-12-28 16:17:39 -05:00
parent 8d7679f9bd
commit ddbaa4c029

130
birb.js
View File

@@ -39,6 +39,7 @@ const AFK_TIME = 1000 * 30;
const SPRITE_HEIGHT = 32;
const START_MENU_ID = "birb-start-menu";
const FIELD_GUIDE_ID = "birb-field-guide";
const FEATHER_ID = "birb-feather";
const styles = `
#birb {
@@ -47,7 +48,7 @@ const styles = `
bottom: 0;
transform: scale(${CSS_SCALE});
transform-origin: bottom;
z-index: 999999999;
z-index: 999999998;
cursor: pointer;
}
@@ -62,7 +63,7 @@ const styles = `
.birb-window {
font-family: "Monocraft";
z-index: 1000;
z-index: 999999999;
position: fixed;
background-color: #ffecda;
box-shadow:
@@ -189,13 +190,10 @@ const styles = `
}
.birb-grid-item {
border: var(--border-size) solid rgb(255, 207, 144);
box-shadow: 0 0 0 var(--border-size) white;
width: 64px;
height: 64px;
overflow: hidden;
margin: 6px;
background: rgb(255, 221, 177, 0.5);
display: flex;
justify-content: center;
align-items: center;
@@ -207,6 +205,21 @@ const styles = `
transform: scale(2);
padding-bottom: var(--border-size);
}
.birb-grid-item, .birb-field-guide-description {
border: var(--border-size) solid rgb(255, 207, 144);
box-shadow: 0 0 0 var(--border-size) white;
background: rgba(255, 221, 177, 0.5);
}
.birb-field-guide-description {
margin-top: 10px;
padding: 8px;
padding-top: 4px;
padding-bottom: 4px;
font-size: 14px;
color: rgb(124, 108, 75);
}
`;
class Layer {
@@ -266,7 +279,7 @@ class Frame {
/**
* @param {CanvasRenderingContext2D} ctx
* @param {number} direction
* @param {Theme} [theme]
* @param {BirdType} [theme]
*/
draw(ctx, direction, theme) {
for (let y = 0; y < this.pixels.length; y++) {
@@ -300,7 +313,7 @@ class Anim {
* @param {CanvasRenderingContext2D} ctx
* @param {number} direction
* @param {number} timeStart The start time of the animation in milliseconds
* @param {Theme} [theme] The theme to use for the animation
* @param {BirdType} [theme] The theme to use for the animation
* @returns {boolean} Whether the animation is complete
*/
draw(ctx, direction, timeStart, theme) {
@@ -337,6 +350,7 @@ const WING_EDGE = "wing-edge";
const HEART = "heart";
const HEART_BORDER = "heart-border";
const HEART_SHINE = "heart-shine";
const FEATHER_SPINE = "feather-spine";
const SPRITESHEET_COLOR_MAP = {
"transparent": TRANSPARENT,
@@ -352,14 +366,19 @@ const SPRITESHEET_COLOR_MAP = {
"#326ed9": WING_EDGE,
"#c82e2e": HEART,
"#501a1a": HEART_BORDER,
"#ff6b6b": HEART_SHINE
"#ff6b6b": HEART_SHINE,
"#373737": FEATHER_SPINE,
};
class Theme {
class BirdType {
/**
* @param {string} name
* @param {string} description
* @param {Record<string, string>} colors
*/
constructor(colors) {
constructor(name, description, colors) {
this.name = name;
this.description = description;
const defaultColors = {
[TRANSPARENT]: "transparent",
[OUTLINE]: "#000000",
@@ -367,13 +386,15 @@ class Theme {
[HEART]: "#c82e2e",
[HEART_BORDER]: "#501a1a",
[HEART_SHINE]: "#ff6b6b",
[FEATHER_SPINE]: "#373737",
};
this.colors = { ...defaultColors, ...colors };
}
}
const themes = {
bluebird: new Theme({
const species = {
bluebird: new BirdType("Eastern Bluebird",
"Native to North American and very social, though can be timid around people.", {
[BEAK]: "#000000",
[FOOT]: "#af8e75",
[EYE]: "#000000",
@@ -383,7 +404,8 @@ const themes = {
[WING]: "#578ae6",
[WING_EDGE]: "#326ed9",
}),
shimaEnaga: new Theme({
shimaEnaga: new BirdType("Shima Enaga",
"Small, fluffy birds found in the snowy regions of Japan", {
[BEAK]: "#000000",
[FOOT]: "#af8e75",
[EYE]: "#000000",
@@ -403,8 +425,10 @@ 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 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=";
/**
* Load the spritesheet and return the pixelmap template
@@ -462,9 +486,10 @@ function loadSpritesheetPixels(dataUri, templateColors = true) {
});
}
Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECORATIONS_SPRITE_SHEET_URI, false)]).then(([birbPixels, decorationPixels]) => {
Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECORATIONS_SPRITE_SHEET_URI, false), loadSpritesheetPixels(FEATHER_SPRITE_SHEET_URI)]).then(([birbPixels, decorationPixels, featherPixels ]) => {
const SPRITE_SHEET = birbPixels;
const DECORATIONS_SPRITE_SHEET = decorationPixels;
const FEATHER_SPRITE_SHEET = featherPixels;
const layers = {
base: new Layer(getLayer(SPRITE_SHEET, 0)),
@@ -482,6 +507,10 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
mac: new Layer(getLayer(DECORATIONS_SPRITE_SHEET, 0, DECORATIONS_SPRITE_WIDTH)),
};
const featherLayers = {
feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)),
};
const birbFrames = {
base: new Frame([layers.base]),
headDown: new Frame([layers.down]),
@@ -497,6 +526,10 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
mac: new Frame([decorationLayers.mac]),
};
const featherFrames = {
feather: new Frame([featherLayers.feather]),
};
const Animations = {
STILL: new Anim([birbFrames.base], [1000]),
BOB: new Anim([
@@ -546,6 +579,14 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
]),
};
const FEATHER_ANIMATIONS = {
feather: new Anim([
featherFrames.feather,
], [
1000,
]),
};
const styleElement = document.createElement("style");
const canvas = document.createElement("canvas");
@@ -644,6 +685,7 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
setState(States.IDLE);
}
}
updateFeather();
}
function draw() {
@@ -676,7 +718,7 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (currentAnimation.draw(ctx, direction, animStart, themes[currentTheme])) {
if (currentAnimation.draw(ctx, direction, animStart, species[currentTheme])) {
setAnimation(Animations.STILL);
}
@@ -724,8 +766,42 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
makeDraggable(decorationCanvas, false);
}
insertFeather();
function insertFeather() {
let theme = species[currentTheme];
const featherCanvas = document.createElement("canvas");
featherCanvas.id = "birb-feather";
featherCanvas.classList.add("birb-decoration");
featherCanvas.width = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE;
featherCanvas.height = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE;
const x = featherCanvas.width * 2 + Math.random() * (window.innerWidth - featherCanvas.width * 4);
featherCanvas.style.marginLeft = `${x}px`;
const featherCtx = featherCanvas.getContext("2d");
if (!featherCtx) {
return;
}
FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), theme);
document.body.appendChild(featherCanvas);
}
function updateFeather() {
const feather = document.querySelector("#birb-feather");
const featherGravity = 1;
if (!feather || !(feather instanceof HTMLElement)) {
return;
}
const y = parseInt(feather.style.top || "0") + featherGravity;
feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`;
if (y < window.innerHeight - feather.offsetHeight) {
feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`;
}
}
// insertDecoration();
// insertFieldGuide();
insertFieldGuide();
function insertFieldGuide() {
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
@@ -748,6 +824,8 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
<div class="birb-grid-item"></div>
<div class="birb-grid-item"></div>
</div>
<div class="birb-field-guide-description">
</div>
</div>`
const fieldGuide = makeElement("birb-window", undefined, FIELD_GUIDE_ID);
fieldGuide.innerHTML = html;
@@ -765,7 +843,17 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
return;
}
content.innerHTML = "";
for (const [name, theme] of Object.entries(themes)) {
const generateDescription = (theme) => {
return "<b>" + theme.name + "</b><div style='height: 0.3em'></div>" + theme.description;
};
const description = fieldGuide.querySelector(".birb-field-guide-description");
if (!description) {
return;
}
description.innerHTML = generateDescription(species[currentTheme]);
for (const [name, theme] of Object.entries(species)) {
const themeElement = makeElement("birb-grid-item");
const themeCanvas = document.createElement("canvas");
themeCanvas.width = SPRITE_WIDTH * CANVAS_PIXEL_SIZE;
@@ -774,7 +862,6 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
if (!themeCtx) {
return;
}
// Draw the bird with the theme
birbFrames.base.draw(themeCtx, Directions.RIGHT, theme);
themeElement.appendChild(themeCanvas);
content.appendChild(themeElement);
@@ -782,6 +869,13 @@ Promise.all([loadSpritesheetPixels(SPRITE_SHEET_URI), loadSpritesheetPixels(DECO
switchTheme(name);
fieldGuide.remove();
});
themeElement.addEventListener("mouseover", () => {
console.log("mouseover");
description.innerHTML = generateDescription(theme);
});
themeElement.addEventListener("mouseout", () => {
description.innerHTML = generateDescription(species[currentTheme]);
});
}
}