Compare commits
70 Commits
soft-outli
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5797c055ed | ||
|
|
96ff61625a | ||
|
|
85ade65a57 | ||
|
|
2f3d7958ea | ||
|
|
eab6086f4d | ||
|
|
c770e3d1f6 | ||
|
|
0d007f8c1e | ||
|
|
92c083138d | ||
|
|
5d6ea50c87 | ||
|
|
77a29c549f | ||
|
|
a8ba15489f | ||
|
|
39e84be775 | ||
|
|
f924343ac3 | ||
|
|
fd55924025 | ||
|
|
f891f8f06d | ||
|
|
9aee4eab1a | ||
|
|
763b50f34b | ||
|
|
b4c577a0ac | ||
|
|
5a3b555d3a | ||
|
|
74a776cd4f | ||
|
|
ebb9f92be2 | ||
|
|
b7d6ca63c1 | ||
|
|
7d16459a76 | ||
|
|
abe4439d5e | ||
|
|
6f88d386ec | ||
|
|
86a14d6dca | ||
|
|
30b9c86cca | ||
|
|
3765713fd0 | ||
|
|
0f90eb4492 | ||
|
|
30d6c2fee5 | ||
|
|
18fa5e8683 | ||
|
|
c43dd4c7b4 | ||
|
|
9e6f5feae1 | ||
|
|
b6e93088a8 | ||
|
|
b440f633a5 | ||
|
|
7b20a376ce | ||
|
|
61fbe89986 | ||
|
|
c1511aae71 | ||
|
|
0a11ebe87d | ||
|
|
5cf96da868 | ||
|
|
5d99142b74 | ||
|
|
2a7ad229be | ||
|
|
c880b99744 | ||
|
|
fe0310cb36 | ||
|
|
efddf12ba5 | ||
|
|
7aa9996857 | ||
|
|
7f334d789f | ||
|
|
a57615b3da | ||
|
|
37a8b6cc6e | ||
|
|
31a3f7cac9 | ||
|
|
9fb0ab3f3f | ||
|
|
736d01e015 | ||
|
|
dd3ef01bef | ||
|
|
3e48360632 | ||
|
|
3eda5ffc92 | ||
|
|
6cfd32270c | ||
|
|
1d4c1a000e | ||
|
|
71b74c9b6f | ||
|
|
80bcf60a07 | ||
|
|
a2dea8a17d | ||
|
|
fd09a35b51 | ||
|
|
11ea3c012b | ||
|
|
1bf82dfbad | ||
|
|
b04edbc2c5 | ||
|
|
927b287f98 | ||
|
|
45743d2caf | ||
|
|
953d2cde47 | ||
|
|
6309aed971 | ||
|
|
ea85c61955 | ||
|
|
cd06a886bd |
5
.gitignore
vendored
@@ -3,3 +3,8 @@
|
|||||||
/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
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ 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!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 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
|
||||||
@@ -95,7 +97,8 @@ 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.dev](https://binarydigit.dev)
|
- [https://binarydigit.net](https://binarydigit.net)
|
||||||
|
- [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!*
|
||||||
|
|
||||||
|
|||||||
24
build.js
@@ -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_SRC_KEY = "__MONOCRAFT_SRC__";
|
const MONOCRAFT_URL_KEY = "__MONOCRAFT_URL__";
|
||||||
const CODE_KEY = "__CODE__";
|
const CODE_KEY = "__CODE__";
|
||||||
|
|
||||||
const spriteSheets = [
|
const spriteSheets = [
|
||||||
@@ -85,7 +85,9 @@ writeFileSync(BUILD_CACHE_PATH, JSON.stringify(buildCache), 'utf8');
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} entryPoint
|
* @param {string} entryPoint
|
||||||
* @param {boolean} [embedFont]
|
* @param {boolean} [embedFont] When true, the Monocraft font is base64-encoded
|
||||||
|
* 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) {
|
||||||
@@ -109,6 +111,15 @@ 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');
|
||||||
@@ -119,14 +130,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +190,7 @@ 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
1394
dist/extension/birb.js
vendored
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
dist/extension/images/icons/transparent/16x16x1.png
vendored
|
Before Width: | Height: | Size: 635 B After Width: | Height: | Size: 662 B |
BIN
dist/extension/images/icons/transparent/16x16x2.png
vendored
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
BIN
dist/extension/images/icons/transparent/27x20x2.png
vendored
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 881 B |
BIN
dist/extension/images/icons/transparent/27x20x3.png
vendored
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 996 B |
BIN
dist/extension/images/icons/transparent/29x29x2.png
vendored
|
Before Width: | Height: | Size: 914 B After Width: | Height: | Size: 933 B |
BIN
dist/extension/images/icons/transparent/29x29x3.png
vendored
|
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1.0 KiB |
BIN
dist/extension/images/icons/transparent/32x24x2.png
vendored
|
Before Width: | Height: | Size: 881 B After Width: | Height: | Size: 933 B |
BIN
dist/extension/images/icons/transparent/32x24x3.png
vendored
|
Before Width: | Height: | Size: 953 B After Width: | Height: | Size: 1008 B |
BIN
dist/extension/images/icons/transparent/32x32x1.png
vendored
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 848 B |
BIN
dist/extension/images/icons/transparent/32x32x2.png
vendored
|
Before Width: | Height: | Size: 936 B After Width: | Height: | Size: 957 B |
BIN
dist/extension/images/icons/transparent/48x48x1.png
vendored
|
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 866 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
BIN
dist/extension/images/icons/transparent/60x45x2.png
vendored
|
Before Width: | Height: | Size: 1014 B After Width: | Height: | Size: 1.0 KiB |
BIN
dist/extension/images/icons/transparent/60x45x3.png
vendored
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
dist/extension/images/icons/transparent/67x50x2.png
vendored
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
dist/extension/images/icons/transparent/74x55x2.png
vendored
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
dist/extension/images/icons/transparent/96x96x1.png
vendored
|
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
2
dist/extension/manifest.json
vendored
@@ -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.1.24",
|
"version": "2026.4.6",
|
||||||
"homepage_url": "https://idreesinc.com",
|
"homepage_url": "https://idreesinc.com",
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "images/icons/transparent/48x48x1.png",
|
"48": "images/icons/transparent/48x48x1.png",
|
||||||
|
|||||||
1386
dist/obsidian/main.js
vendored
2
dist/obsidian/manifest.json
vendored
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "pocket-bird",
|
"id": "pocket-bird",
|
||||||
"name": "Pocket Bird",
|
"name": "Pocket Bird",
|
||||||
"version": "2026.1.24",
|
"version": "2026.4.6",
|
||||||
"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",
|
||||||
|
|||||||
1386
dist/userscript/birb.user.js
vendored
1384
dist/web/birb.embed.js
vendored
1384
dist/web/birb.js
vendored
380
editor/editor.js
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
// @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();
|
||||||
|
})();
|
||||||
24
editor/index.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!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>
|
||||||
143
editor/stylesheet.css
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 635 B After Width: | Height: | Size: 662 B |
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 881 B |
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 996 B |
|
Before Width: | Height: | Size: 914 B After Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 881 B After Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 953 B After Width: | Height: | Size: 1008 B |
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 936 B After Width: | Height: | Size: 957 B |
|
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 866 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 1014 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
BIN
sprites/birb.png
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
sprites/hats.png
|
Before Width: | Height: | Size: 939 B After Width: | Height: | Size: 949 B |
@@ -1,10 +1,6 @@
|
|||||||
import { TAG } from "./layer.js";
|
import species from "../species.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",
|
||||||
@@ -14,20 +10,37 @@ export const PALETTE = {
|
|||||||
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, string>}
|
* @type {Record<string, PaletteColor>}
|
||||||
*/
|
*/
|
||||||
export const SPRITE_SHEET_COLOR_MAP = {
|
export const SPRITE_SHEET_COLOR_MAP = {
|
||||||
"transparent": PALETTE.TRANSPARENT,
|
"transparent": PALETTE.TRANSPARENT,
|
||||||
@@ -39,10 +52,25 @@ 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,
|
||||||
@@ -50,16 +78,52 @@ 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, colors, tags = []) {
|
constructor(name, description, latinName, url, colors, tags = [], rarity = RARITY.COMMON) {
|
||||||
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",
|
||||||
@@ -71,139 +135,90 @@ 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 = {
|
export const SPECIES = Object.fromEntries(
|
||||||
bluebird: new BirdType("Eastern Bluebird",
|
Object.entries(species).map(([id, data]) => [
|
||||||
"Native to North American and very social, though can be timid around people.", {
|
id,
|
||||||
[PALETTE.FOOT]: "#af8e75",
|
new BirdType(data.name, data.description, data.latinName, data.url, data.colors, data.tags, data.rarity)
|
||||||
[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",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@@ -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, ObsidianContext } from './context.js';
|
import { Context } from './context.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getContext,
|
getContext,
|
||||||
@@ -24,8 +24,9 @@ import {
|
|||||||
} from './shared.js';
|
} from './shared.js';
|
||||||
import {
|
import {
|
||||||
PALETTE,
|
PALETTE,
|
||||||
SPRITE_SHEET_COLOR_MAP,
|
SPECIES,
|
||||||
SPECIES
|
RARITY,
|
||||||
|
loadSpriteSheetPixels,
|
||||||
} from './animation/sprites.js';
|
} from './animation/sprites.js';
|
||||||
import {
|
import {
|
||||||
StickyNote,
|
StickyNote,
|
||||||
@@ -50,6 +51,13 @@ 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
|
||||||
@@ -58,6 +66,7 @@ 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]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,18 +118,24 @@ 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 HAT_CHANCE = 1 / (60 * 60 * 10); // Every 10 minutes
|
const UNCOMMON_FEATHER_CHANCE = 0.15; // 15% of feathers are uncommon
|
||||||
|
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;
|
const PET_FEATHER_BOOST = 2; // Multiplier for feather effect
|
||||||
const PET_HAT_BOOST = 1.5;
|
const PET_HAT_BOOST = 1.5; // Multiplier for hat effect
|
||||||
|
|
||||||
// Focus element constraints
|
// Focus element constraints
|
||||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
const MIN_FOCUS_ELEMENT_WIDTH = 100; // Minimum width (in px) for an element to be considered a valid perch target
|
||||||
|
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 = {};
|
||||||
@@ -167,12 +182,47 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
new MenuItem(() => `Pet ${birdBirb()}`, pet, [
|
||||||
new MenuItem("Field Guide", insertFieldGuide),
|
[0, 1, 1, 0, 1, 1, 0],
|
||||||
new MenuItem("Wardrobe", insertWardrobe),
|
[1, 0, 0, 1, 0, 0, 1],
|
||||||
new ConditionalMenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote), () => getContext().areStickyNotesEnabled()),
|
[1, 0, 0, 0, 0, 0, 1],
|
||||||
new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)),
|
[0, 1, 0, 0, 0, 1, 0],
|
||||||
new DebugMenuItem("Freeze/Unfreeze", () => {
|
[0, 0, 1, 0, 1, 0, 0],
|
||||||
|
[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),
|
||||||
@@ -191,11 +241,18 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
|||||||
setDebug(false);
|
setDebug(false);
|
||||||
}),
|
}),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), false),
|
new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), [
|
||||||
|
[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), false),
|
new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), undefined, 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;
|
||||||
@@ -215,11 +272,9 @@ 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("__VERSION__", () => { alert("Thank you for using Pocket Bird! You are on version: __VERSION__") }, false),
|
new MenuItem("Build __VERSION__", () => { alert("Thank you for using Pocket Bird! You are on version: __VERSION__") }, undefined, false),
|
||||||
];
|
];
|
||||||
|
|
||||||
const styleElement = document.createElement("style");
|
|
||||||
|
|
||||||
/** @type {Birb} */
|
/** @type {Birb} */
|
||||||
let birb;
|
let birb;
|
||||||
|
|
||||||
@@ -256,6 +311,13 @@ 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 = [];
|
||||||
|
|
||||||
@@ -274,6 +336,7 @@ 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) {
|
||||||
@@ -308,6 +371,9 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -345,8 +411,8 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onLoad() {
|
function onLoad() {
|
||||||
styleElement.textContent = STYLESHEET;
|
injectStyleElement(getContext().getFontStyles());
|
||||||
document.head.appendChild(styleElement);
|
injectStyleElement(STYLESHEET);
|
||||||
|
|
||||||
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);
|
||||||
@@ -395,19 +461,26 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
|||||||
|
|
||||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||||
|
|
||||||
let lastPath = getContext().getPath().split("?")[0];
|
let lastPath = normalizePath(getContext().getPath());
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const currentPath = getContext().getPath().split("?")[0];
|
const currentPath = normalizePath(getContext().getPath());
|
||||||
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));
|
||||||
|
|
||||||
focusOnElement(true);
|
if (!restoreBirdPosition()) {
|
||||||
|
flyToElement(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
@@ -426,11 +499,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
|
||||||
focusOnElement();
|
flyToElement();
|
||||||
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
|
||||||
focusOnElement();
|
flyToElement();
|
||||||
lastActionTimestamp = Date.now();
|
lastActionTimestamp = Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,9 +539,11 @@ 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()) {
|
||||||
flySomewhere();
|
flyToElement();
|
||||||
|
}
|
||||||
|
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)) {
|
||||||
@@ -482,7 +557,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
|
||||||
flySomewhere();
|
flyToElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (birb.draw(SPECIES[currentSpecies], currentHat)) {
|
if (birb.draw(SPECIES[currentSpecies], currentHat)) {
|
||||||
@@ -498,6 +573,25 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -551,7 +645,8 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
|||||||
if (document.querySelector("#" + FEATHER_ID)) {
|
if (document.querySelector("#" + FEATHER_ID)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species));
|
const rarity = Math.random() < UNCOMMON_FEATHER_CHANCE ? RARITY.UNCOMMON : RARITY.COMMON;
|
||||||
|
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;
|
||||||
@@ -669,7 +764,6 @@ 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");
|
||||||
@@ -748,9 +842,23 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
|||||||
removeWardrobe();
|
removeWardrobe();
|
||||||
|
|
||||||
const contentContainer = document.createElement("div");
|
const contentContainer = document.createElement("div");
|
||||||
const content = makeElement("birb-grid-content");
|
const familiarBirds = 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(content);
|
contentContainer.appendChild(familiarLabel);
|
||||||
|
contentContainer.appendChild(familiarBirds);
|
||||||
|
contentContainer.appendChild(uncommonLabel);
|
||||||
|
contentContainer.appendChild(uncommonBirds);
|
||||||
contentContainer.appendChild(description);
|
contentContainer.appendChild(description);
|
||||||
|
|
||||||
const fieldGuide = createWindow(
|
const fieldGuide = createWindow(
|
||||||
@@ -766,14 +874,26 @@ 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");
|
|
||||||
spacer.style.height = "0.3em";
|
const spacerOne = document.createElement("div");
|
||||||
|
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(spacer);
|
fragment.appendChild(spacerOne);
|
||||||
|
fragment.appendChild(latinName);
|
||||||
|
fragment.appendChild(spacerTwo);
|
||||||
fragment.appendChild(descText);
|
fragment.appendChild(descText);
|
||||||
|
|
||||||
return fragment;
|
return fragment;
|
||||||
@@ -795,7 +915,11 @@ 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);
|
||||||
content.appendChild(speciesElement);
|
let section = familiarBirds;
|
||||||
|
if (type.rarity === RARITY.UNCOMMON) {
|
||||||
|
section = uncommonBirds;
|
||||||
|
}
|
||||||
|
section.appendChild(speciesElement);
|
||||||
if (unlocked) {
|
if (unlocked) {
|
||||||
onClick(speciesElement, () => {
|
onClick(speciesElement, () => {
|
||||||
switchSpecies(id);
|
switchSpecies(id);
|
||||||
@@ -977,26 +1101,6 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -1014,8 +1118,7 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
/** @type {HTMLElement[]} */
|
const largeElements = /** @type {HTMLElement[]} */ (Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH));
|
||||||
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
|
||||||
@@ -1035,20 +1138,21 @@ function startApplication(birbPixels, featherPixels, hatsPixels) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Focus on an element within the viewport
|
* Fly to 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 focus on was found
|
* @returns Whether an element to fly to was found (null if flying to the ground)
|
||||||
*/
|
*/
|
||||||
function focusOnElement(teleport = false) {
|
function flyToElement(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 {
|
} else if (focusedElement !== previousElement) {
|
||||||
flyTo(getFocusedElementRandomX(), getFocusedY());
|
flyTo(getFocusedElementRandomX(), getFocusedY());
|
||||||
}
|
}
|
||||||
return focusedElement !== null;
|
return focusedElement !== null;
|
||||||
@@ -1059,6 +1163,7 @@ 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);
|
||||||
@@ -1102,6 +1207,7 @@ 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) {
|
||||||
@@ -1132,6 +1238,7 @@ 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);
|
||||||
@@ -1165,6 +1272,183 @@ 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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1193,60 +1477,3 @@ 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);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ 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
|
||||||
@@ -92,6 +93,13 @@ 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 {
|
||||||
@@ -194,6 +202,16 @@ 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 {
|
||||||
@@ -276,6 +294,14 @@ 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
|
||||||
|
|||||||
35
src/menu.js
@@ -14,11 +14,13 @@ 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, removeMenu = true) {
|
constructor(text, action, icon, removeMenu = true) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
|
this.icon = icon;
|
||||||
this.removeMenu = removeMenu;
|
this.removeMenu = removeMenu;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,10 +30,11 @@ 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, removeMenu = true) {
|
constructor(text, action, condition, icon, removeMenu = true) {
|
||||||
super(text, action, removeMenu);
|
super(text, action, icon, removeMenu);
|
||||||
this.condition = condition;
|
this.condition = condition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +45,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(), removeMenu);
|
super(text, action, () => isDebug(), undefined, removeMenu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +60,29 @@ export class Separator extends MenuItem {
|
|||||||
* @param {() => void} removeMenuCallback
|
* @param {() => void} removeMenuCallback
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
function makeMenuItem(item, removeMenuCallback) {
|
function createMenuItem(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();
|
||||||
@@ -89,7 +110,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(makeMenuItem(item, removeCallback));
|
content.appendChild(createMenuItem(item, removeCallback));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
menu.appendChild(header);
|
menu.appendChild(header);
|
||||||
@@ -146,7 +167,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(makeMenuItem(item, removeCallback));
|
content.appendChild(createMenuItem(item, removeCallback));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateLocationCallback(menu);
|
updateLocationCallback(menu);
|
||||||
|
|||||||
65
src/sound.js
@@ -8,36 +8,41 @@ export class Birdsong {
|
|||||||
audioContext;
|
audioContext;
|
||||||
|
|
||||||
chirp() {
|
chirp() {
|
||||||
if (!this.audioContext) {
|
const count = Math.floor(1 + Math.random() * 1.5);
|
||||||
this.audioContext = new AudioContext();
|
for (let i = 0; i < count; i++) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.audioContext) {
|
||||||
|
this.audioContext = new AudioContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMES = [0, 0.06, 0.10, 0.15];
|
||||||
|
const FREQUENCIES = [2200,
|
||||||
|
3500 + Math.random() * 600 * count,
|
||||||
|
2100 + Math.random() * 200 * count,
|
||||||
|
1600 + Math.random() * 400 * count];
|
||||||
|
const VOLUMES = [0.00005, 0.165, 0.165, 0.0001];
|
||||||
|
|
||||||
|
const oscillator = this.audioContext.createOscillator();
|
||||||
|
oscillator.type = "sine";
|
||||||
|
const gain = this.audioContext.createGain();
|
||||||
|
oscillator.connect(gain);
|
||||||
|
gain.connect(this.audioContext.destination);
|
||||||
|
|
||||||
|
const now = this.audioContext.currentTime;
|
||||||
|
for (let i = 0; i < TIMES.length; i++) {
|
||||||
|
const time = TIMES[i] + now;
|
||||||
|
if (i === 0) {
|
||||||
|
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
|
||||||
|
gain.gain.setValueAtTime(VOLUMES[i], time);
|
||||||
|
} else {
|
||||||
|
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oscillator.start(now);
|
||||||
|
oscillator.stop(now + TIMES[TIMES.length - 1]);
|
||||||
|
}, i * 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIMES = [0, 0.06, 0.10, 0.15];
|
|
||||||
const FREQUENCIES = [2200,
|
|
||||||
3500 + Math.random() * 600,
|
|
||||||
2100 + Math.random() * 200,
|
|
||||||
1600 + Math.random() * 400];
|
|
||||||
const VOLUMES = [0.0001, 0.2, 0.2, 0.0001];
|
|
||||||
|
|
||||||
const oscillator = this.audioContext.createOscillator();
|
|
||||||
oscillator.type = "sine";
|
|
||||||
const gain = this.audioContext.createGain();
|
|
||||||
oscillator.connect(gain);
|
|
||||||
gain.connect(this.audioContext.destination);
|
|
||||||
|
|
||||||
const now = this.audioContext.currentTime;
|
|
||||||
for (let i = 0; i < TIMES.length; i++) {
|
|
||||||
const time = TIMES[i] + now;
|
|
||||||
if (i === 0) {
|
|
||||||
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
|
|
||||||
gain.gain.setValueAtTime(VOLUMES[i], time);
|
|
||||||
} else {
|
|
||||||
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
|
|
||||||
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
oscillator.start(now);
|
|
||||||
oscillator.stop(now + TIMES[TIMES.length - 1]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
430
src/species.js
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
/** @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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
@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);
|
||||||
@@ -218,15 +211,17 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
padding-left: 10px;
|
padding-left: 2px;
|
||||||
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: space-between;
|
justify-content: left;
|
||||||
|
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 {
|
||||||
@@ -238,6 +233,21 @@
|
|||||||
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 {
|
||||||
@@ -259,7 +269,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#birb-field-guide .birb-grid-content {
|
#birb-field-guide .birb-grid-content {
|
||||||
grid-template-rows: repeat(3, auto);
|
grid-template-columns: repeat(4, auto);
|
||||||
}
|
}
|
||||||
|
|
||||||
#birb-wardrobe .birb-grid-content {
|
#birb-wardrobe .birb-grid-content {
|
||||||
@@ -269,7 +279,7 @@
|
|||||||
|
|
||||||
.birb-grid-content {
|
.birb-grid-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: row;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
@@ -288,10 +298,12 @@
|
|||||||
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 {
|
||||||
@@ -301,7 +313,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 rgb(255, 207, 144);
|
border: var(--birb-border-size) solid #ffcf90;
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -320,6 +332,15 @@
|
|||||||
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;
|
||||||
@@ -331,7 +352,14 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: rgb(124, 108, 75);
|
color: #7c6c4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birb-field-guide-latin-name {
|
||||||
|
text-decoration: underline;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: bold;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#birb-feather {
|
#birb-feather {
|
||||||
@@ -344,7 +372,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: rgb(124, 108, 75);
|
color: #7c6c4b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.birb-sticky-note {
|
.birb-sticky-note {
|
||||||
|
|||||||