1 Commits

Author SHA1 Message Date
Idrees Hassan
ddcd7a693d Create birb-white.aseprite 2026-01-24 22:32:16 -05:00
76 changed files with 2232 additions and 7273 deletions

5
.gitignore vendored
View File

@@ -3,8 +3,3 @@
/dist/birb.bundled.js /dist/birb.bundled.js
obsidian-test.sh obsidian-test.sh
build-cache.json build-cache.json
.vscode/settings.json
aseprite/birb-test.aseprite
aseprite/wren.aseprite
aseprite/birb-no-shoulder.aseprite
aseprite/birb-fat.aseprite

View File

@@ -19,8 +19,6 @@ It's a pet bird that hops around your computer, what more could you want?
#### Join the [Discord](https://discord.gg/6yxE9prcNc) to help me beta test new features and suggest ideas! #### Join the [Discord](https://discord.gg/6yxE9prcNc) to help me beta test new features and suggest ideas!
![Screen Recording 2026-01-26 at 5 09 45PM](https://github.com/user-attachments/assets/dbde4b75-7195-476f-9411-c1ee7ba7a96f)
## Features ## Features
- A cute little pixel art bird hops around your apps and websites - A cute little pixel art bird hops around your apps and websites
@@ -97,8 +95,7 @@ If you are running Pocket bird on a browser, the extension needs these permissio
Here are some websites where you can find Pocket Bird hopping around: Here are some websites where you can find Pocket Bird hopping around:
- [https://grepjason.sh](https://grepjason.sh) - [https://grepjason.sh](https://grepjason.sh)
- [https://binarydigit.net](https://binarydigit.net) - [https://binarydigit.dev](https://binarydigit.dev)
- [melvinsalas.com](melvinsalas.com)
*If you've added Pocket Bird to your website, let me know and I'll add it to this list!* *If you've added Pocket Bird to your website, let me know and I'll add it to this list!*

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -35,7 +35,7 @@ const MONOCRAFT_URL = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40
const VERSION_KEY = "__VERSION__"; const VERSION_KEY = "__VERSION__";
const STYLESHEET_KEY = "___STYLESHEET___"; const STYLESHEET_KEY = "___STYLESHEET___";
const MONOCRAFT_URL_KEY = "__MONOCRAFT_URL__"; const MONOCRAFT_SRC_KEY = "__MONOCRAFT_SRC__";
const CODE_KEY = "__CODE__"; const CODE_KEY = "__CODE__";
const spriteSheets = [ const spriteSheets = [
@@ -85,9 +85,7 @@ writeFileSync(BUILD_CACHE_PATH, JSON.stringify(buildCache), 'utf8');
/** /**
* @param {string} entryPoint * @param {string} entryPoint
* @param {boolean} [embedFont] When true, the Monocraft font is base64-encoded * @param {boolean} [embedFont]
* and substituted into the __MONOCRAFT_FONT_FACE__ placeholder so the
* build is fully self-contained (used for Obsidian).
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
async function generateCode(entryPoint, embedFont = false) { async function generateCode(entryPoint, embedFont = false) {
@@ -111,15 +109,6 @@ async function generateCode(entryPoint, embedFont = false) {
// Replace version placeholder // Replace version placeholder
birbJs = birbJs.replaceAll(VERSION_KEY, version); birbJs = birbJs.replaceAll(VERSION_KEY, version);
// Replace CDN font URL placeholder
if (embedFont) {
// Embed as a base64 data URI so the build works fully offline.
const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64');
birbJs = birbJs.replaceAll(MONOCRAFT_URL_KEY, `data:font/otf;base64,${monocraftFontData}`);
} else {
birbJs = birbJs.replaceAll(MONOCRAFT_URL_KEY, MONOCRAFT_URL);
}
// Compile and insert sprite sheets // Compile and insert sprite sheets
for (const spriteSheet of spriteSheets) { for (const spriteSheet of spriteSheets) {
const dataUri = readFileSync(spriteSheet.path, 'base64'); const dataUri = readFileSync(spriteSheet.path, 'base64');
@@ -130,6 +119,14 @@ async function generateCode(entryPoint, embedFont = false) {
const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8'); const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8');
birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent); birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent);
if (embedFont) {
// Encode font to data URI
const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64');
const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`;
birbJs = birbJs.replaceAll(MONOCRAFT_SRC_KEY, monocraftDataUri);
} else {
birbJs = birbJs.replaceAll(MONOCRAFT_SRC_KEY, MONOCRAFT_URL);
}
return birbJs; return birbJs;
} }
@@ -190,7 +187,6 @@ async function buildExtension() {
} }
async function buildObsidian() { async function buildObsidian() {
// embedFont=true: bakes the font as base64 so the plugin works fully offline.
const birbJs = await generateCode(OBSIDIAN_ENTRY, true); const birbJs = await generateCode(OBSIDIAN_ENTRY, true);
mkdirSync(OBSIDIAN_DIR, { recursive: true }); mkdirSync(OBSIDIAN_DIR, { recursive: true });

BIN
dist/extension.zip vendored

Binary file not shown.

1464
dist/extension/birb.js vendored

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 B

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1008 B

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 957 B

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 866 B

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Pocket Bird", "name": "Pocket Bird",
"description": "It's a pet bird in your browser, what more could you want?", "description": "It's a pet bird in your browser, what more could you want?",
"version": "2026.4.6", "version": "2026.1.24",
"homepage_url": "https://idreesinc.com", "homepage_url": "https://idreesinc.com",
"icons": { "icons": {
"48": "images/icons/transparent/48x48x1.png", "48": "images/icons/transparent/48x48x1.png",

1456
dist/obsidian/main.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{ {
"id": "pocket-bird", "id": "pocket-bird",
"name": "Pocket Bird", "name": "Pocket Bird",
"version": "2026.4.6", "version": "2026.1.24",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "Add a pet bird to fly around your notes and keep you company!", "description": "Add a pet bird to fly around your notes and keep you company!",
"author": "Idrees Hassan", "author": "Idrees Hassan",

File diff suppressed because it is too large Load Diff

1454
dist/web/birb.embed.js vendored

File diff suppressed because it is too large Load Diff

1454
dist/web/birb.js vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,380 +0,0 @@
// @ts-check
import { SPRITE_SHEET_COLOR_MAP, PALETTE, DEFAULT_COLOR_OVERRIDES, loadSpriteSheetPixels } from '../src/animation/sprites.js';
import Layer, { TAG } from '../src/animation/layer.js';
import Frame from '../src/animation/frame.js';
import { Directions, getLayerPixels } from '../src/shared.js';
import species from '../src/species.js';
/** @typedef {import('../src/species.js').Species} Species */
const COLOR_MAP = SPRITE_SHEET_COLOR_MAP;
const SPRITE_PATH = "../sprites/birb.png";
const SPRITE_SIZE = 32;
const IGNORED_PARTS = new Set(
["transparent", "border", "heart", "heart-border", "heart-shine", "feather-spine"]
);
/** @type {HTMLCanvasElement} */
// @ts-ignore
const canvas = document.getElementById("preview");
/** @type {CanvasRenderingContext2D} */
// @ts-ignore
const ctx = canvas.getContext("2d");
/** @type {HTMLElement} */
// @ts-ignore
const editor = document.getElementById("editor");
const colorPickerInput = document.createElement("input");
/** @type {HTMLElement} */
// @ts-ignore
const jsonElement = document.getElementById("json");
/** @type {Record<string, HTMLElement>} */
const colorElements = {};
/** @type {string|null} */
let selectedPart = null;
/** @type {HTMLElement|null} */
let selectedColorElement = null;
const spriteCanvas = document.createElement('canvas');
spriteCanvas.width = canvas.width;
spriteCanvas.height = canvas.height;
/** @type {CanvasRenderingContext2D} */
// @ts-ignore
const spriteCtx = spriteCanvas.getContext('2d');
/** @type {Species} */
let currentSpecies = JSON.parse(JSON.stringify(species.bluebird));
let speciesHistory = [JSON.parse(JSON.stringify(currentSpecies))];
let historyIndex = 0;
/** @type {Frame|null} */
let baseFrame = null;
function drawBackground() {
const patternSize = 2;
const colors = ["#edf0f4", "#dadbe0"];
for (let y = 0; y < canvas.height; y += patternSize) {
for (let x = 0; x < canvas.width; x += patternSize) {
ctx.fillStyle = ((x / patternSize + y / patternSize) % 2 === 0) ? colors[0] : colors[1];
ctx.fillRect(x, y, patternSize, patternSize);
}
}
}
/**
* Build the full palette color scheme from the current species settings
* @returns {Record<string, string>}
*/
function buildColorScheme() {
/** @type {Record<string, string>} */
const scheme = {};
for (const paletteName of Object.values(PALETTE)) {
scheme[paletteName] = getColor(paletteName);
}
return scheme;
}
function draw() {
if (!baseFrame) {
return;
}
drawBackground();
baseFrame.draw(spriteCtx, Directions.LEFT, 1, buildColorScheme(), currentSpecies.tags || []);
ctx.drawImage(spriteCanvas, 0, 0);
}
function commitChange() {
const previousSpecies = speciesHistory[historyIndex];
let changed = false;
// Check for changes in colors
for (const part of Object.keys(currentSpecies.colors)) {
if (currentSpecies.colors[part] !== previousSpecies.colors[part]) {
changed = true;
break;
}
}
if (!changed) {
for (const part of Object.keys(previousSpecies.colors)) {
if (!(part in currentSpecies.colors)) {
changed = true;
break;
}
}
}
// Check for changes in tags
if (!changed) {
const prevTags = new Set(previousSpecies.tags || []);
const currTags = new Set(currentSpecies.tags || []);
for (const tag of prevTags) {
if (!currTags.has(tag)) {
changed = true;
break;
}
}
}
if (!changed) {
for (const tag of currentSpecies.tags || []) {
if (!previousSpecies.tags || !previousSpecies.tags.includes(tag)) {
changed = true;
break;
}
}
}
if (changed) {
speciesHistory = speciesHistory.slice(0, historyIndex + 1);
speciesHistory.push(JSON.parse(JSON.stringify(currentSpecies)));
historyIndex++;
localStorage.setItem("speciesHistory", JSON.stringify(speciesHistory));
}
updateJson();
draw();
}
function loadEditor() {
for (const [color, part] of Object.entries(COLOR_MAP)) {
if (IGNORED_PARTS.has(part)) {
continue;
}
const item = createColorSwatch(part, getColor(part) || color);
editor.appendChild(item);
}
for (const value of Object.values(TAG)) {
if (value === TAG.DEFAULT) {
continue;
}
editor.appendChild(createTagToggle(value, getTag(value)));
}
}
/**
* @param {string} part
* @return {string}
*/
function getColor(part) {
if (currentSpecies.colors[part]) {
return currentSpecies.colors[part];
}
const override = DEFAULT_COLOR_OVERRIDES[/** @type {keyof typeof DEFAULT_COLOR_OVERRIDES} */ (part)];
if (override) {
return getColor(override);
}
for (const [color, partName] of Object.entries(COLOR_MAP)) {
if (partName === part) {
return color;
}
}
return "transparent";
}
/**
* @param {string} tag
* @returns {boolean}
*/
function getTag(tag) {
return currentSpecies.tags ? currentSpecies.tags.includes(tag) : false;
}
/**
* @param {string} tag
* @param {boolean} enabled
*/
function setTag(tag, enabled) {
if (!currentSpecies.tags) {
currentSpecies.tags = [];
}
if (enabled) {
if (!currentSpecies.tags.includes(tag)) {
currentSpecies.tags.push(tag);
}
} else {
currentSpecies.tags = currentSpecies.tags.filter(t => t !== tag);
}
}
function createColorPicker() {
colorPickerInput.type = "text";
colorPickerInput.id = "color-picker-interceptor";
colorPickerInput.setAttribute("data-coloris", "");
document.body.appendChild(colorPickerInput);
colorPickerInput.addEventListener("input", () => {
if (selectedColorElement && selectedPart !== null) {
const newColor = colorPickerInput.value;
selectedColorElement.style.backgroundColor = newColor;
currentSpecies.colors[selectedPart] = newColor;
draw();
}
});
document.addEventListener("mouseup", () => {
if (selectedPart !== null && !jsonElement.contains(document.activeElement)) {
commitChange();
}
});
}
/**
* @param {string} label
* @param {string} color
* @returns {HTMLDivElement}
*/
function createColorSwatch(label, color) {
const item = document.createElement("div");
item.classList.add("editor-item");
const colorElement = document.createElement("div");
colorElement.classList.add("color");
colorElement.style.backgroundColor = color;
colorElements[label] = colorElement;
item.appendChild(colorElement);
if (color !== "transparent") {
colorElement.addEventListener("click", () => {
selectedPart = label;
selectedColorElement = colorElement;
const rect = colorElement.getBoundingClientRect();
colorPickerInput.style.left = rect.left + "px";
colorPickerInput.style.top = (rect.bottom + window.scrollY) + "px";
colorPickerInput.value = currentSpecies.colors[label] || color;
colorPickerInput.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
} else {
colorElement.classList.add("color--transparent");
}
const labelElement = document.createElement("div");
const labelText = label.replaceAll("-", " ").toUpperCase();
labelElement.classList.add("label");
labelElement.textContent = labelText;
labelElement.title = "Click to remove from species";
labelElement.addEventListener("click", () => {
delete currentSpecies.colors[label];
colorElement.style.backgroundColor = getColor(label);
commitChange();
refreshEditor();
});
item.appendChild(labelElement);
return item;
}
/**
* @param {string} tag
* @param {boolean} enabled
* @returns {HTMLDivElement}
*/
function createTagToggle(tag, enabled) {
const item = document.createElement("div");
item.classList.add("editor-item");
const toggle = document.createElement("button");
toggle.id = `tag-toggle-${tag}`;
toggle.classList.add("tag-toggle");
toggle.textContent = "✓";
toggle.addEventListener("click", () => {
setTag(tag, !getTag(tag));
toggle.classList.toggle("tag-toggle--active", getTag(tag));
commitChange();
draw();
});
item.appendChild(toggle);
const labelElement = document.createElement("div");
labelElement.classList.add("label");
labelElement.textContent = tag.toUpperCase();
item.appendChild(labelElement);
return item;
}
function refreshEditor() {
for (const [, part] of Object.entries(COLOR_MAP)) {
const el = colorElements[part];
if (el && !el.classList.contains("color--transparent")) {
el.style.backgroundColor = getColor(part);
}
}
if (selectedColorElement && selectedPart !== null) {
colorPickerInput.value = currentSpecies.colors[selectedPart] || "";
}
for (const value of Object.values(TAG)) {
const toggle = editor.querySelector(`#tag-toggle-${value}`);
if (toggle && toggle instanceof HTMLElement) {
toggle.classList.toggle("tag-toggle--active", getTag(value));
}
}
}
function updateJson() {
jsonElement.textContent = JSON.stringify(currentSpecies, null, 2);
}
document.addEventListener("keydown", (e) => {
if (!(e.metaKey || e.ctrlKey)) {
return;
}
if (e.key === "z" && !e.shiftKey) {
if (historyIndex > 0) {
historyIndex--;
currentSpecies = JSON.parse(JSON.stringify(speciesHistory[historyIndex]));
refreshEditor();
updateJson();
draw();
e.preventDefault();
}
} else if ((e.key === "z" && e.shiftKey) || e.key === "y") {
if (historyIndex < speciesHistory.length - 1) {
historyIndex++;
currentSpecies = JSON.parse(JSON.stringify(speciesHistory[historyIndex]));
refreshEditor();
updateJson();
draw();
e.preventDefault();
}
}
});
jsonElement.addEventListener("input", () => {
try {
const parsed = JSON.parse(jsonElement.textContent || "");
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
currentSpecies = parsed;
refreshEditor();
draw();
}
} catch (e) {
}
});
jsonElement.addEventListener("blur", () => {
commitChange();
});
function loadSpeciesHistory() {
const storedHistory = localStorage.getItem("speciesHistory");
if (storedHistory) {
try {
const parsedHistory = JSON.parse(storedHistory);
if (Array.isArray(parsedHistory) && parsedHistory.length > 0) {
speciesHistory = parsedHistory;
currentSpecies = JSON.parse(JSON.stringify(speciesHistory[speciesHistory.length - 1]));
historyIndex = speciesHistory.length - 1;
}
} catch (e) {
console.warn("Failed to parse species history from localStorage:", e);
}
}
refreshEditor();
draw();
}
createColorPicker();
loadEditor();
loadSpeciesHistory();
(async () => {
const pixels = await loadSpriteSheetPixels(SPRITE_PATH);
baseFrame = new Frame([
new Layer(getLayerPixels(pixels, 0, SPRITE_SIZE)),
new Layer(getLayerPixels(pixels, 5, SPRITE_SIZE), TAG.TUFT),
]);
updateJson();
draw();
})();

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Birb Editor</title>
<link rel="stylesheet" href="stylesheet.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.css"/>
<script src="https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.js"></script>
</head>
<body>
<div class="container">
<div class="horizontal-container">
<canvas id="preview" width="32px" height="32px"></canvas>
<div id="editor"></div>
<pre id="json" contenteditable="true"></pre>
</div>
</div>
<script type="module" src="editor.js"></script>
</body>
</html>

View File

@@ -1,143 +0,0 @@
@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css);
body {
background: linear-gradient(to top, #D2DAE9, white);
height: 100%;
margin: 0;
background-repeat: no-repeat;
background-attachment: fixed;
}
* {
box-sizing: border-box;
}
.container {
width: 100vw;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.horizontal-container {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 40px;
}
#preview {
width: 480px;
height: 480px;
image-rendering: pixelated;
filter: drop-shadow(0px 0px 40px rgba(0, 0, 0, 0.1));
box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.1);
border-radius: 40px;
}
#editor {
width: 460px;
height: 480px;
background: #ffffff;
border-radius: 16px;
box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.1);
padding: 40px 50px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 30px 20px;
column-count: 2;
overflow-y: scroll;
}
#json {
width: 200px;
height: 480px;
background: #ffffff;
border-radius: 16px;
box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.1);
padding: 20px;
gap: 20px;
text-align: left;
overflow-x: hidden;
overflow-y: auto;
white-space: pre-wrap;
font-family: 'Fira Code', monospace;
font-size: 12px;
}
.editor-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 20px;
height: 38px;
}
.label {
font-size: 18px;
font-family: 'Fira Code', monospace;
width: 100px;
}
.color {
width: 32px;
height: 32px;
border-radius: 4px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
background: red;
transition: transform 0.1s;
cursor: pointer;
}
.color:hover {
transform: scale(1.15);
transition: 0.1s ease-in;
}
.color--transparent {
pointer-events: none;
cursor: default;
}
#color-picker-interceptor {
position: fixed;
width: 1px;
height: 1px;
opacity: 0;
border: none;
padding: 0;
pointer-events: none;
}
.tag-toggle {
width: 32px;
height: 32px;
border-radius: 4px;
border: none;
background: #f1f1f1;
cursor: pointer;
font-size: 16px;
color: transparent;
transition: background 0.15s, color 0.15s, transform 0.1s;
border: 3px solid #dadada;
}
.tag-toggle:hover {
transform: scale(1.15);
transition: 0.1s ease-in;
}
.tag-toggle--active {
background: #34c85a;
color: white;
border: 0;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 B

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1008 B

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 957 B

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 866 B

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 949 B

After

Width:  |  Height:  |  Size: 939 B

View File

@@ -1,6 +1,10 @@
import species from "../species.js" import { TAG } from "./layer.js";
export const PALETTE = Object.freeze(/** @type {const} */ ({ /**
* Palette color names
* @type {Record<string, string>}
*/
export const PALETTE = {
THEME_HIGHLIGHT: "theme-highlight", THEME_HIGHLIGHT: "theme-highlight",
TRANSPARENT: "transparent", TRANSPARENT: "transparent",
OUTLINE: "outline", OUTLINE: "outline",
@@ -10,37 +14,20 @@ export const PALETTE = Object.freeze(/** @type {const} */ ({
EYE: "eye", EYE: "eye",
FACE: "face", FACE: "face",
HOOD: "hood", HOOD: "hood",
EYEBROW: "eyebrow",
UPPER_EYELID: "upper-eyelid",
UPPER_CORNER_EYE: "upper-corner-eye",
BEHIND_EYE: "behind-eye",
CORNER_EYE: "corner-eye",
TEMPLE: "temple",
LOWER_EYELID: "lower-eyelid",
NOSE: "nose", NOSE: "nose",
NOSE_TIP: "nose-tip",
CHEEK: "cheek",
SCRUFF: "scruff",
CHIN: "chin",
COLLAR: "collar",
COLLAR_SCRUFF: "collar-scruff",
BELLY: "belly", BELLY: "belly",
UNDERBELLY: "underbelly", UNDERBELLY: "underbelly",
WING: "wing", WING: "wing",
SHOULDER: "shoulder",
WING_SPOTS: "wing-spots",
WING_EDGE: "wing-edge", WING_EDGE: "wing-edge",
HEART: "heart", HEART: "heart",
HEART_BORDER: "heart-border", HEART_BORDER: "heart-border",
HEART_SHINE: "heart-shine", HEART_SHINE: "heart-shine",
FEATHER_SPINE: "feather-spine", FEATHER_SPINE: "feather-spine",
})); };
/** @typedef {typeof PALETTE[keyof typeof PALETTE]} PaletteColor */
/** /**
* Mapping of sprite sheet colors to palette colors * Mapping of sprite sheet colors to palette colors
* @type {Record<string, PaletteColor>} * @type {Record<string, string>}
*/ */
export const SPRITE_SHEET_COLOR_MAP = { export const SPRITE_SHEET_COLOR_MAP = {
"transparent": PALETTE.TRANSPARENT, "transparent": PALETTE.TRANSPARENT,
@@ -52,25 +39,10 @@ export const SPRITE_SHEET_COLOR_MAP = {
"#af8e75": PALETTE.FOOT, "#af8e75": PALETTE.FOOT,
"#639bff": PALETTE.FACE, "#639bff": PALETTE.FACE,
"#99e550": PALETTE.HOOD, "#99e550": PALETTE.HOOD,
"#ff5573": PALETTE.EYEBROW,
"#ff768e": PALETTE.UPPER_EYELID,
"#ff90a4": PALETTE.UPPER_CORNER_EYE,
"#ff2c88": PALETTE.BEHIND_EYE,
"#e34f9c": PALETTE.CORNER_EYE,
"#b53477": PALETTE.TEMPLE,
"#ae65f1": PALETTE.LOWER_EYELID,
"#d95763": PALETTE.NOSE, "#d95763": PALETTE.NOSE,
"#b93844": PALETTE.NOSE_TIP,
"#ff67a9": PALETTE.CHEEK,
"#c5e550": PALETTE.SCRUFF,
"#b87af1": PALETTE.CHIN,
"#ffe955": PALETTE.COLLAR,
"#f8ff55": PALETTE.COLLAR_SCRUFF,
"#f8b143": PALETTE.BELLY, "#f8b143": PALETTE.BELLY,
"#ec8637": PALETTE.UNDERBELLY, "#ec8637": PALETTE.UNDERBELLY,
"#578ae6": PALETTE.WING, "#578ae6": PALETTE.WING,
"#55d1f3": PALETTE.SHOULDER,
"#90b0e8": PALETTE.WING_SPOTS,
"#326ed9": PALETTE.WING_EDGE, "#326ed9": PALETTE.WING_EDGE,
"#c82e2e": PALETTE.HEART, "#c82e2e": PALETTE.HEART,
"#501a1a": PALETTE.HEART_BORDER, "#501a1a": PALETTE.HEART_BORDER,
@@ -78,52 +50,16 @@ export const SPRITE_SHEET_COLOR_MAP = {
"#373737": PALETTE.FEATHER_SPINE, "#373737": PALETTE.FEATHER_SPINE,
}; };
/**
* @type {Partial<Record<PaletteColor, PaletteColor>>}
*/
export const DEFAULT_COLOR_OVERRIDES = {
[PALETTE.HOOD]: PALETTE.FACE,
[PALETTE.EYEBROW]: PALETTE.FACE,
[PALETTE.UPPER_EYELID]: PALETTE.EYEBROW,
[PALETTE.UPPER_CORNER_EYE]: PALETTE.EYEBROW,
[PALETTE.BEHIND_EYE]: PALETTE.FACE,
[PALETTE.CORNER_EYE]: PALETTE.FACE,
[PALETTE.TEMPLE]: PALETTE.FACE,
[PALETTE.LOWER_EYELID]: PALETTE.FACE,
[PALETTE.NOSE]: PALETTE.FACE,
[PALETTE.NOSE_TIP]: PALETTE.NOSE,
[PALETTE.CHEEK]: PALETTE.FACE,
[PALETTE.SCRUFF]: PALETTE.FACE,
[PALETTE.CHIN]: PALETTE.FACE,
[PALETTE.COLLAR]: PALETTE.FACE,
[PALETTE.COLLAR_SCRUFF]: PALETTE.COLLAR,
[PALETTE.WING_SPOTS]: PALETTE.WING,
[PALETTE.SHOULDER]: PALETTE.WING,
};
export const RARITY = Object.freeze(/** @type {const} */ ({
COMMON: "common",
UNCOMMON: "uncommon"
}));
/** @typedef {typeof RARITY[keyof typeof RARITY]} Rarity */
export class BirdType { export class BirdType {
/** /**
* @param {string} name * @param {string} name
* @param {string} description * @param {string} description
* @param {string} latinName
* @param {string} url
* @param {Record<string, string>} colors * @param {Record<string, string>} colors
* @param {string[]} [tags] * @param {string[]} [tags]
* @param {Rarity} [rarity]
*/ */
constructor(name, description, latinName, url, colors, tags = [], rarity = RARITY.COMMON) { constructor(name, description, colors, tags = []) {
this.name = name; this.name = name;
this.description = description; this.description = description;
this.latinName = latinName;
this.url = url;
const defaultColors = { const defaultColors = {
[PALETTE.TRANSPARENT]: "transparent", [PALETTE.TRANSPARENT]: "transparent",
[PALETTE.OUTLINE]: "#000000", [PALETTE.OUTLINE]: "#000000",
@@ -135,90 +71,139 @@ export class BirdType {
[PALETTE.HEART_SHINE]: "#ff6b6b", [PALETTE.HEART_SHINE]: "#ff6b6b",
[PALETTE.FEATHER_SPINE]: "#373737", [PALETTE.FEATHER_SPINE]: "#373737",
[PALETTE.HOOD]: colors.face, [PALETTE.HOOD]: colors.face,
[PALETTE.EYEBROW]: colors.face,
[PALETTE.UPPER_EYELID]: colors.eyebrow || colors.face,
[PALETTE.UPPER_CORNER_EYE]: colors.eyebrow || colors.face,
[PALETTE.BEHIND_EYE]: colors.face,
[PALETTE.CORNER_EYE]: colors.face,
[PALETTE.TEMPLE]: colors.face,
[PALETTE.LOWER_EYELID]: colors.face,
[PALETTE.NOSE]: colors.face, [PALETTE.NOSE]: colors.face,
[PALETTE.NOSE_TIP]: colors.nose || colors.face,
[PALETTE.CHEEK]: colors.face,
[PALETTE.SCRUFF]: colors.face,
[PALETTE.CHIN]: colors.face,
[PALETTE.COLLAR]: colors.face,
[PALETTE.COLLAR_SCRUFF]: colors.collar || colors.face,
[PALETTE.SHOULDER]: colors.wing,
}; };
/** @type {Record<string, string>} */ /** @type {Record<string, string>} */
this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
this.tags = tags; this.tags = tags;
/** @type {Rarity} */
this.rarity = rarity;
} }
} }
/**
* Load a sprite sheet image and convert it to a 2D array of palette color names
* @param {string} src URL or data URI of the sprite sheet image
* @param {boolean} [templateColors] Whether to map pixel colors to palette names
* @returns {Promise<string[][]>}
*/
export function loadSpriteSheetPixels(src, templateColors = true) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = src;
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;
}
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(PALETTE.TRANSPARENT);
continue;
}
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
if (!templateColors) {
row.push(hex);
continue;
}
if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) {
row.push(hex);
continue;
}
row.push(SPRITE_SHEET_COLOR_MAP[hex]);
}
hexArray.push(row);
}
resolve(hexArray);
};
img.onerror = (err) => {
reject(err);
};
});
}
/** @type {Record<string, BirdType>} */ /** @type {Record<string, BirdType>} */
export const SPECIES = Object.fromEntries( export const SPECIES = {
Object.entries(species).map(([id, data]) => [ bluebird: new BirdType("Eastern Bluebird",
id, "Native to North American and very social, though can be timid around people.", {
new BirdType(data.name, data.description, data.latinName, data.url, data.colors, data.tags, data.rarity) [PALETTE.FOOT]: "#af8e75",
]), [PALETTE.FACE]: "#639bff",
); [PALETTE.BELLY]: "#f8b143",
[PALETTE.UNDERBELLY]: "#ec8637",
[PALETTE.WING]: "#578ae6",
[PALETTE.WING_EDGE]: "#326ed9",
}),
shimaEnaga: new BirdType("Shima Enaga",
"Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", {
[PALETTE.FOOT]: "#af8e75",
[PALETTE.FACE]: "#ffffff",
[PALETTE.BELLY]: "#ebe9e8",
[PALETTE.UNDERBELLY]: "#ebd9d0",
[PALETTE.WING]: "#f3d3c1",
[PALETTE.WING_EDGE]: "#2d2d2d",
[PALETTE.THEME_HIGHLIGHT]: "#d7ac93",
}),
tuftedTitmouse: new BirdType("Tufted Titmouse",
"Native to the eastern United States, full of personality, and notably my wife's favorite bird.", {
[PALETTE.FOOT]: "#af8e75",
[PALETTE.FACE]: "#c7cad7",
[PALETTE.BELLY]: "#e4e5eb",
[PALETTE.UNDERBELLY]: "#d7cfcb",
[PALETTE.WING]: "#b1b5c5",
[PALETTE.WING_EDGE]: "#9d9fa9",
[PALETTE.THEME_HIGHLIGHT]: "#b9abcf",
}, [TAG.TUFT]),
europeanRobin: new BirdType("European Robin",
"Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", {
[PALETTE.FOOT]: "#af8e75",
[PALETTE.FACE]: "#ffaf34",
[PALETTE.HOOD]: "#aaa094",
[PALETTE.BELLY]: "#ffaf34",
[PALETTE.UNDERBELLY]: "#babec2",
[PALETTE.WING]: "#aaa094",
[PALETTE.WING_EDGE]: "#888580",
[PALETTE.THEME_HIGHLIGHT]: "#ffaf34",
}),
redCardinal: new BirdType("Red Cardinal",
"Native to the eastern United States, this strikingly red bird is hard to miss.", {
[PALETTE.BEAK]: "#d93619",
[PALETTE.FOOT]: "#af8e75",
[PALETTE.FACE]: "#31353d",
[PALETTE.HOOD]: "#e83a1b",
[PALETTE.BELLY]: "#e83a1b",
[PALETTE.UNDERBELLY]: "#dc3719",
[PALETTE.WING]: "#d23215",
[PALETTE.WING_EDGE]: "#b1321c",
}, [TAG.TUFT]),
americanGoldfinch: new BirdType("American Goldfinch",
"Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", {
[PALETTE.BEAK]: "#ffaf34",
[PALETTE.FOOT]: "#af8e75",
[PALETTE.FACE]: "#fff255",
[PALETTE.NOSE]: "#383838",
[PALETTE.HOOD]: "#383838",
[PALETTE.BELLY]: "#fff255",
[PALETTE.UNDERBELLY]: "#f5ea63",
[PALETTE.WING]: "#e8e079",
[PALETTE.WING_EDGE]: "#191919",
[PALETTE.THEME_HIGHLIGHT]: "#ffcc00"
}),
barnSwallow: new BirdType("Barn Swallow",
"Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", {
[PALETTE.FOOT]: "#af8e75",
[PALETTE.FACE]: "#db7c4d",
[PALETTE.BELLY]: "#f7e1c9",
[PALETTE.UNDERBELLY]: "#ebc9a3",
[PALETTE.WING]: "#2252a9",
[PALETTE.WING_EDGE]: "#1c448b",
[PALETTE.HOOD]: "#2252a9",
}),
mistletoebird: new BirdType("Mistletoebird",
"Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", {
[PALETTE.FOOT]: "#6c6a7c",
[PALETTE.FACE]: "#352e6d",
[PALETTE.BELLY]: "#fd6833",
[PALETTE.UNDERBELLY]: "#e6e1d8",
[PALETTE.WING]: "#342b7c",
[PALETTE.WING_EDGE]: "#282065",
}),
redAvadavat: new BirdType("Red Avadavat",
"Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.", {
[PALETTE.BEAK]: "#f71919",
[PALETTE.FOOT]: "#af7575",
[PALETTE.FACE]: "#cb092b",
[PALETTE.BELLY]: "#ae1724",
[PALETTE.UNDERBELLY]: "#831b24",
[PALETTE.WING]: "#7e3030",
[PALETTE.WING_EDGE]: "#490f0f",
}),
scarletRobin: new BirdType("Scarlet Robin",
"Native to Australia, this striking robin can be found in Eucalyptus forests.", {
[PALETTE.FOOT]: "#494949",
[PALETTE.FACE]: "#3d3d3d",
[PALETTE.BELLY]: "#fc5633",
[PALETTE.UNDERBELLY]: "#dcdcdc",
[PALETTE.WING]: "#2b2b2b",
[PALETTE.WING_EDGE]: "#ebebeb",
[PALETTE.THEME_HIGHLIGHT]: "#fc5633",
}),
americanRobin: new BirdType("American Robin",
"While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", {
[PALETTE.BEAK]: "#e89f30",
[PALETTE.FOOT]: "#9f8075",
[PALETTE.FACE]: "#2d2d2d",
[PALETTE.BELLY]: "#eb7a3a",
[PALETTE.UNDERBELLY]: "#eb7a3a",
[PALETTE.WING]: "#444444",
[PALETTE.WING_EDGE]: "#232323",
[PALETTE.THEME_HIGHLIGHT]: "#eb7a3a",
}),
carolinaWren: new BirdType("Carolina Wren",
"Native to the eastern United States, these little birds are known for their curious and energetic nature.", {
[PALETTE.FOOT]: "#af8e75",
[PALETTE.FACE]: "#edc7a9",
[PALETTE.NOSE]: "#f7eee5",
[PALETTE.HOOD]: "#c58a5b",
[PALETTE.BELLY]: "#e1b796",
[PALETTE.UNDERBELLY]: "#c79e7c",
[PALETTE.WING]: "#c58a5b",
[PALETTE.WING_EDGE]: "#866348",
}),
};

View File

@@ -3,7 +3,7 @@ import Layer, { TAG } from './animation/layer.js';
import Anim from './animation/anim.js'; import Anim from './animation/anim.js';
import { Birb, Animations } from './birb.js'; import { Birb, Animations } from './birb.js';
import { Birdsong } from './sound.js'; import { Birdsong } from './sound.js';
import { Context } from './context.js'; import { Context, ObsidianContext } from './context.js';
import { import {
getContext, getContext,
@@ -24,9 +24,8 @@ import {
} from './shared.js'; } from './shared.js';
import { import {
PALETTE, PALETTE,
SPECIES, SPRITE_SHEET_COLOR_MAP,
RARITY, SPECIES
loadSpriteSheetPixels,
} from './animation/sprites.js'; } from './animation/sprites.js';
import { import {
StickyNote, StickyNote,
@@ -51,13 +50,6 @@ import { HAT, HAT_METADATA, createHatItemAnimation } from './hats.js';
* @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote
*/ */
/**
* @typedef {Object} SavedBirdPosition
* @property {number} x
* @property {number} y
* @property {number} updatedAt
*/
/** /**
* @typedef {Object} BirbSaveData * @typedef {Object} BirbSaveData
* @property {string[]} unlockedSpecies * @property {string[]} unlockedSpecies
@@ -66,7 +58,6 @@ import { HAT, HAT_METADATA, createHatItemAnimation } from './hats.js';
* @property {string} currentHat * @property {string} currentHat
* @property {Partial<Settings>} settings * @property {Partial<Settings>} settings
* @property {SavedStickyNote[]} [stickyNotes] * @property {SavedStickyNote[]} [stickyNotes]
* @property {Record<string, SavedBirdPosition>} [birdPositions]
*/ */
/** /**
@@ -118,24 +109,18 @@ const HOP_DELAY = 500;
const HOP_CHANCE = 1 / (60 * 2.5); // Every 2.5 seconds const HOP_CHANCE = 1 / (60 * 2.5); // Every 2.5 seconds
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours
const UNCOMMON_FEATHER_CHANCE = 0.15; // 15% of feathers are uncommon const HAT_CHANCE = 1 / (60 * 60 * 10); // Every 10 minutes
const HAT_CHANCE = 1 / (60 * 60 * 25); // Every 25 minutes
// Feathers // Feathers
const FEATHER_FALL_SPEED = 1; const FEATHER_FALL_SPEED = 1;
// Petting boosts // Petting boosts
const PET_BOOST_DURATION = 1000 * 60 * 5; // 5 minutes const PET_BOOST_DURATION = 1000 * 60 * 5; // 5 minutes
const PET_FEATHER_BOOST = 2; // Multiplier for feather effect const PET_FEATHER_BOOST = 2;
const PET_HAT_BOOST = 1.5; // Multiplier for hat effect const PET_HAT_BOOST = 1.5;
// Focus element constraints // Focus element constraints
const MIN_FOCUS_ELEMENT_WIDTH = 100; // Minimum width (in px) for an element to be considered a valid perch target const MIN_FOCUS_ELEMENT_WIDTH = 100;
const BIRD_POSITION_SAVE_INTERVAL = 2000; // How often (ms) we attempt to persist position in normal flow
const BIRD_POSITION_SAVE_MIN_DELTA = 6; // Minimum movement (px) compared to last saved position required before writing again
const BIRD_POSITION_TRACKING_DELTA = 0.5; // Minimum movement (px) in runtime tracking to mark position as "dirty"
const MAX_SAVED_BIRD_POSITIONS = 200; // Maximum number of saved bird positions to keep
const TAB_SESSION_MARKER = "__pocket_bird_tab_session__="; // Marker used in localStorage to identify which tab session saved bird positions belong to, to prevent restoring positions from a different tab
/** @type {Partial<Settings>} */ /** @type {Partial<Settings>} */
let userSettings = {}; let userSettings = {};
@@ -182,47 +167,12 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
}; };
const menuItems = [ const menuItems = [
new MenuItem(() => `Pet ${birdBirb()}`, pet, [ new MenuItem(`Pet ${birdBirb()}`, pet),
[0, 1, 1, 0, 1, 1, 0], new MenuItem("Field Guide", insertFieldGuide),
[1, 0, 0, 1, 0, 0, 1], new MenuItem("Wardrobe", insertWardrobe),
[1, 0, 0, 0, 0, 0, 1], new ConditionalMenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote), () => getContext().areStickyNotesEnabled()),
[0, 1, 0, 0, 0, 1, 0], new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)),
[0, 0, 1, 0, 1, 0, 0], new DebugMenuItem("Freeze/Unfreeze", () => {
[0, 0, 0, 1, 0, 0, 0],
]),
new MenuItem("Field Guide", insertFieldGuide, [
[0, 1, 1, 0, 1, 1, 0],
[1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 1],
[1, 1, 1, 0, 1, 1, 1],
]),
new MenuItem("Wardrobe", insertWardrobe, [
[0, 1, 1, 0, 1, 1, 0],
[1, 0, 0, 1, 0, 0, 1],
[1, 1, 0, 0, 0, 1, 1],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 0],
]),
new ConditionalMenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote), () => getContext().areStickyNotesEnabled(), [
[0, 0, 1, 1, 1, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[1, 0, 0, 1, 0, 1, 0],
[1, 0, 1, 0, 0, 1, 0],
[1, 0, 0, 0, 0, 1, 0],
[1, 1, 1, 1, 1, 1, 0],
]),
new MenuItem(() => `Hide ${birdBirb()}`, () => birb.setVisible(false), [
[0, 1, 0, 1, 0, 1, 0],
[1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1],
[0, 1, 0, 0, 0, 1, 0],
[0, 0, 1, 1, 1, 0, 0],
]),
new DebugMenuItem("Freeze", () => {
frozen = !frozen; frozen = !frozen;
}), }),
new DebugMenuItem("Reset Data", resetSaveData), new DebugMenuItem("Reset Data", resetSaveData),
@@ -241,18 +191,11 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
setDebug(false); setDebug(false);
}), }),
new Separator(), new Separator(),
new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), [ new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), false),
[0, 0, 0, 0, 1, 1, 1],
[1, 1, 1, 1, 1, 0, 1],
[0, 0, 0, 0, 1, 1, 1],
[1, 1, 1, 0, 0, 0, 0],
[1, 0, 1, 1, 1, 1, 1],
[1, 1, 1, 0, 0, 0, 0],
], false),
]; ];
const settingsItems = [ const settingsItems = [
new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), undefined, false), new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false),
new Separator(), new Separator(),
new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => { new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => {
userSettings.soundEnabled = !settings().soundEnabled; userSettings.soundEnabled = !settings().soundEnabled;
@@ -272,9 +215,11 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
}), }),
new Separator(), new Separator(),
new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }), new MenuItem(() => `Source Code ${isPetBoostActive() ? " ❤" : ""}`, () => { window.open("https://github.com/IdreesInc/Pocket-Bird"); }),
new MenuItem("Build __VERSION__", () => { alert("Thank you for using Pocket Bird! You are on version: __VERSION__") }, undefined, false), new MenuItem("__VERSION__", () => { alert("Thank you for using Pocket Bird! You are on version: __VERSION__") }, false),
]; ];
const styleElement = document.createElement("style");
/** @type {Birb} */ /** @type {Birb} */
let birb; let birb;
@@ -311,13 +256,6 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
let currentHat = DEFAULT_HAT; let currentHat = DEFAULT_HAT;
// let visible = true; // let visible = true;
let lastPetTimestamp = 0; let lastPetTimestamp = 0;
/** @type {Record<string, SavedBirdPosition>} */
let savedBirdPositions = {};
let holdRestoredYPosition = false;
let birdPositionDirty = false;
let lastTrackedBirdX = birdX;
let lastTrackedBirdY = birdY;
let birdSessionKey = "";
/** @type {StickyNote[]} */ /** @type {StickyNote[]} */
let stickyNotes = []; let stickyNotes = [];
@@ -336,7 +274,6 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD;
unlockedHats = saveData.unlockedHats ?? [DEFAULT_HAT]; unlockedHats = saveData.unlockedHats ?? [DEFAULT_HAT];
currentHat = saveData.currentHat ?? DEFAULT_HAT; currentHat = saveData.currentHat ?? DEFAULT_HAT;
savedBirdPositions = sanitizeSavedBirdPositions(saveData.birdPositions);
stickyNotes = []; stickyNotes = [];
if (saveData.stickyNotes) { if (saveData.stickyNotes) {
@@ -371,9 +308,6 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
left: note.left left: note.left
})); }));
} }
if (Object.keys(savedBirdPositions).length > 0) {
saveData.birdPositions = savedBirdPositions;
}
getContext().putSaveData(saveData); getContext().putSaveData(saveData);
} }
@@ -411,8 +345,8 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
} }
function onLoad() { function onLoad() {
injectStyleElement(getContext().getFontStyles()); styleElement.textContent = STYLESHEET;
injectStyleElement(STYLESHEET); document.head.appendChild(styleElement);
birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT, HATS_SPRITE_SHEET); birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT, HATS_SPRITE_SHEET);
birb.setAnimation(Animations.BOB); birb.setAnimation(Animations.BOB);
@@ -461,26 +395,19 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
drawStickyNotes(stickyNotes, save, deleteStickyNote); drawStickyNotes(stickyNotes, save, deleteStickyNote);
let lastPath = normalizePath(getContext().getPath()); let lastPath = getContext().getPath().split("?")[0];
setInterval(() => { setInterval(() => {
const currentPath = normalizePath(getContext().getPath()); const currentPath = getContext().getPath().split("?")[0];
if (currentPath !== lastPath) { if (currentPath !== lastPath) {
log("Path changed from '" + lastPath + "' to '" + currentPath + "'"); log("Path changed from '" + lastPath + "' to '" + currentPath + "'");
saveBirdPosition(true);
lastPath = currentPath; lastPath = currentPath;
drawStickyNotes(stickyNotes, save, deleteStickyNote); drawStickyNotes(stickyNotes, save, deleteStickyNote);
restoreBirdPosition();
} }
}, URL_CHECK_INTERVAL); }, URL_CHECK_INTERVAL);
setInterval(update, UPDATE_INTERVAL); setInterval(update, UPDATE_INTERVAL);
setInterval(() => saveBirdPosition(), BIRD_POSITION_SAVE_INTERVAL);
window.addEventListener("pagehide", () => saveBirdPosition(true));
window.addEventListener("beforeunload", () => saveBirdPosition(true));
if (!restoreBirdPosition()) { focusOnElement(true);
flyToElement(true);
}
} }
function update() { function update() {
@@ -499,11 +426,11 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
// Idle for a while, do something // Idle for a while, do something
if (focusedElement === null) { if (focusedElement === null) {
// Fly to an element // Fly to an element
flyToElement(); focusOnElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} else if (Math.random() < FOCUS_SWITCH_CHANCE) { } else if (Math.random() < FOCUS_SWITCH_CHANCE) {
// Fly to another element if idle for a longer while // Fly to another element if idle for a longer while
flyToElement(); focusOnElement();
lastActionTimestamp = Date.now(); lastActionTimestamp = Date.now();
} }
} }
@@ -539,11 +466,9 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
// Update the bird's position // Update the bird's position
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) { if (focusedElement && !isWithinHorizontalBounds()) {
flyToElement(); flySomewhere();
} }
if (focusedElement || !holdRestoredYPosition) {
birdY = getFocusedY(); birdY = getFocusedY();
}
} else if (currentState === States.FLYING) { } else if (currentState === States.FLYING) {
// Fly to target location (even if in the air) // Fly to target location (even if in the air)
if (updateParabolicPath(FLY_SPEED, 2)) { if (updateParabolicPath(FLY_SPEED, 2)) {
@@ -557,7 +482,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
startY += targetY - oldTargetY; startY += targetY - oldTargetY;
if (targetY < 0 || targetY > getWindowHeight()) { if (targetY < 0 || targetY > getWindowHeight()) {
// Fly to another element or the ground if the focused element moves out of bounds // Fly to another element or the ground if the focused element moves out of bounds
flyToElement(); flySomewhere();
} }
if (birb.draw(SPECIES[currentSpecies], currentHat)) { if (birb.draw(SPECIES[currentSpecies], currentHat)) {
@@ -573,25 +498,6 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
// Update HTML element position // Update HTML element position
birb.setX(birdX); birb.setX(birdX);
birb.setY(birdY); birb.setY(birdY);
const movedX = Math.abs(birdX - lastTrackedBirdX);
const movedY = Math.abs(birdY - lastTrackedBirdY);
if (movedX >= BIRD_POSITION_TRACKING_DELTA || movedY >= BIRD_POSITION_TRACKING_DELTA) {
birdPositionDirty = true;
lastTrackedBirdX = birdX;
lastTrackedBirdY = birdY;
}
}
/**
* @param {string|null} stylesheetContents
*/
function injectStyleElement(stylesheetContents) {
if (!stylesheetContents) {
return;
}
const element = document.createElement("style");
element.textContent = stylesheetContents;
document.head.appendChild(element);
} }
/** /**
@@ -645,8 +551,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
if (document.querySelector("#" + FEATHER_ID)) { if (document.querySelector("#" + FEATHER_ID)) {
return; return;
} }
const rarity = Math.random() < UNCOMMON_FEATHER_CHANCE ? RARITY.UNCOMMON : RARITY.COMMON; const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species));
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species) && SPECIES[species].rarity === rarity);
if (speciesToUnlock.length === 0) { if (speciesToUnlock.length === 0) {
// No more species to unlock // No more species to unlock
return; return;
@@ -764,6 +669,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
if (!unlockedHats.includes(hatId)) { if (!unlockedHats.includes(hatId)) {
unlockedHats.push(hatId); unlockedHats.push(hatId);
save(); save();
switchHat(hatId);
const message = makeElement("birb-message-content"); const message = makeElement("birb-message-content");
message.appendChild(document.createTextNode("You've unlocked the ")); message.appendChild(document.createTextNode("You've unlocked the "));
const bold = document.createElement("b"); const bold = document.createElement("b");
@@ -842,23 +748,9 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
removeWardrobe(); removeWardrobe();
const contentContainer = document.createElement("div"); const contentContainer = document.createElement("div");
const familiarBirds = makeElement("birb-grid-content"); const content = makeElement("birb-grid-content");
const uncommonBirds = makeElement("birb-grid-content");
const familiarLabel = document.createElement("div");
familiarLabel.className = "birb-field-guide-section-label";
familiarLabel.textContent = `----- Familiar ${birdBirb()}s -----`;
const uncommonLabel = document.createElement("div");
uncommonLabel.className = "birb-field-guide-section-label";
uncommonLabel.textContent = `----- Uncommon ${birdBirb()}s -----`;
uncommonLabel.title = "Arbitrarily classified birds that are a little harder to find, but worth the wait!";
const description = makeElement("birb-field-guide-description"); const description = makeElement("birb-field-guide-description");
contentContainer.appendChild(familiarLabel); contentContainer.appendChild(content);
contentContainer.appendChild(familiarBirds);
contentContainer.appendChild(uncommonLabel);
contentContainer.appendChild(uncommonBirds);
contentContainer.appendChild(description); contentContainer.appendChild(description);
const fieldGuide = createWindow( const fieldGuide = createWindow(
@@ -874,26 +766,14 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
const boldName = document.createElement("b"); const boldName = document.createElement("b");
boldName.textContent = type.name; boldName.textContent = type.name;
const spacer = document.createElement("div");
const spacerOne = document.createElement("div"); spacer.style.height = "0.3em";
spacerOne.style.height = "0.3em";
const latinName = document.createElement("a");
latinName.className = "birb-field-guide-latin-name";
latinName.textContent = type.latinName;
latinName.href = type.url;
latinName.target = "_blank";
const spacerTwo = document.createElement("div");
spacerTwo.style.height = "0.4em";
const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description); const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.appendChild(boldName); fragment.appendChild(boldName);
fragment.appendChild(spacerOne); fragment.appendChild(spacer);
fragment.appendChild(latinName);
fragment.appendChild(spacerTwo);
fragment.appendChild(descText); fragment.appendChild(descText);
return fragment; return fragment;
@@ -915,11 +795,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
} }
birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags); birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type.colors, type.tags);
speciesElement.appendChild(speciesCanvas); speciesElement.appendChild(speciesCanvas);
let section = familiarBirds; content.appendChild(speciesElement);
if (type.rarity === RARITY.UNCOMMON) {
section = uncommonBirds;
}
section.appendChild(speciesElement);
if (unlocked) { if (unlocked) {
onClick(speciesElement, () => { onClick(speciesElement, () => {
switchSpecies(id); switchSpecies(id);
@@ -1101,6 +977,26 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
return getWindowHeight() - focusedBounds.top; return getWindowHeight() - focusedBounds.top;
} }
/**
* Fly to either an element or the ground
*/
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() {
focusedElement = null;
updateFocusedElementBounds();
flyTo(Math.random() * window.innerWidth, 0);
}
/** /**
* @returns {HTMLElement|null} The random element, or null if no valid element was found * @returns {HTMLElement|null} The random element, or null if no valid element was found
*/ */
@@ -1118,7 +1014,8 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
} }
return true; return true;
}); });
const largeElements = /** @type {HTMLElement[]} */ (Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH)); /** @type {HTMLElement[]} */
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
// Ensure the bird doesn't land on fixed or sticky elements // Ensure the bird doesn't land on fixed or sticky elements
// const fixedAllowed = getContext() instanceof ObsidianContext; // const fixedAllowed = getContext() instanceof ObsidianContext;
// TODO: FIX // TODO: FIX
@@ -1138,21 +1035,20 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
} }
/** /**
* Fly to an element within the viewport * Focus on an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying * @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to fly to was found (null if flying to the ground) * @returns Whether an element to focus on was found
*/ */
function flyToElement(teleport = false) { function focusOnElement(teleport = false) {
if (frozen) { if (frozen) {
return false; return false;
} }
holdRestoredYPosition = false;
const previousElement = focusedElement;
focusedElement = getRandomValidElement(); focusedElement = getRandomValidElement();
log("Focusing on element: ", focusedElement);
updateFocusedElementBounds(); updateFocusedElementBounds();
if (teleport) { if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY()); teleportTo(getFocusedElementRandomX(), getFocusedY());
} else if (focusedElement !== previousElement) { } else {
flyTo(getFocusedElementRandomX(), getFocusedY()); flyTo(getFocusedElementRandomX(), getFocusedY());
} }
return focusedElement !== null; return focusedElement !== null;
@@ -1163,7 +1059,6 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
* @param {number} y * @param {number} y
*/ */
function teleportTo(x, y) { function teleportTo(x, y) {
holdRestoredYPosition = false;
birdX = x; birdX = x;
birdY = y; birdY = y;
setState(States.IDLE); setState(States.IDLE);
@@ -1207,7 +1102,6 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
return; return;
} }
if (currentState === States.IDLE) { if (currentState === States.IDLE) {
holdRestoredYPosition = false;
setState(States.HOP); setState(States.HOP);
birb.setAnimation(Animations.FLYING); birb.setAnimation(Animations.FLYING);
if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) {
@@ -1238,7 +1132,6 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
* @param {number} y * @param {number} y
*/ */
function flyTo(x, y) { function flyTo(x, y) {
holdRestoredYPosition = false;
targetX = x; targetX = x;
targetY = y; targetY = y;
setState(States.FLYING); setState(States.FLYING);
@@ -1272,183 +1165,6 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
return Math.random() < 0.5; return Math.random() < 0.5;
} }
/**
* @param {unknown} value
* @returns {Record<string, SavedBirdPosition>}
*/
function sanitizeSavedBirdPositions(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return {};
}
/** @type {Record<string, SavedBirdPosition>} */
const result = {};
for (const [key, position] of Object.entries(value)) {
if (!position || typeof position !== "object" || Array.isArray(position)) {
continue;
}
// @ts-expect-error
const x = Number(position.x);
// @ts-expect-error
const y = Number(position.y);
// @ts-expect-error
const updatedAt = Number(position.updatedAt ?? 0);
if (!Number.isFinite(x) || !Number.isFinite(y)) {
continue;
}
result[key] = { x, y, updatedAt: Number.isFinite(updatedAt) ? updatedAt : 0 };
}
return result;
}
/**
* @param {string} path
* @returns {string}
*/
function normalizePath(path) {
return path.split("?")[0].split("#")[0];
}
function trimSavedBirdPositions() {
const entries = Object.entries(savedBirdPositions);
if (entries.length <= MAX_SAVED_BIRD_POSITIONS) {
return;
}
entries.sort((a, b) => a[1].updatedAt - b[1].updatedAt);
for (let i = 0; i < entries.length - MAX_SAVED_BIRD_POSITIONS; i++) {
delete savedBirdPositions[entries[i][0]];
}
}
function getBirdPositionScopeKey() {
if (birdSessionKey) {
return birdSessionKey;
}
const existingWindowName = typeof window.name === "string" ? window.name : "";
const markerIndex = existingWindowName.indexOf(TAB_SESSION_MARKER);
if (markerIndex >= 0) {
const end = existingWindowName.indexOf("|", markerIndex);
birdSessionKey = end >= 0
? existingWindowName.slice(markerIndex, end)
: existingWindowName.slice(markerIndex);
return birdSessionKey;
}
const sessionToken = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
birdSessionKey = `${TAB_SESSION_MARKER}${sessionToken}`;
try {
window.name = existingWindowName
? `${existingWindowName}|${birdSessionKey}`
: birdSessionKey;
} catch {
// Ignore if the page blocks changing window.name.
}
return birdSessionKey;
}
/**
* @param {boolean} [force]
*/
function saveBirdPosition(force = false) {
if (!Number.isFinite(birdX) || !Number.isFinite(birdY)) {
return;
}
if (!force && !birdPositionDirty) {
return;
}
const now = Date.now();
const scopeKey = getBirdPositionScopeKey();
const previous = savedBirdPositions[scopeKey];
if (!force && previous) {
const movedX = Math.abs(previous.x - birdX);
const movedY = Math.abs(previous.y - birdY);
if (movedX < BIRD_POSITION_SAVE_MIN_DELTA && movedY < BIRD_POSITION_SAVE_MIN_DELTA) {
birdPositionDirty = false;
return;
}
}
savedBirdPositions[scopeKey] = {
x: birdX,
y: birdY,
updatedAt: now
};
trimSavedBirdPositions();
birdPositionDirty = false;
save();
}
/**
* @returns {boolean}
*/
function restoreBirdPosition() {
const scopeKey = getBirdPositionScopeKey();
const saved = savedBirdPositions[scopeKey];
if (!saved) {
holdRestoredYPosition = false;
return false;
}
const maxX = Math.max(0, window.innerWidth - getCanvasWidth());
const maxY = getWindowHeight() * 1.5;
birdX = Math.min(Math.max(saved.x, 0), maxX);
birdY = Math.min(Math.max(saved.y, 0), maxY);
// Attempt to keep the bird perched if an element still exists near the saved position.
focusedElement = getElementAtPosition(birdX, birdY);
updateFocusedElementBounds();
holdRestoredYPosition = focusedElement === null;
birdPositionDirty = false;
lastTrackedBirdX = birdX;
lastTrackedBirdY = birdY;
setState(States.IDLE);
birb.setX(birdX);
birb.setY(birdY);
return true;
}
/**
* @param {number} x
* @param {number} y
* @returns {HTMLElement|null}
*/
function getElementAtPosition(x, y) {
const desiredTop = getWindowHeight() - y;
let bestElement = null;
let bestScore = Number.POSITIVE_INFINITY;
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
for (const element of elements) {
if (!(element instanceof HTMLElement)) {
continue;
}
if (element.offsetWidth < MIN_FOCUS_ELEMENT_WIDTH) {
continue;
}
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
continue;
}
const xDistance = Math.abs((rect.left + rect.right) / 2 - x);
const yDistance = Math.abs(rect.top - desiredTop);
const score = xDistance + yDistance * 1.5;
if (score < bestScore) {
bestScore = score;
bestElement = element;
}
}
if (bestScore > Math.max(window.innerWidth, getWindowHeight()) * 0.75) {
return null;
}
return bestElement;
}
// Helper functions // Helper functions
/** /**
@@ -1477,3 +1193,60 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
init(); init();
draw(); draw();
} }
/**
* Load the sprite sheet and return the pixel-map template
* @param {string} dataUri
* @param {boolean} [templateColors]
* @returns {Promise<string[][]>}
*/
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;
}
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(PALETTE.TRANSPARENT);
continue;
}
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
if (!templateColors) {
row.push(hex);
continue;
}
if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) {
// Return the color as-is if not found in the map
row.push(hex);
continue;
}
row.push(SPRITE_SHEET_COLOR_MAP[hex]);
}
hexArray.push(row);
}
resolve(hexArray);
};
img.onerror = (err) => {
reject(err);
};
});
}

View File

@@ -3,7 +3,6 @@ import { debug, log, error } from "./shared.js";
export const SAVE_KEY = "birbSaveData"; export const SAVE_KEY = "birbSaveData";
const ROOT_PATH = ""; const ROOT_PATH = "";
const SET_CONTEXT = "__CONTEXT__" const SET_CONTEXT = "__CONTEXT__"
const MONOCRAFT_URL = "__MONOCRAFT_URL__";
/** /**
* @typedef {import('./application.js').BirbSaveData} BirbSaveData * @typedef {import('./application.js').BirbSaveData} BirbSaveData
@@ -93,13 +92,6 @@ export class Context {
areStickyNotesEnabled() { areStickyNotesEnabled() {
return true; return true;
} }
/**
* @returns {string}
*/
getFontStyles() {
return getFontFaceImport(MONOCRAFT_URL);
}
} }
export class LocalContext extends Context { export class LocalContext extends Context {
@@ -202,16 +194,6 @@ export class BrowserExtensionContext extends Context {
// @ts-expect-error // @ts-expect-error
chrome.storage.sync.clear(); chrome.storage.sync.clear();
} }
/**
* @override
* @returns {string}
*/
getFontStyles() {
// Use extension bundled font file
// @ts-expect-error
return getFontFaceImport(chrome.runtime.getURL('fonts/Monocraft.otf'));
}
} }
export class ObsidianContext extends Context { export class ObsidianContext extends Context {
@@ -294,14 +276,6 @@ export class ObsidianContext extends Context {
} }
} }
/**
* @param {string} src
* @returns {string}
*/
function getFontFaceImport(src) {
return `@font-face { font-family: 'Monocraft'; src: url("${src}") format('opentype'); font-weight: normal; font-style: normal; }`;
}
/** /**
* Parse URL parameters into a key-value map * Parse URL parameters into a key-value map
* @param {string} url * @param {string} url

View File

@@ -14,13 +14,11 @@ export class MenuItem {
/** /**
* @param {string|(() => string)} text * @param {string|(() => string)} text
* @param {() => void} action * @param {() => void} action
* @param {number[][]} [icon]
* @param {boolean} [removeMenu] * @param {boolean} [removeMenu]
*/ */
constructor(text, action, icon, removeMenu = true) { constructor(text, action, removeMenu = true) {
this.text = text; this.text = text;
this.action = action; this.action = action;
this.icon = icon;
this.removeMenu = removeMenu; this.removeMenu = removeMenu;
} }
} }
@@ -30,11 +28,10 @@ export class ConditionalMenuItem extends MenuItem {
* @param {string} text * @param {string} text
* @param {() => void} action * @param {() => void} action
* @param {() => boolean} condition * @param {() => boolean} condition
* @param {number[][]} [icon]
* @param {boolean} [removeMenu] * @param {boolean} [removeMenu]
*/ */
constructor(text, action, condition, icon, removeMenu = true) { constructor(text, action, condition, removeMenu = true) {
super(text, action, icon, removeMenu); super(text, action, removeMenu);
this.condition = condition; this.condition = condition;
} }
} }
@@ -45,7 +42,7 @@ export class DebugMenuItem extends ConditionalMenuItem {
* @param {() => void} action * @param {() => void} action
*/ */
constructor(text, action, removeMenu = true) { constructor(text, action, removeMenu = true) {
super(text, action, () => isDebug(), undefined, removeMenu); super(text, action, () => isDebug(), removeMenu);
} }
} }
@@ -60,29 +57,11 @@ export class Separator extends MenuItem {
* @param {() => void} removeMenuCallback * @param {() => void} removeMenuCallback
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
function createMenuItem(item, removeMenuCallback) { function makeMenuItem(item, removeMenuCallback) {
if (item instanceof Separator) { if (item instanceof Separator) {
return makeElement("birb-window-separator"); return makeElement("birb-window-separator");
} }
let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text); let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text);
if (item.icon) {
const iconCanvas = document.createElement("canvas");
iconCanvas.width = 7;
iconCanvas.height = 6;
iconCanvas.classList.add("birb-menu-item-icon");
const ctx = iconCanvas.getContext("2d");
if (ctx) {
for (let row = 0; row < item.icon.length; row++) {
for (let col = 0; col < item.icon[row].length; col++) {
if (item.icon[row][col]) {
ctx.fillStyle = "black";
ctx.fillRect(col, row, 1, 1);
}
}
}
}
menuItem.prepend(iconCanvas);
}
onClick(menuItem, () => { onClick(menuItem, () => {
if (item.removeMenu) { if (item.removeMenu) {
removeMenuCallback(); removeMenuCallback();
@@ -110,7 +89,7 @@ export function insertMenu(menuItems, title, updateLocationCallback) {
const removeCallback = () => removeMenu(); const removeCallback = () => removeMenu();
for (const item of menuItems) { for (const item of menuItems) {
if (!(item instanceof ConditionalMenuItem) || item.condition()) { if (!(item instanceof ConditionalMenuItem) || item.condition()) {
content.appendChild(createMenuItem(item, removeCallback)); content.appendChild(makeMenuItem(item, removeCallback));
} }
} }
menu.appendChild(header); menu.appendChild(header);
@@ -167,7 +146,7 @@ export function switchMenuItems(menuItems, updateLocationCallback) {
const removeCallback = () => removeMenu(); const removeCallback = () => removeMenu();
for (const item of menuItems) { for (const item of menuItems) {
if (!(item instanceof ConditionalMenuItem) || item.condition()) { if (!(item instanceof ConditionalMenuItem) || item.condition()) {
content.appendChild(createMenuItem(item, removeCallback)); content.appendChild(makeMenuItem(item, removeCallback));
} }
} }
updateLocationCallback(menu); updateLocationCallback(menu);

View File

@@ -8,19 +8,16 @@ export class Birdsong {
audioContext; audioContext;
chirp() { chirp() {
const count = Math.floor(1 + Math.random() * 1.5);
for (let i = 0; i < count; i++) {
setTimeout(() => {
if (!this.audioContext) { if (!this.audioContext) {
this.audioContext = new AudioContext(); this.audioContext = new AudioContext();
} }
const TIMES = [0, 0.06, 0.10, 0.15]; const TIMES = [0, 0.06, 0.10, 0.15];
const FREQUENCIES = [2200, const FREQUENCIES = [2200,
3500 + Math.random() * 600 * count, 3500 + Math.random() * 600,
2100 + Math.random() * 200 * count, 2100 + Math.random() * 200,
1600 + Math.random() * 400 * count]; 1600 + Math.random() * 400];
const VOLUMES = [0.00005, 0.165, 0.165, 0.0001]; const VOLUMES = [0.0001, 0.2, 0.2, 0.0001];
const oscillator = this.audioContext.createOscillator(); const oscillator = this.audioContext.createOscillator();
oscillator.type = "sine"; oscillator.type = "sine";
@@ -42,7 +39,5 @@ export class Birdsong {
oscillator.start(now); oscillator.start(now);
oscillator.stop(now + TIMES[TIMES.length - 1]); oscillator.stop(now + TIMES[TIMES.length - 1]);
}, i * 120);
}
} }
} }

View File

@@ -1,430 +0,0 @@
/** @typedef {Object} Species
* @property {string} name
* @property {string} description
* @property {Record<string, string>} colors
* @property {string[]} [tags]
*/
export default {
"bluebird": {
"name": "Eastern Bluebird",
"description": "Native to North American and very social, though can be timid around people.",
"latinName": "Sialia sialis",
"url": "https://en.wikipedia.org/wiki/Eastern_bluebird",
"colors": {
"foot": "#af8e75",
"face": "#639bff",
"belly": "#f8b143",
"underbelly": "#ec8637",
"wing": "#578ae6",
"wing-edge": "#326ed9"
}
},
"shimaEnaga": {
"name": "Shima Enaga",
"description": "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.",
"latinName": "Aegithalos caudatus",
"url": "https://en.wikipedia.org/wiki/Long-tailed_tit",
"colors": {
"foot": "#af8e75",
"face": "#ffffff",
"belly": "#ebe9e8",
"underbelly": "#ebd9d0",
"wing": "#f3d3c1",
"wing-edge": "#2d2d2d",
"theme-highlight": "#d7ac93"
}
},
"tuftedTitmouse": {
"name": "Tufted Titmouse",
"description": "Native to the eastern United States, full of personality, and notably my wife's favorite bird.",
"latinName": "Baeolophus bicolor",
"url": "https://en.wikipedia.org/wiki/Tufted_titmouse",
"colors": {
"foot": "#af8e75",
"face": "#c7cad7",
"belly": "#e4e5eb",
"underbelly": "#d7cfcb",
"wing": "#b1b5c5",
"wing-edge": "#9d9fa9",
"theme-highlight": "#b9abcf"
},
"tags": [
"tuft"
]
},
"europeanRobin": {
"name": "European Robin",
"description": "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.",
"latinName": "Erithacus rubecula",
"url": "https://en.wikipedia.org/wiki/European_robin",
"colors": {
"foot": "#af8e75",
"face": "#ffaf34",
"hood": "#aaa094",
"belly": "#ffaf34",
"underbelly": "#babec2",
"wing": "#aaa094",
"wing-edge": "#888580",
"theme-highlight": "#ffaf34"
}
},
"redCardinal": {
"name": "Red Cardinal",
"description": "Native to the eastern United States, this strikingly red bird is hard to miss.",
"latinName": "Cardinalis cardinalis",
"url": "https://en.wikipedia.org/wiki/Red_cardinal",
"colors": {
"beak": "#d93619",
"foot": "#af8e75",
"face": "#31353d",
"hood": "#e83a1b",
"belly": "#e83a1b",
"underbelly": "#dc3719",
"wing": "#d23215",
"wing-edge": "#b1321c",
"collar": "#e83a1b",
"scruff": "#d23215",
},
"tags": [
"tuft"
]
},
"americanGoldfinch": {
"name": "American Goldfinch",
"description": "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.",
"latinName": "Spinus tristis",
"url": "https://en.wikipedia.org/wiki/American_goldfinch",
"colors": {
"beak": "#ffaf34",
"foot": "#af8e75",
"face": "#fff255",
"nose": "#383838",
"hood": "#383838",
"belly": "#fff255",
"underbelly": "#f5ea63",
"wing": "#e8e079",
"wing-edge": "#191919",
"theme-highlight": "#ffcc00"
}
},
"barnSwallow": {
"name": "Barn Swallow",
"description": "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.",
"latinName": "Hirundo rustica",
"url": "https://en.wikipedia.org/wiki/Barn_swallow",
"colors": {
"foot": "#af8e75",
"face": "#db7c4d",
"belly": "#f7e1c9",
"underbelly": "#ebc9a3",
"wing": "#2252a9",
"wing-edge": "#1c448b",
"hood": "#2252a9"
}
},
"mistletoebird": {
"name": "Mistletoebird",
"description": "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.",
"latinName": "Dicaeum hirundinaceum",
"url": "https://en.wikipedia.org/wiki/Mistletoebird",
"colors": {
"foot": "#6c6a7c",
"face": "#352e6d",
"belly": "#fd6833",
"underbelly": "#e6e1d8",
"wing": "#342b7c",
"wing-edge": "#282065"
}
},
"scarletRobin": {
"name": "Scarlet Robin",
"description": "Native to Australia, this striking robin can be found in Eucalyptus forests.",
"latinName": "Petroica boodang",
"url": "https://en.wikipedia.org/wiki/Scarlet_robin",
"colors": {
"foot": "#494949",
"face": "#3d3d3d",
"belly": "#fc5633",
"underbelly": "#dcdcdc",
"wing": "#2b2b2b",
"wing-edge": "#ebebeb",
"nose": "#ebebeb",
"theme-highlight": "#fc5633"
}
},
"americanRobin": {
"name": "American Robin",
"description": "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.",
"latinName": "Turdus migratorius",
"url": "https://en.wikipedia.org/wiki/American_robin",
"colors": {
"beak": "#e89f30",
"foot": "#9f8075",
"face": "#2d2d2d",
"belly": "#eb7a3a",
"underbelly": "#eb7a3a",
"wing": "#444444",
"wing-edge": "#232323",
"theme-highlight": "#eb7a3a"
}
},
"carolinaWren": {
"name": "Carolina Wren",
"description": "Native to the eastern United States, these little birds are known for their curious and energetic nature.",
"latinName": "Thryothorus ludovicianus",
"url": "https://en.wikipedia.org/wiki/Carolina_wren",
"colors": {
"foot": "#af8e75",
"face": "#edc7a9",
"nose": "#f7eee5",
"hood": "#c58a5b",
"belly": "#e1b796",
"underbelly": "#c79e7c",
"wing": "#c58a5b",
"wing-edge": "#866348"
}
},
"blackCappedChickadee": {
"name": "Black-capped Chickadee",
"description": "Native to North America, these small and curious birds are known for their distinctive call from which they get their name.",
"latinName": "Poecile atricapillus",
"url": "https://en.wikipedia.org/wiki/Black-capped_chickadee",
"colors": {
"hood": "#363636",
"cheek": "#363636",
"eyebrow": "#363636",
"nose": "#363636",
"collar": "#363636",
"belly": "#d6d4cf",
"underbelly": "#cfc5b4",
"face": "#eaeaea",
"wing": "#8f8e9a",
"wing-edge": "#706f7d",
"scruff": "#8f8e9a",
"foot": "#535259"
},
"tags": []
},
"blueJay": {
"name": "Blue Jay",
"description": "This loud and rambunctious bird is native to North America and is known for challenging anything in its path.",
"latinName": "Cyanocitta cristata",
"url": "https://en.wikipedia.org/wiki/Blue_jay",
"colors": {
"foot": "#5a626b",
"face": "#ebf2ff",
"belly": "#e5ecfa",
"underbelly": "#c4cbd6",
"wing": "#5890ff",
"wing-edge": "#3a77e8",
"hood": "#6391e8",
"nose": "#6391e8",
"collar": "#2e3136",
"scruff": "#6391e8"
},
"tags": [
"tuft"
]
},
"darkEyedJunco": {
"name": "Dark-eyed Junco",
"description": "Native across North America, these social birds will often be seen hopping along the ground in winter.",
"latinName": "Junco hyemalis",
"url": "https://en.wikipedia.org/wiki/Dark-eyed_junco",
"colors": {
"face": "#55565e",
"wing": "#5c5f69",
"wing-edge": "#444547",
"belly": "#6c7180",
"underbelly": "#b8bbcc",
"foot": "#87776d",
"beak": "#ab8a98"
}
},
"houseFinch": {
"name": "House Finch",
"description": "Native to North America, these highly social birds sing cheerful songs and are often seen at bird feeders.",
"latinName": "Haemorhous mexicanus",
"url": "https://en.wikipedia.org/wiki/House_finch",
"colors": {
"face": "#cc3a3f",
"wing": "#ae8e78",
"wing-edge": "#8f6c54",
"belly": "#d97c77",
"underbelly": "#c5a489",
"foot": "#705b4c",
"beak": "#cf8479",
"hood": "#b02f35",
"nose": "#ab2b31",
"theme-highlight": "#ef444d"
}
},
"pigeon": {
"name": "Rock Pigeon",
"description": "Descended from the Rock Dove, these once domesticated birds are often found in cities worldwide. Quite friendly and intelligent, they were favored companions of Nikola Tesla.",
"latinName": "Columba livia",
"url": "https://en.wikipedia.org/wiki/Rock_dove",
"colors": {
"foot": "#ef6e5b",
"face": "#5a6c91",
"wing-edge": "#65686e",
"nose": "#ebebeb",
"belly": "#977699",
"underbelly": "#b0b3ba",
"wing": "#c7cbd4"
}
},
"redAvadavat": {
"name": "Red Avadavat",
"description": "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.",
"latinName": "Amandava amandava",
"url": "https://en.wikipedia.org/wiki/Red_avadavat",
"colors": {
"beak": "#f71919",
"foot": "#af7575",
"face": "#cb092b",
"belly": "#ae1724",
"underbelly": "#831b24",
"wing": "#7e3030",
"wing-edge": "#490f0f",
"wing-spots": "#e8e4e4",
},
"rarity": "uncommon"
},
"pinkRobin": {
"name": "Pink Robin",
"description": "Native to Australia, these bubblegum-pink puffballs are quieter than most, instead relying on their vibrant colours to attract partners.",
"latinName": "Petroica rodinogaster",
"url": "https://en.wikipedia.org/wiki/Pink_robin",
"colors": {
"face": "#403a46",
"wing": "#38333d",
"wing-edge": "#252325",
"underbelly": "#ff7eb8",
"belly": "#ff6eaf",
"foot": "#3c393c",
"theme-highlight": "#ff82ba"
},
"rarity": "uncommon"
},
"spangledCotinga": {
"name": "Spangled Cotinga",
"description": "This South American bird can be found in the Amazon rainforest, flashing its iridescent turquoise feathers high above in the canopy.",
"latinName": "Cotinga cayana",
"url": "https://en.wikipedia.org/wiki/Spangled_cotinga",
"colors": {
"face": "#62eafe",
"chin": "#a12457",
"collar": "#a12457",
"belly": "#62eafe",
"underbelly": "#5cd8ea",
"wing": "#227c89",
"wing-edge": "#13353a",
"foot": "#68696b",
"collar-scruff": "#62eafe"
},
"rarity": "uncommon"
},
"elegantEuphonia": {
"name": "Elegant Euphonia",
"description": "This vividly coloured finch is found throughout Central America and is known for the distinctive blue hood that crowns its head.",
"latinName": "Chlorophonia elegantissima",
"url": "https://en.wikipedia.org/wiki/Elegant_euphonia",
"colors": {
"wing": "#2d31a1",
"wing-edge": "#191c6d",
"face": "#1f2392",
"hood": "#6bc6ed",
"nose-tip": "#fd7e1d",
"foot": "#555650",
"belly": "#ff952b",
"underbelly": "#fd7e1d",
"temple": "#57c8fa",
"upper-corner-eye": "#57c8fa",
"upper-eyelid": "#57c8fa",
"collar-scruff": "#57c8fa",
"scruff": "#57c8fa",
"beak": "#252c31",
"collar": "#191c6d"
},
"rarity": "uncommon"
},
"paintedBunting": {
"name": "Painted Bunting",
"description": "A remarkably colourful bird, this North American species is quite difficult to observe despite its vivid palette due to its shy nature and vulnerable habitat.",
"latinName": "Passerina ciris",
"url": "https://en.wikipedia.org/wiki/Painted_bunting",
"colors": {
"face": "#5567f0",
"underbelly": "#f16534",
"belly": "#ef3b3b",
"wing": "#a3e65a",
"wing-edge": "#91cc50",
"shoulder": "#f6fe40",
"foot": "#767980"
},
"rarity": "uncommon"
},
"redWarbler": {
"name": "Red Warbler",
"description": "Endemic to the highlands of Mexico, this bird has the rare distinction of being one of the very few toxic birds in the world.",
"latinName": "Cardellina rubra",
"url": "https://en.wikipedia.org/wiki/Red_warbler",
"colors": {
"face": "#e80a28",
"belly": "#d90921",
"underbelly": "#c70c18",
"wing": "#ba121d",
"wing-edge": "#5b3535",
"foot": "#5e4645",
"behind-eye": "#deedff",
"temple": "#e8f0fa",
"corner-eye": "#d5e4f5",
"lower-eyelid": "#e34a61",
"beak": "#873535",
"cheek": "#db1734"
},
"rarity": "uncommon"
},
"cubanTody": {
"name": "Cuban Tody",
"description": "As the name suggests, this little green bird is only found on the island of Cuba and is known for being particularly round.",
"latinName": "Todus multicolor",
"url": "https://en.wikipedia.org/wiki/Cuban_tody",
"colors": {
"beak": "#f16f54",
"face": "#5ad63e",
"chin": "#e8273b",
"collar": "#f12d3e",
"belly": "#f6f5e4",
"collar-scruff": "#a3ebff",
"underbelly": "#eae9d2",
"wing": "#11c751",
"wing-edge": "#156631",
"foot": "#ac7055",
"scruff": "#11c751",
"theme-highlight": "#4adc67"
},
"rarity": "uncommon"
},
"violetBackedStarling": {
"name": "Violet-backed Starling",
"description": "Native to Sub-Saharan Africa, these small starlings are known for being the most vividly purple birds in the world.",
"latinName": "Cinnyricinclus leucogaster",
"url": "https://en.wikipedia.org/wiki/Violet-backed_starling",
"colors": {
"face": "#9c3af2",
"wing": "#8f37ed",
"wing-edge": "#5b20c2",
"belly": "#ffffff",
"underbelly": "#f2f2f2",
"foot": "#736a66",
"collar": "#b760e6",
"nose": "#7a2ec7",
"cheek": "#7a2ec7",
"nose-tip": "#7a2ec7"
},
"rarity": "uncommon"
}
}

View File

@@ -1,3 +1,10 @@
@font-face {
font-family: 'Monocraft';
src: url("__MONOCRAFT_SRC__") format('opentype');
font-weight: normal;
font-style: normal;
}
:root { :root {
--birb-border-size: 2px; --birb-border-size: 2px;
--birb-neg-border-size: calc(var(--birb-border-size) * -1); --birb-neg-border-size: calc(var(--birb-border-size) * -1);
@@ -211,17 +218,15 @@
font-size: 14px; font-size: 14px;
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
padding-left: 2px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
box-sizing: border-box; box-sizing: border-box;
opacity: 0.7 !important; opacity: 0.7 !important;
user-select: none; user-select: none;
display: flex; display: flex;
justify-content: left; justify-content: space-between;
align-items: center;
cursor: pointer; cursor: pointer;
color: black !important; color: black !important;
transition: background 0.1s, color 0.1s;
} }
.birb-menu-item:hover { .birb-menu-item:hover {
@@ -233,21 +238,6 @@
var(--birb-neg-border-size) 0 var(--birb-highlight), var(--birb-neg-border-size) 0 var(--birb-highlight),
0 var(--birb-neg-border-size) var(--birb-highlight), 0 var(--birb-neg-border-size) var(--birb-highlight),
0 var(--birb-border-size) var(--birb-highlight); 0 var(--birb-border-size) var(--birb-highlight);
transition: none;
}
.birb-menu-item-icon {
width: calc(7 * var(--birb-border-size));
height: calc(6 * var(--birb-border-size));
padding-right: calc(5 * var(--birb-border-size));
flex-shrink: 0;
image-rendering: pixelated;
color: var(--birb-highlight);
opacity: 0.9;
}
.birb-menu-item:hover > .birb-menu-item-icon {
filter: invert(1);
} }
.birb-menu-item-arrow { .birb-menu-item-arrow {
@@ -269,7 +259,7 @@
} }
#birb-field-guide .birb-grid-content { #birb-field-guide .birb-grid-content {
grid-template-columns: repeat(4, auto); grid-template-rows: repeat(3, auto);
} }
#birb-wardrobe .birb-grid-content { #birb-wardrobe .birb-grid-content {
@@ -279,7 +269,7 @@
.birb-grid-content { .birb-grid-content {
display: grid; display: grid;
grid-auto-flow: row; grid-auto-flow: column;
gap: 10px; gap: 10px;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
@@ -298,12 +288,10 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
transition: border-color 0.1s;
} }
.birb-grid-item:hover { .birb-grid-item:hover {
border-color: var(--birb-highlight); border-color: var(--birb-highlight);
transition: none;
} }
.birb-grid-item canvas { .birb-grid-item canvas {
@@ -313,7 +301,7 @@
} }
.birb-grid-item, .birb-field-guide-description, .birb-message-content { .birb-grid-item, .birb-field-guide-description, .birb-message-content {
border: var(--birb-border-size) solid #ffcf90; border: var(--birb-border-size) solid rgb(255, 207, 144);
box-shadow: 0 0 0 var(--birb-border-size) white; box-shadow: 0 0 0 var(--birb-border-size) white;
background: rgba(255, 221, 177, 0.5); background: rgba(255, 221, 177, 0.5);
} }
@@ -332,15 +320,6 @@
background: var(--birb-mix-color); background: var(--birb-mix-color);
} }
.birb-field-guide-section-label {
padding-top: 4px;
/* padding-left: calc(10px + var(--birb-border-size) / 2); */
color: #876c4e;
text-align: center;
/* Italics */
font-style: italic;
}
.birb-field-guide-description { .birb-field-guide-description {
max-width: calc(100% - 20px); max-width: calc(100% - 20px);
margin-left: 10px; margin-left: 10px;
@@ -352,14 +331,7 @@
margin-bottom: 10px; margin-bottom: 10px;
font-size: 14px; font-size: 14px;
box-sizing: border-box; box-sizing: border-box;
color: #7c6c4b; color: rgb(124, 108, 75);
}
.birb-field-guide-latin-name {
text-decoration: underline;
font-style: italic;
font-weight: bold;
color: inherit;
} }
#birb-feather { #birb-feather {
@@ -372,7 +344,7 @@
width: 100%; width: 100%;
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
color: #7c6c4b; color: rgb(124, 108, 75);
} }
.birb-sticky-note { .birb-sticky-note {