128 Commits

Author SHA1 Message Date
√(noham)²
5797c055ed Persist and restore bird position across sessions
Add saving and restoring of bird positions so the bird can persist its location between navigations and reloads.

Introduces SavedBirdPosition and birdPositions in save data, constants for save intervals and thresholds, and tracking of position deltas to mark dirty state. Implements sanitizeSavedBirdPositions, normalizePath, trimming logic, and a per-tab session key (using window.name) to avoid cross-tab restores. Saves periodically and on pagehide/beforeunload, restores on startup (with heuristics to snap to a nearby perchable element), and avoids overwriting restored Y when appropriate.
2026-04-06 12:55:19 +02:00
Idrees Hassan
96ff61625a Fix bird/birb in menu items 2026-04-04 10:13:17 -07:00
Idrees Hassan
85ade65a57 Update colors and add icons 2026-04-03 19:06:28 -07:00
Idrees Hassan
2f3d7958ea Update application.js 2026-03-30 06:36:33 -07:00
Idrees
eab6086f4d Merge pull request #9 from IdreesInc/new-birds
New birds and new logic!
2026-03-29 14:45:03 -07:00
Idrees Hassan
c770e3d1f6 Update icon to new bird design 2026-03-29 14:43:21 -07:00
Idrees Hassan
0d007f8c1e Update highlight color 2026-03-29 14:26:26 -07:00
Idrees Hassan
92c083138d Add the violet backed starling 2026-03-29 14:24:03 -07:00
Idrees Hassan
5d6ea50c87 Add the cuban tody 2026-03-29 12:26:50 -07:00
Idrees Hassan
77a29c549f Slightly increase uncommon feather chance 2026-03-29 11:51:21 -07:00
Idrees Hassan
a8ba15489f Add the spangled cotinga 2026-03-29 11:47:01 -07:00
Idrees Hassan
39e84be775 Add chin to sprite 2026-03-29 11:36:43 -07:00
Idrees Hassan
f924343ac3 Add the painted bunting 2026-03-29 11:19:45 -07:00
Idrees Hassan
fd55924025 Add shoulder to sprite 2026-03-29 11:13:36 -07:00
Idrees Hassan
f891f8f06d Update euphonia colors 2026-03-28 15:38:43 -07:00
Idrees Hassan
9aee4eab1a Remove shoulder 2026-03-28 14:36:30 -07:00
Idrees Hassan
763b50f34b Change pigeon name 2026-03-28 12:15:53 -07:00
Idrees Hassan
b4c577a0ac Fix collar scruff 2026-03-28 12:12:06 -07:00
Idrees Hassan
5a3b555d3a Fix flapping sprite 2026-03-28 12:07:36 -07:00
Idrees Hassan
74a776cd4f Add latin names to the birds 2026-03-28 11:48:02 -07:00
Idrees Hassan
ebb9f92be2 Fix chickadee color overrides 2026-03-28 11:34:42 -07:00
Idrees Hassan
b7d6ca63c1 Enforce enum types 2026-03-28 11:29:35 -07:00
Idrees Hassan
7d16459a76 Implement rarity calculations 2026-03-28 11:26:39 -07:00
Idrees Hassan
abe4439d5e Add spots to the strawberry finch 2026-03-28 11:15:24 -07:00
Idrees Hassan
6f88d386ec Add the pigeon 2026-03-28 11:07:32 -07:00
Idrees Hassan
86a14d6dca Add the elegant euphonia 2026-03-28 10:52:08 -07:00
Idrees Hassan
30b9c86cca Remove color override redundency 2026-03-28 10:38:19 -07:00
Idrees Hassan
3765713fd0 Fix sprite color overrides 2026-03-28 10:31:23 -07:00
Idrees Hassan
0f90eb4492 Add more sprite template colors 2026-03-28 10:27:26 -07:00
Idrees Hassan
30d6c2fee5 Update house finch colors 2026-03-28 09:48:22 -07:00
Idrees Hassan
18fa5e8683 Lighten pink robin 2026-03-28 09:44:29 -07:00
Idrees Hassan
c43dd4c7b4 Add the red warbler 2026-03-21 16:36:27 -07:00
Idrees Hassan
9e6f5feae1 Fix template colors 2026-03-21 14:44:27 -07:00
Idrees Hassan
b6e93088a8 Add more eye template colors 2026-03-21 14:37:15 -07:00
Idrees Hassan
b440f633a5 Add nose tip 2026-03-21 14:25:41 -07:00
Idrees Hassan
7b20a376ce Reduce pink robin wing edge contrast 2026-03-21 14:12:31 -07:00
Idrees Hassan
61fbe89986 Prevent flying to the same element and reduce chirp volume 2026-03-21 14:11:58 -07:00
Idrees Hassan
c1511aae71 Update pink robin 2026-03-20 17:15:08 -07:00
Idrees Hassan
0a11ebe87d Update pink robin colors 2026-03-18 17:16:08 -07:00
Idrees Hassan
5cf96da868 Update house finch theme highlight 2026-03-18 17:12:57 -07:00
Idrees Hassan
5d99142b74 Add pink robin and update scarlet robin 2026-03-18 17:11:05 -07:00
Idrees Hassan
2a7ad229be Add support for different rarities in field guide 2026-03-18 16:48:13 -07:00
Idrees Hassan
c880b99744 Add house finch 2026-03-18 15:52:16 -07:00
Idrees Hassan
fe0310cb36 Add dark eyed junco 2026-03-11 20:50:16 -07:00
Idrees Hassan
efddf12ba5 Add potential for multiple cheeps 2026-03-11 18:47:38 -07:00
Idrees Hassan
7aa9996857 Add wing edge to blue jay 2026-03-11 18:35:34 -07:00
Idrees Hassan
7f334d789f Add blue jay 2026-03-11 18:28:37 -07:00
Idrees Hassan
a57615b3da Add black-capped chickadee 2026-03-11 17:07:50 -07:00
Idrees Hassan
37a8b6cc6e Merge branch 'main' into new-birds 2026-03-11 16:57:53 -07:00
Idrees Hassan
31a3f7cac9 Update .gitignore 2026-03-11 16:57:48 -07:00
Idrees Hassan
9fb0ab3f3f Revert carolina wren colors 2026-03-11 16:56:52 -07:00
Idrees Hassan
736d01e015 Add scruff 2026-03-11 16:51:11 -07:00
Idrees Hassan
dd3ef01bef Actually fix colors not updating on refresh 2026-03-11 16:39:09 -07:00
Idrees Hassan
3e48360632 Fix colors not updating on refresh 2026-03-11 16:36:24 -07:00
Idrees Hassan
3eda5ffc92 Store changes in local storage 2026-03-11 16:33:17 -07:00
Idrees Hassan
6cfd32270c Fix sprite flag inconsistencies 2026-03-11 16:22:00 -07:00
Idrees Hassan
1d4c1a000e Fix collar 2026-03-11 16:14:12 -07:00
Idrees Hassan
71b74c9b6f Add history for species changes 2026-03-11 16:09:36 -07:00
Idrees Hassan
80bcf60a07 Add other palette items and toggles 2026-03-11 15:46:38 -07:00
Idrees Hassan
a2dea8a17d Add editor to this repository 2026-03-11 15:24:55 -07:00
Idrees Hassan
fd09a35b51 Decrease hat chance default 2026-03-08 17:25:34 -07:00
Idrees Hassan
11ea3c012b Merge branch 'main' of https://github.com/IdreesInc/Pocket-Bird 2026-03-08 17:25:18 -07:00
Idrees Hassan
1bf82dfbad Use species json 2026-03-08 17:25:17 -07:00
Idrees
b04edbc2c5 Update README.md 2026-03-08 12:53:31 -07:00
Idrees
927b287f98 Update README.md 2026-03-08 12:51:29 -07:00
Idrees Hassan
45743d2caf Update font handling to better bundle fonts 2026-03-08 12:47:08 -07:00
Idrees
953d2cde47 Add gif to README 2026-02-03 12:45:03 -08:00
Idrees Hassan
6309aed971 Create new build for release 2026-01-25 14:04:37 -05:00
Idrees Hassan
ea85c61955 Don't switch hats when unlocked 2026-01-24 22:55:46 -05:00
Idrees Hassan
cd06a886bd Update viking helmet design 2026-01-24 22:50:20 -05:00
Idrees Hassan
868cd06210 Update wizard hat design 2026-01-24 22:30:59 -05:00
Idrees Hassan
307d4a8895 Update fez design 2026-01-24 22:28:58 -05:00
Idrees Hassan
5a33cef4d5 Move hats around 2026-01-24 21:31:36 -05:00
Idrees Hassan
7b4ebf7ab8 Add description to cordovan hat 2026-01-24 21:18:49 -05:00
Idrees Hassan
e393013b27 Make top hat taller 2026-01-24 18:34:56 -05:00
Idrees Hassan
db1a3dcbb6 Add more hats 2026-01-24 18:27:47 -05:00
Idrees
8cd93bb623 Merge pull request #7 from IdreesInc/hat
Add hats!
2026-01-22 18:51:02 -05:00
Idrees Hassan
a3a09c6819 Add source code link to menu 2026-01-22 18:50:43 -05:00
Idrees Hassan
912327a348 Spawn hats based on time again 2026-01-22 00:01:46 -05:00
Idrees Hassan
d54f208cc4 Revert "Remove hat pet boost because it doesn't work on page load"
This reverts commit cb1f2f605f.
2026-01-21 23:36:54 -05:00
Idrees Hassan
cb1f2f605f Remove hat pet boost because it doesn't work on page load 2026-01-21 23:36:31 -05:00
Idrees Hassan
2ee6ea84a7 Move pet boost indicator to settings menu 2026-01-21 23:27:33 -05:00
Idrees Hassan
5e04727a1b Use chance to determine hat unlocks and add heart to menu title 2026-01-21 23:23:00 -05:00
Idrees Hassan
7b1df9bc4f Store unlocked hats 2026-01-21 22:52:53 -05:00
Idrees Hassan
130fae6e0c Add hat collection message 2026-01-21 22:36:12 -05:00
Idrees Hassan
3b2081943d Add hat item 2026-01-21 22:25:49 -05:00
Idrees Hassan
f5742ac3a7 Make the baseball cap pink 2026-01-20 17:17:37 -05:00
Idrees Hassan
867d214292 Add more hats 2026-01-20 17:02:48 -05:00
Idrees Hassan
d97e39449e Fix hats on birds with tufts 2026-01-19 21:06:27 -05:00
Idrees Hassan
7628ee2c87 Add hat to save data 2026-01-19 20:38:00 -05:00
Idrees Hassan
3227167cb5 Add wardrobe menu 2026-01-19 20:31:49 -05:00
Idrees Hassan
e0fae3781a Add bowler hat and fez 2026-01-19 17:18:02 -05:00
Idrees Hassan
2773538a6c Add cowboy hat 2026-01-18 22:41:37 -05:00
Idrees Hassan
2a90a56a2b Add viking helmet 2026-01-18 21:07:02 -05:00
Idrees Hassan
94454a2338 Fix missing tuft sprites 2026-01-18 19:32:38 -05:00
Idrees Hassan
cf968dfec4 Move heart up 2026-01-18 19:30:07 -05:00
Idrees Hassan
4838457054 Create none hat 2026-01-18 19:25:17 -05:00
Idrees Hassan
e09d4f9eea Control hat type from application 2026-01-18 19:24:17 -05:00
Idrees Hassan
7c38bf9164 Separate hat functions 2026-01-18 19:14:18 -05:00
Idrees Hassan
8263fadfba Add hat placeholder 2026-01-18 19:04:40 -05:00
Idrees Hassan
9f7d864e57 Add tag enum 2026-01-18 18:14:40 -05:00
Idrees Hassan
579967a302 Rename color constants enum 2026-01-18 18:10:43 -05:00
Idrees Hassan
ca1495a9f1 Rename getLayer 2026-01-18 18:05:36 -05:00
Idrees Hassan
fd865cacb8 Minor path changes 2026-01-18 17:08:30 -05:00
Idrees Hassan
5e94998410 Move sprite related files to separate folder 2026-01-18 17:03:14 -05:00
Idrees Hassan
e13a67e967 Reduce volume of chirp 2026-01-18 16:55:50 -05:00
Idrees
e1759bc235 Add binarydigit.dev to the readme 2026-01-10 10:46:55 -05:00
Idrees
1d818d83cf Merge pull request #6 from IdreesInc/birdsong
Add chirping when pet
2026-01-04 18:08:44 -05:00
Idrees Hassan
47b418324c Fix menu item text not working at first 2026-01-04 18:08:26 -05:00
Idrees Hassan
e5956426d5 Add toggle to enable/disable sound 2026-01-04 18:02:47 -05:00
Idrees Hassan
0cc06a8856 Change sound timings 2026-01-04 17:39:38 -05:00
Idrees Hassan
dd4184f642 Refine chirp sound 2026-01-04 17:38:14 -05:00
Idrees Hassan
5a82ba858f Add chirping when pet 2026-01-04 17:34:34 -05:00
Idrees Hassan
b8de14bb94 Create new build for extension update 2026-01-01 14:09:13 -05:00
Idrees Hassan
e0ab21d608 Update README.md 2025-12-13 23:08:46 -05:00
Idrees Hassan
86c254b2cb Merge branch 'main' of https://github.com/IdreesInc/Pocket-Bird 2025-12-13 23:05:37 -05:00
Idrees Hassan
14ef2713d8 Add site to readme 2025-12-13 23:04:56 -05:00
Idrees
bc3f5cce02 Update README.md 2025-11-16 13:13:11 -05:00
Idrees Hassan
c3ff3b39dc Add embed code and instructions 2025-11-16 13:10:57 -05:00
Idrees
8b8aa50cae Update README with new download options
Added download links for Obsidian and TamperMonkey.
2025-11-16 13:02:22 -05:00
Idrees Hassan
f4b598d2dc Remove vencord 2025-11-16 12:09:17 -05:00
Idrees
bccaa0aa15 Merge pull request #5 from IdreesInc/vencord
Add support for Vencord and clean up build and context process
2025-11-16 11:12:51 -05:00
Idrees Hassan
37a30ea509 Add Vencord save support 2025-11-16 11:05:44 -05:00
Idrees Hassan
e7be2b7661 Clean up output 2025-11-16 10:38:11 -05:00
Idrees Hassan
c750bf5560 Move files into src 2025-11-16 10:34:43 -05:00
Idrees Hassan
c927ce23e4 Add separate entry points 2025-11-16 10:25:57 -05:00
Idrees Hassan
6ee9efd5a8 Add browser-specific entry point 2025-11-16 09:51:46 -05:00
Idrees Hassan
a5e81e4265 Allow for set contexts 2025-11-16 09:25:48 -05:00
101 changed files with 11023 additions and 3687 deletions

5
.gitignore vendored
View File

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

12
.vscode/launch.json vendored
View File

@@ -1,12 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}/dist/vscode"]
}
]
}

View File

@@ -9,12 +9,18 @@
It's a pet bird that hops around your computer, what more could you want?
### Get it for [Google Chrome](https://chromewebstore.google.com/detail/pocket-bird/lbbdngkbbgaecefacpnhnhleggabghak)
### Get it for [Google Chrome](https://chromewebstore.google.com/detail/pocket-bird/lbbdngkbbgaecefacpnhnhleggabghak)
### Get it for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/pocket-bird/)
### Get it for [Obsidian (beta)](https://github.com/IdreesInc/Pocket-Bird#Obsidian)
### Get it for [TamperMonkey](https://github.com/IdreesInc/Pocket-Bird#Userscript)
#### Join the [Discord](https://discord.gg/6yxE9prcNc) to help me beta test new features and suggest ideas!
![Screen Recording 2026-01-26 at 5 09 45PM](https://github.com/user-attachments/assets/dbde4b75-7195-476f-9411-c1ee7ba7a96f)
## Features
- A cute little pixel art bird hops around your apps and websites
@@ -52,6 +58,14 @@ It's a pet bird that hops around your computer, what more could you want?
3. Install my Pocket Bird script by going to this link and clicking install: [https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js](https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js)
4. Now any websites you visit will have a little bird hopping around!
### Your Own Website
Pocket Bird can also be embedded directly into your own website! Just include the following code snippet anywhere in your HTML:
```html
<script src="https://cdn.jsdelivr.net/gh/IdreesInc/Pocket-Bird@main/dist/web/birb.embed.js"></script>
```
## FAQ
### How do I pet the bird?
@@ -78,6 +92,17 @@ Open the Pocket Bird menu by clicking the bird and select "Settings". From there
If you are running Pocket bird on a browser, the extension needs these permissions in order to insert the bird and sticky notes into your webpages. Pocket Bird does not collect any of your data or browsing history and all data is stored locally on your device!
## Sites With Pocket Bird
Here are some websites where you can find Pocket Bird hopping around:
- [https://grepjason.sh](https://grepjason.sh)
- [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!*
## Getting in Touch
If you'd like to get in touch, check out the [Discord](https://discord.gg/6yxE9prcNc) to suggest features, report bugs, and stay updated on development!

Binary file not shown.

BIN
aseprite/hats.aseprite Normal file

Binary file not shown.

229
build.js
View File

@@ -12,28 +12,30 @@ const IMAGES_DIR = "./images";
const FONTS_DIR = "./fonts";
const DIST_DIR = "./dist";
const BROWSER_MANIFEST = "./platform-specific/extension/manifest.json";
const OBSIDIAN_MANIFEST = "./platform-specific/obsidian/manifest.json";
const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt";
const VSCODE_PACKAGE = "./platform-specific/vscode/package.json";
const OBSIDIAN_WRAPPER = "./platform-specific/obsidian/wrapper.js";
const VSCODE_WRAPPER = "./platform-specific/vscode/extension.js";
const WEB_DIR = DIST_DIR + "/web";
const USERSCRIPT_DIR = DIST_DIR + "/userscript";
const EXTENSION_DIR = DIST_DIR + "/extension";
const OBSIDIAN_DIR = DIST_DIR + "/obsidian";
const VSCODE_DIR = DIST_DIR + "/vscode";
const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css";
const APPLICATION_ENTRY = SRC_DIR + "/application.js";
const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js";
const BIRB_OUTPUT = DIST_DIR + "/birb.js";
const WEB_ENTRY = SRC_DIR + "/platforms/web/web.js";
const USERSCRIPT_ENTRY = SRC_DIR + "/platforms/userscript/userscript.js";
const BROWSER_EXTENSION_ENTRY = SRC_DIR + "/platforms/extension/extension.js";
const OBSIDIAN_ENTRY = SRC_DIR + "/platforms/obsidian/obsidian.js";
const BROWSER_MANIFEST = SRC_DIR + "/platforms/extension/manifest.json";
const OBSIDIAN_MANIFEST = SRC_DIR + "/platforms/obsidian/manifest.json";
const USERSCRIPT_HEADER = SRC_DIR + "/platforms/userscript/header.txt";
const OBSIDIAN_WRAPPER = SRC_DIR + "/platforms/obsidian/wrapper.js";
const TEMP_BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js";
const MONOCRAFT_URL = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
const VERSION_KEY = "__VERSION__";
const STYLESHEET_KEY = "___STYLESHEET___";
const MONOCRAFT_SRC_KEY = "__MONOCRAFT_SRC__";
const MONOCRAFT_URL_KEY = "__MONOCRAFT_URL__";
const CODE_KEY = "__CODE__";
const spriteSheets = [
@@ -44,6 +46,10 @@ const spriteSheets = [
{
key: "__FEATHER_SPRITE_SHEET__",
path: SPRITES_DIR + "/feather.png"
},
{
key: "__HATS_SPRITE_SHEET__",
path: SPRITES_DIR + "/hats.png"
}
];
@@ -77,130 +83,135 @@ const version = `${versionDate}`; // Disable build number for now
buildCache.version = version;
writeFileSync(BUILD_CACHE_PATH, JSON.stringify(buildCache), 'utf8');
// =============================================
// Build JavaScript function
// =============================================
/**
* @param {string} entryPoint
* @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>}
*/
async function generateCode(entryPoint, embedFont = false) {
// Bundle with rollup
const bundle = await rollup({
input: entryPoint,
});
// Bundle with rollup
const bundle = await rollup({
input: APPLICATION_ENTRY,
});
await bundle.write({
file: TEMP_BUNDLED_OUTPUT,
format: 'iife',
});
await bundle.write({
file: BUNDLED_OUTPUT,
format: 'iife',
});
await bundle.close();
await bundle.close();
let birbJs = readFileSync(TEMP_BUNDLED_OUTPUT, 'utf8');
let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8');
// Delete bundled file
unlinkSync(TEMP_BUNDLED_OUTPUT);
// Delete bundled file
unlinkSync(BUNDLED_OUTPUT);
// Replace version placeholder
birbJs = birbJs.replaceAll(VERSION_KEY, version);
// Replace version placeholder
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
for (const spriteSheet of spriteSheets) {
const dataUri = readFileSync(spriteSheet.path, 'base64');
birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`);
// Compile and insert sprite sheets
for (const spriteSheet of spriteSheets) {
const dataUri = readFileSync(spriteSheet.path, 'base64');
birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`);
}
// Insert stylesheet
const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8');
birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent);
return birbJs;
}
// Insert stylesheet
const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8');
birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent).replace(MONOCRAFT_SRC_KEY, MONOCRAFT_URL);
async function buildWeb() {
const birbJs = await generateCode(WEB_ENTRY);
mkdirSync(WEB_DIR, { recursive: true });
writeFileSync(WEB_DIR + '/birb.js', birbJs);
writeFileSync(WEB_DIR + '/birb.embed.js', birbJs);
}
async function buildUserscript() {
const birbJs = await generateCode(USERSCRIPT_ENTRY);
// Write bundled JavaScript function
writeFileSync(BIRB_OUTPUT, birbJs);
// Get userscript header
const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version);
// =============================================
// Build userscript
// =============================================
mkdirSync(USERSCRIPT_DIR, { recursive: true });
const userScript = userScriptHeader + "\n" + birbJs;
writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript);
}
// Get userscript header
const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version);
async function buildExtension() {
const birbJs = await generateCode(BROWSER_EXTENSION_ENTRY);
mkdirSync(USERSCRIPT_DIR, { recursive: true });
const userScript = userScriptHeader + "\n" + birbJs;
writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript);
mkdirSync(EXTENSION_DIR, { recursive: true });
// =============================================
// Build browser extension
// =============================================
// Copy birb.js
writeFileSync(EXTENSION_DIR + '/birb.js', birbJs);
mkdirSync(EXTENSION_DIR, { recursive: true });
// Copy manifest.json
let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8');
browserManifest = browserManifest.replace(VERSION_KEY, version);
writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest);
// Copy birb.js
writeFileSync(EXTENSION_DIR + '/birb.js', birbJs);
// Copy icons folder
mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true });
cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true });
// Copy manifest.json
let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8');
browserManifest = browserManifest.replace(VERSION_KEY, version);
writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest);
// Copy fonts folder
mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true });
cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true });
// Copy icons folder
mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true });
cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true });
// Compress extension folder into zip
const output = createWriteStream(DIST_DIR + "/extension.zip");
const archive = archiver('zip');
// Copy fonts folder
mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true });
cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true });
output.on('close', () => {
console.log(`Created zip file: ${archive.pointer()} total bytes`);
});
// Compress extension folder into zip
const output = createWriteStream(DIST_DIR + "/extension.zip");
const archive = archiver('zip');
archive.on('error', (err) => {
throw err;
});
output.on('close', () => {
console.log(`Created zip file: ${archive.pointer()} total bytes`);
});
archive.pipe(output);
archive.directory(EXTENSION_DIR + '/', false);
archive.finalize();
}
archive.on('error', (err) => {
throw err;
});
async function buildObsidian() {
// embedFont=true: bakes the font as base64 so the plugin works fully offline.
const birbJs = await generateCode(OBSIDIAN_ENTRY, true);
archive.pipe(output);
archive.directory(EXTENSION_DIR + '/', false);
archive.finalize();
mkdirSync(OBSIDIAN_DIR, { recursive: true });
// =============================================
// Build Obsidian plugin
// =============================================
// Wrap birb.js with plugin boilerplate
let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs);
mkdirSync(OBSIDIAN_DIR, { recursive: true });
// Create main.js with plugin code
writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin);
// Wrap birb.js with plugin boilerplate
let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs);
// Copy manifest.json
let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8');
obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`);
writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest);
}
// Encode font to data URI since Obsidian plugins can't have external font files
const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64');
const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`;
obsidianPlugin = obsidianPlugin.replace(MONOCRAFT_URL, monocraftDataUri);
console.log("Starting build...");
// Create main.js with plugin code
writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin);
await buildWeb();
await buildUserscript();
await buildExtension();
await buildObsidian();
// Copy manifest.json
let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8');
obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`);
writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest);
// =============================================
// Build VSCode extension
// =============================================
mkdirSync(VSCODE_DIR, { recursive: true });
// Wrap birb.js with VSCode extension boilerplate
let vscodeExtension = readFileSync(VSCODE_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs);
// Create extension.js with extension code
writeFileSync(VSCODE_DIR + '/extension.js', vscodeExtension);
// Copy package.json
let vscodePackage = readFileSync(VSCODE_PACKAGE, 'utf8');
vscodePackage = vscodePackage.replace(VERSION_KEY, version);
writeFileSync(VSCODE_DIR + '/package.json', vscodePackage);
console.log(`Build complete: ${version}`);
console.log("Build completed successfully!");

BIN
dist/extension.zip vendored

Binary file not shown.

2281
dist/extension/birb.js vendored

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 996 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 914 B

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 B

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 953 B

After

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 936 B

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 B

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

2243
dist/obsidian/main.js vendored

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
{
"name": "pocket-bird",
"version": "2025.11.15",
"engines": {
"vscode": "^1.32.0"
},
"activationEvents": [
"onStartupFinished"
],
"main": "extension.js",
"contributes": {
"commands": [
{
"command": "pocket-bird.helloWorld",
"title": "Hello World",
"category": "Example"
}
]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

380
editor/editor.js Normal file
View 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
View 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
View 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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 996 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 914 B

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 B

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 953 B

After

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 936 B

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 B

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,16 +0,0 @@
// The module 'vscode' contains the VS Code extensibility API
const vscode = require("vscode");
module.exports = {
activate,
deactivate,
};
function activate(context) {
console.log("Loading Pocket Bird...");
__CODE__
console.log("Pocket Bird loaded!");
}
function deactivate() {}

View File

@@ -1,5 +0,0 @@
#!/bin/bash
# my-vscode
export NODE_OPTIONS="--require /Users/idrees/Documents/Programs/JavaScript/Birb/platform-specific/vscode/patch.js"
"/Applications/Visual Studio Code.app/Contents/MacOS/Electron" \
"$@"

View File

@@ -1,21 +0,0 @@
{
"name": "pocket-bird",
"version": "__VERSION__",
"engines": {
"vscode": "^1.32.0"
},
"activationEvents": [
"onStartupFinished"
],
"main": "extension.js",
"contributes": {
"commands": [
{
"command": "pocket-bird.helloWorld",
"title": "Hello World",
"category": "Example"
}
]
}
}

View File

@@ -1 +0,0 @@
console.log("Birb patch for VSCode loaded.");

View File

@@ -26,6 +26,6 @@
</div>
<div id="spacer"></div>
<!-- <script type="module" src="spritesheet-compiler.js"></script> -->
<script src="../dist/birb.js"></script>
<script src="../dist/web/birb.js"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
sprites/hats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

View File

@@ -1,76 +0,0 @@
import { Directions } from './shared.js';
import { Sprite, BirdType } from './sprites.js';
import Layer from './layer.js';
class Frame {
/** @type {{ [tag: string]: string[][] }} */
#pixelsByTag = {};
/**
* @param {Layer[]} layers
*/
constructor(layers) {
/** @type {Set<string>} */
let tags = new Set();
for (let layer of layers) {
tags.add(layer.tag);
}
tags.add("default");
for (let tag of tags) {
let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0);
if (layers[0].tag !== "default") {
throw new Error("First layer must have the 'default' tag");
}
this.pixels = layers[0].pixels.map(row => row.slice());
// Pad from top with transparent pixels
while (this.pixels.length < maxHeight) {
this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT));
}
// Combine layers
for (let i = 1; i < layers.length; i++) {
if (layers[i].tag === "default" || layers[i].tag === tag) {
let layerPixels = layers[i].pixels;
let topMargin = maxHeight - layerPixels.length;
for (let y = 0; y < layerPixels.length; y++) {
for (let x = 0; x < layerPixels[y].length; x++) {
this.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x];
}
}
}
}
this.#pixelsByTag[tag] = this.pixels.map(row => row.slice());
}
}
/**
* @param {string} [tag]
* @returns {string[][]}
*/
getPixels(tag = "default") {
return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"];
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {BirdType} [species]
* @param {number} direction
* @param {number} canvasPixelSize
*/
draw(ctx, direction, canvasPixelSize, species) {
// Clear the canvas before drawing the new frame
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const pixels = this.getPixels(species?.tags[0]);
for (let y = 0; y < pixels.length; y++) {
const row = pixels[y];
for (let x = 0; x < pixels[y].length; x++) {
const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1];
ctx.fillStyle = species?.colors[cell] ?? cell;
ctx.fillRect(x * canvasPixelSize, y * canvasPixelSize, canvasPixelSize, canvasPixelSize);
};
};
}
}
export default Frame;

View File

@@ -1,12 +0,0 @@
class Layer {
/**
* @param {string[][]} pixels
* @param {string} [tag]
*/
constructor(pixels, tag = "default") {
this.pixels = pixels;
this.tag = tag;
}
}
export default Layer;

View File

@@ -1,5 +1,5 @@
import Frame from "./frame.js";
import { BirdType } from "./sprites";
import { BirdType } from "./sprites.js";
class Anim {
/**
@@ -59,10 +59,11 @@ class Anim {
* @param {number} direction
* @param {number} timeStart The start time of the animation in milliseconds
* @param {number} canvasPixelSize The size of a canvas pixel in pixels
* @param {BirdType} [species] The species to use for the animation
* @param {{ [key: string]: string }} colorScheme The color scheme to use for the animation
* @param {string[]} tags The tags to use for the animation
* @returns {boolean} Whether the animation is complete
*/
draw(ctx, direction, timeStart, canvasPixelSize, species) {
draw(ctx, direction, timeStart, canvasPixelSize, colorScheme, tags) {
// Reset cache if animation was restarted
if (this.lastTimeStart !== timeStart) {
this.#clearCache();
@@ -79,7 +80,7 @@ class Anim {
const currentFrameIndex = this.getCurrentFrameIndex(time);
if (this.#shouldRedraw(currentFrameIndex, direction)) {
this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, species);
this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, colorScheme, tags);
this.lastFrameIndex = currentFrameIndex;
this.lastDirection = direction;
}

View File

@@ -1,6 +1,6 @@
import { Directions } from './shared.js';
import { Sprite, BirdType } from './sprites.js';
import Layer from './layer.js';
import { Directions } from '../shared.js';
import { PALETTE, BirdType } from './sprites.js';
import Layer, { TAG } from './layer.js';
class Frame {
@@ -16,25 +16,25 @@ class Frame {
for (let layer of layers) {
tags.add(layer.tag);
}
tags.add("default");
tags.add(TAG.DEFAULT);
for (let tag of tags) {
let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0);
if (layers[0].tag !== "default") {
if (layers[0].tag !== TAG.DEFAULT) {
throw new Error("First layer must have the 'default' tag");
}
this.pixels = layers[0].pixels.map(row => row.slice());
// Pad from top with transparent pixels
while (this.pixels.length < maxHeight) {
this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT));
this.pixels.unshift(new Array(this.pixels[0].length).fill(PALETTE.TRANSPARENT));
}
// Combine layers
for (let i = 1; i < layers.length; i++) {
if (layers[i].tag === "default" || layers[i].tag === tag) {
if (layers[i].tag === TAG.DEFAULT || layers[i].tag === tag) {
let layerPixels = layers[i].pixels;
let topMargin = maxHeight - layerPixels.length;
for (let y = 0; y < layerPixels.length; y++) {
for (let x = 0; x < layerPixels[y].length; x++) {
this.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x];
this.pixels[y + topMargin][x] = layerPixels[y][x] !== PALETTE.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x];
}
}
}
@@ -44,29 +44,36 @@ class Frame {
}
/**
* @param {string} [tag]
* @param {string[]} [tags]
* @returns {string[][]}
*/
getPixels(tag = "default") {
return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"];
getPixels(tags = [TAG.DEFAULT]) {
for (let i = tags.length - 1; i >= 0; i--) {
const tag = tags[i];
if (this.#pixelsByTag[tag]) {
return this.#pixelsByTag[tag];
}
}
return this.#pixelsByTag[TAG.DEFAULT];
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {BirdType} [species]
* @param {number} direction
* @param {number} direction
* @param {number} canvasPixelSize
* @param {{ [key: string]: string }} colorScheme
* @param {string[]} tags
*/
draw(ctx, direction, canvasPixelSize, species) {
draw(ctx, direction, canvasPixelSize, colorScheme, tags) {
// Clear the canvas before drawing the new frame
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const pixels = this.getPixels(species?.tags[0]);
const pixels = this.getPixels(tags);
for (let y = 0; y < pixels.length; y++) {
const row = pixels[y];
for (let x = 0; x < pixels[y].length; x++) {
const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1];
ctx.fillStyle = species?.colors[cell] ?? cell;
ctx.fillStyle = colorScheme[cell] ?? cell;
ctx.fillRect(x * canvasPixelSize, y * canvasPixelSize, canvasPixelSize, canvasPixelSize);
};
};

View File

@@ -1,9 +1,14 @@
export const TAG = {
DEFAULT: "default",
TUFT: "tuft",
};
class Layer {
/**
* @param {string[][]} pixels
* @param {string} [tag]
*/
constructor(pixels, tag = "default") {
constructor(pixels, tag = TAG.DEFAULT) {
this.pixels = pixels;
this.tag = tag;
}

224
src/animation/sprites.js Normal file
View File

@@ -0,0 +1,224 @@
import species from "../species.js"
export const PALETTE = Object.freeze(/** @type {const} */ ({
THEME_HIGHLIGHT: "theme-highlight",
TRANSPARENT: "transparent",
OUTLINE: "outline",
BORDER: "border",
FOOT: "foot",
BEAK: "beak",
EYE: "eye",
FACE: "face",
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_TIP: "nose-tip",
CHEEK: "cheek",
SCRUFF: "scruff",
CHIN: "chin",
COLLAR: "collar",
COLLAR_SCRUFF: "collar-scruff",
BELLY: "belly",
UNDERBELLY: "underbelly",
WING: "wing",
SHOULDER: "shoulder",
WING_SPOTS: "wing-spots",
WING_EDGE: "wing-edge",
HEART: "heart",
HEART_BORDER: "heart-border",
HEART_SHINE: "heart-shine",
FEATHER_SPINE: "feather-spine",
}));
/** @typedef {typeof PALETTE[keyof typeof PALETTE]} PaletteColor */
/**
* Mapping of sprite sheet colors to palette colors
* @type {Record<string, PaletteColor>}
*/
export const SPRITE_SHEET_COLOR_MAP = {
"transparent": PALETTE.TRANSPARENT,
"#fff000": PALETTE.THEME_HIGHLIGHT,
"#ffffff": PALETTE.BORDER,
"#000000": PALETTE.OUTLINE,
"#010a19": PALETTE.BEAK,
"#190301": PALETTE.EYE,
"#af8e75": PALETTE.FOOT,
"#639bff": PALETTE.FACE,
"#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,
"#b93844": PALETTE.NOSE_TIP,
"#ff67a9": PALETTE.CHEEK,
"#c5e550": PALETTE.SCRUFF,
"#b87af1": PALETTE.CHIN,
"#ffe955": PALETTE.COLLAR,
"#f8ff55": PALETTE.COLLAR_SCRUFF,
"#f8b143": PALETTE.BELLY,
"#ec8637": PALETTE.UNDERBELLY,
"#578ae6": PALETTE.WING,
"#55d1f3": PALETTE.SHOULDER,
"#90b0e8": PALETTE.WING_SPOTS,
"#326ed9": PALETTE.WING_EDGE,
"#c82e2e": PALETTE.HEART,
"#501a1a": PALETTE.HEART_BORDER,
"#ff6b6b": PALETTE.HEART_SHINE,
"#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 {
/**
* @param {string} name
* @param {string} description
* @param {string} latinName
* @param {string} url
* @param {Record<string, string>} colors
* @param {string[]} [tags]
* @param {Rarity} [rarity]
*/
constructor(name, description, latinName, url, colors, tags = [], rarity = RARITY.COMMON) {
this.name = name;
this.description = description;
this.latinName = latinName;
this.url = url;
const defaultColors = {
[PALETTE.TRANSPARENT]: "transparent",
[PALETTE.OUTLINE]: "#000000",
[PALETTE.BORDER]: "#ffffff",
[PALETTE.BEAK]: "#000000",
[PALETTE.EYE]: "#000000",
[PALETTE.HEART]: "#c82e2e",
[PALETTE.HEART_BORDER]: "#501a1a",
[PALETTE.HEART_SHINE]: "#ff6b6b",
[PALETTE.FEATHER_SPINE]: "#373737",
[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_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>} */
this.colors = { ...defaultColors, ...colors, [PALETTE.THEME_HIGHLIGHT]: colors[PALETTE.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
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>} */
export const SPECIES = Object.fromEntries(
Object.entries(species).map(([id, data]) => [
id,
new BirdType(data.name, data.description, data.latinName, data.url, data.colors, data.tags, data.rarity)
]),
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,9 @@
import { Directions, getLayer, getWindowHeight, getFixedWindowHeight } from './shared.js';
import Layer from './layer.js';
import Frame from './frame.js';
import Anim from './anim.js';
import { BirdType } from './sprites.js';
import { Directions, getLayerPixels, getWindowHeight, getFixedWindowHeight } from './shared.js';
import Layer from './animation/layer.js';
import Frame from './animation/frame.js';
import Anim from './animation/anim.js';
import { BirdType, PALETTE } from './animation/sprites.js';
import { createHatLayers } from './hats.js';
/**
* @typedef {keyof typeof Animations} AnimationType
@@ -31,8 +32,9 @@ export class Birb {
* @param {string[][]} spriteSheet The loaded sprite sheet pixel data
* @param {number} spriteWidth
* @param {number} spriteHeight
* @param {string[][]} hatSpriteSheet The loaded hat sprite sheet pixel data
*/
constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) {
constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight, hatSpriteSheet) {
this.birbCssScale = birbCssScale;
this.canvasPixelSize = canvasPixelSize;
this.windowPixelSize = canvasPixelSize * birbCssScale;
@@ -41,28 +43,31 @@ export class Birb {
// Build layers from sprite sheet
this.layers = {
base: new Layer(getLayer(spriteSheet, 0, this.spriteWidth)),
down: new Layer(getLayer(spriteSheet, 1, this.spriteWidth)),
heartOne: new Layer(getLayer(spriteSheet, 2, this.spriteWidth)),
heartTwo: new Layer(getLayer(spriteSheet, 3, this.spriteWidth)),
heartThree: new Layer(getLayer(spriteSheet, 4, this.spriteWidth)),
tuftBase: new Layer(getLayer(spriteSheet, 5, this.spriteWidth), "tuft"),
tuftDown: new Layer(getLayer(spriteSheet, 6, this.spriteWidth), "tuft"),
wingsUp: new Layer(getLayer(spriteSheet, 7, this.spriteWidth)),
wingsDown: new Layer(getLayer(spriteSheet, 8, this.spriteWidth)),
happyEye: new Layer(getLayer(spriteSheet, 9, this.spriteWidth)),
base: new Layer(getLayerPixels(spriteSheet, 0, this.spriteWidth)),
down: new Layer(getLayerPixels(spriteSheet, 1, this.spriteWidth)),
heartOne: new Layer(getLayerPixels(spriteSheet, 2, this.spriteWidth)),
heartTwo: new Layer(getLayerPixels(spriteSheet, 3, this.spriteWidth)),
heartThree: new Layer(getLayerPixels(spriteSheet, 4, this.spriteWidth)),
tuftBase: new Layer(getLayerPixels(spriteSheet, 5, this.spriteWidth), "tuft"),
tuftDown: new Layer(getLayerPixels(spriteSheet, 6, this.spriteWidth), "tuft"),
wingsUp: new Layer(getLayerPixels(spriteSheet, 7, this.spriteWidth)),
wingsDown: new Layer(getLayerPixels(spriteSheet, 8, this.spriteWidth)),
happyEye: new Layer(getLayerPixels(spriteSheet, 9, this.spriteWidth)),
};
// Build hat layers
const hatLayers = createHatLayers(hatSpriteSheet);
// Build frames from layers
this.frames = {
base: new Frame([this.layers.base, this.layers.tuftBase]),
headDown: new Frame([this.layers.down, this.layers.tuftDown]),
wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]),
wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]),
heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]),
heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]),
heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
base: new Frame([this.layers.base, this.layers.tuftBase, ...hatLayers.base]),
headDown: new Frame([this.layers.down, this.layers.tuftDown, ...hatLayers.down]),
wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown, ...hatLayers.base]),
wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp, ...hatLayers.down]),
heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, ...hatLayers.base, this.layers.heartOne]),
heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, ...hatLayers.base,this.layers.heartTwo]),
heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, ...hatLayers.base, this.layers.heartThree]),
heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, ...hatLayers.base, this.layers.heartTwo]),
};
// Build animations from frames
@@ -121,14 +126,16 @@ export class Birb {
/**
* Draw the current animation frame
* @param {BirdType} species The species color data
* @param {BirdType} species The species data
* @param {string} [hat] The name of the current hat
* @returns {boolean} Whether the animation has completed (for non-looping animations)
*/
draw(species) {
draw(species, hat) {
const anim = this.animations[this.currentAnimation];
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species);
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species.colors, [...species.tags, hat || '']);
}
/**
* @returns {AnimationType} The current animation key
*/

View File

@@ -1,7 +1,9 @@
import { debug, log, error } from "./shared.js";
const SAVE_KEY = "birbSaveData";
export const SAVE_KEY = "birbSaveData";
const ROOT_PATH = "";
const SET_CONTEXT = "__CONTEXT__"
const MONOCRAFT_URL = "__MONOCRAFT_URL__";
/**
* @typedef {import('./application.js').BirbSaveData} BirbSaveData
@@ -12,14 +14,6 @@ const ROOT_PATH = "";
*/
export class Context {
/**
* @abstract
* @returns {boolean} Whether this context is applicable
*/
isContextActive() {
throw new Error("Method not implemented");
}
/**
* @abstract
* @returns {Promise<BirbSaveData|{}>}
@@ -99,20 +93,17 @@ export class Context {
areStickyNotesEnabled() {
return true;
}
/**
* @returns {string}
*/
getFontStyles() {
return getFontFaceImport(MONOCRAFT_URL);
}
}
export class LocalContext extends Context {
/**
* @override
* @returns {boolean}
*/
isContextActive() {
return window.location.hostname === "127.0.0.1"
|| window.location.hostname === "localhost"
|| window.location.hostname.startsWith("192.168.");
}
/**
* @override
* @returns {Promise<BirbSaveData|{}>}
@@ -140,15 +131,6 @@ export class LocalContext extends Context {
export class UserScriptContext extends Context {
/**
* @override
* @returns {boolean}
*/
isContextActive() {
// @ts-expect-error
return typeof GM_getValue === "function";
}
/**
* @override
* @returns {Promise<BirbSaveData|{}>}
@@ -180,16 +162,7 @@ export class UserScriptContext extends Context {
}
}
class BrowserExtensionContext extends Context {
/**
* @override
* @returns {boolean}
*/
isContextActive() {
// @ts-expect-error
return typeof chrome !== "undefined";
}
export class BrowserExtensionContext extends Context {
/**
* @override
@@ -216,9 +189,9 @@ class BrowserExtensionContext extends Context {
// @ts-expect-error
if (chrome.runtime.lastError) {
// @ts-expect-error
console.error(chrome.runtime.lastError);
error(chrome.runtime.lastError);
} else {
console.log("Settings saved successfully");
log("Settings saved successfully");
}
});
}
@@ -229,17 +202,19 @@ class BrowserExtensionContext extends Context {
// @ts-expect-error
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 {
/**
* @override
* @returns {boolean}
*/
isContextActive() {
// @ts-expect-error
return typeof app !== "undefined" && typeof app.vault !== "undefined";
}
/**
* @override
@@ -319,21 +294,12 @@ export class ObsidianContext extends Context {
}
}
const CONTEXTS = [
new UserScriptContext(),
new ObsidianContext(),
new BrowserExtensionContext(),
new LocalContext()
];
export function getContext() {
for (const context of CONTEXTS) {
if (context.isContextActive()) {
return context;
}
}
error("No applicable context found, defaulting to LocalContext");
return new LocalContext();
/**
* @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; }`;
}
/**

0
src/fieldGuide.js Normal file
View File

240
src/hats.js Normal file
View File

@@ -0,0 +1,240 @@
import Anim from "./animation/anim.js";
import Frame from "./animation/frame.js";
import Layer, { TAG } from "./animation/layer.js";
import { PALETTE } from "./animation/sprites.js";
import { getLayerPixels } from "./shared.js";
const HAT_WIDTH = 12;
export const HAT = {
NONE: "none",
TOP_HAT: "top-hat",
FEZ: "fez",
WIZARD_HAT: "wizard-hat",
BASEBALL_CAP: "baseball-cap",
FLOWER_HAT: "flower-hat",
COWBOY_HAT: "cowboy-hat",
BEANIE: "beanie",
SUN_HAT: "sun-hat",
VIKING_HELMET: "viking-helmet",
STRAW_HAT: "straw-hat",
CORDOVAN_HAT: "cordovan-hat"
};
/** @type {{ [hatId: string]: { name: string, description: string } }} */
export const HAT_METADATA = {
[HAT.NONE]: {
name: "Invisible Hat",
description: "It's like you're wearing nothing at all!"
},
[HAT.TOP_HAT]: {
name: "Top Hat",
description: "The mark of a true gentlebird."
},
[HAT.VIKING_HELMET]: {
name: "Viking Helmet",
description: "Sure, vikings never actually wore this style of helmet, but why let facts get in the way of good fashion?"
},
[HAT.COWBOY_HAT]: {
name: "Cowboy Hat",
description: "You can't jam with the console cowboys without the appropriate attire."
},
[HAT.FEZ]: {
name: "Fez",
description: "It's a fez. Fezzes are cool."
},
[HAT.WIZARD_HAT]: {
name: "Wizard Hat",
description: "Grants the bearer terrifying mystical power, but luckily birds only use it to summon old ladies with bread crumbs."
},
[HAT.BASEBALL_CAP]: {
name: "Baseball Cap",
description: "Birds unfortunately only ever hit 'fowl' balls..."
},
[HAT.FLOWER_HAT]: {
name: "Flower Hat",
description: "To be fair, this is less of a hat and more of a dirt clod that your pet happened to pick up."
},
[HAT.BEANIE]: {
name: "Beanie",
description: "Keeps feathers warm on those long migrations south!"
},
[HAT.SUN_HAT]: {
name: "Sun Hat",
description: "Perfect for frolicking through enchanted flower fields."
},
[HAT.STRAW_HAT]: {
name: "Straw Hat",
description: "A classic design, though keep away from water as this particular hat is seemingly unable to float."
},
[HAT.CORDOVAN_HAT]: {
name: "Cordovan Hat",
description: "A traditional Spanish hat that stays put even in the wildest of sword fights."
}
};
/**
* @param {string[][]} spriteSheet
* @returns {{ base: Layer[], down: Layer[] }}
*/
export function createHatLayers(spriteSheet) {
const hatLayers = {
base: [],
down: []
};
for (let i = 0; i < Object.keys(HAT).length; i++) {
const hatName = Object.keys(HAT)[i];
if (hatName === 'NONE') {
continue;
}
const index = i - 1;
const hatKey = HAT[hatName];
const hatLayer = buildHatLayer(spriteSheet, hatKey, index);
const downHatLayer = buildHatLayer(spriteSheet, hatKey, index, 1);
hatLayers.base.push(hatLayer);
hatLayers.down.push(downHatLayer);
}
return hatLayers;
}
/**
* @param {string[][]} spriteSheet
* @param {string} hatId
* @returns {Anim}
*/
export function createHatItemAnimation(hatId, spriteSheet) {
const hatLayer = buildHatItemLayer(spriteSheet, hatId);
const frames = [
new Frame([hatLayer])
];
return new Anim(frames, [1000], true);
}
/**
* @param {string[][]} spriteSheet
* @param {string} hatName
* @param {number} hatIndex
* @param {number} [yOffset=0]
* @returns {Layer}
*/
function buildHatLayer(spriteSheet, hatName, hatIndex, yOffset = 0) {
const LEFT_PADDING = 6;
const RIGHT_PADDING = 14;
const TOP_PADDING = 5 + yOffset;
const BOTTOM_PADDING = Math.max(0, 15 - yOffset);
let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
hatPixels = pad(hatPixels, TOP_PADDING, BOTTOM_PADDING, LEFT_PADDING, RIGHT_PADDING);
hatPixels = drawOutline(hatPixels, false);
return new Layer(hatPixels, hatName);
}
/**
* @param {string[][]} spriteSheet
* @param {string} hatId
* @returns {Layer}
*/
function buildHatItemLayer(spriteSheet, hatId) {
if (hatId === HAT.NONE) {
return new Layer([], TAG.DEFAULT);
}
const hatIndex = Object.values(HAT).indexOf(hatId) - 1;
let hatPixels = getLayerPixels(spriteSheet, hatIndex, HAT_WIDTH);
hatPixels = pad(hatPixels, 1, 1, 1, 1);
hatPixels = drawOutline(hatPixels, true);
hatPixels = pushToBottom(hatPixels);
return new Layer(hatPixels, TAG.DEFAULT);
}
/**
* Add transparent padding around the pixel array
* @param {string[][]} pixels
* @param {number} top
* @param {number} bottom
* @param {number} left
* @param {number} right
* @returns {string[][]}
*/
function pad(pixels, top, bottom, left, right) {
const paddedPixels = [];
const rowLength = pixels[0].length + left + right;
// Top padding
for (let y = 0; y < top; y++) {
paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
}
// Left and right padding
for (let y = 0; y < pixels.length; y++) {
const row = [];
for (let x = 0; x < left; x++) {
row.push(PALETTE.TRANSPARENT);
}
for (let x = 0; x < pixels[y].length; x++) {
row.push(pixels[y][x]);
}
for (let x = 0; x < right; x++) {
row.push(PALETTE.TRANSPARENT);
}
paddedPixels.push(row);
}
// Bottom padding
for (let y = 0; y < bottom; y++) {
paddedPixels.push(Array(rowLength).fill(PALETTE.TRANSPARENT));
}
return paddedPixels;
}
/**
* Draw an outline around non-transparent pixels
* @param {string[][]} pixels
* @param {boolean} [outlineBottom=false]
* @return {string[][]}
*/
function drawOutline(pixels, outlineBottom = false) {
let neighborOffsets = [
[-1, 0],
[1, 0],
[0, -1],
[-1, -1],
[1, -1],
];
if (outlineBottom) {
neighborOffsets.push([0, 1], [-1, 1], [1, 1]);
}
for (let y = 0; y < pixels.length; y++) {
for (let x = 0; x < pixels[y].length; x++) {
const pixel = pixels[y][x];
if (pixel !== PALETTE.TRANSPARENT && pixel !== PALETTE.BORDER) {
for (let [dx, dy] of neighborOffsets) {
const newX = x + dx;
const newY = y + dy;
if (newY >= 0 && newY < pixels.length && newX >= 0 && newX < pixels[newY].length && pixels[newY][newX] === PALETTE.TRANSPARENT) {
pixels[newY][newX] = PALETTE.BORDER;
}
}
}
}
}
return pixels;
}
/**
* Trim transparent rows from the bottom and push them to the top
* @param {string[][]} pixels
* @returns {string[][]}
*/
function pushToBottom(pixels) {
let trimmedPixels = pixels.slice();
let trimCount = 0;
while (trimmedPixels.length > 1) {
const firstRow = trimmedPixels[trimmedPixels.length - 1];
if (firstRow.every(pixel => pixel === PALETTE.TRANSPARENT)) {
trimmedPixels.pop();
trimCount++;
} else {
break;
}
}
trimmedPixels = pad(trimmedPixels, trimCount, 0, 0, 0);
return trimmedPixels;
}

View File

@@ -12,13 +12,15 @@ export const MENU_EXIT_ID = "birb-menu-exit";
export class MenuItem {
/**
* @param {string} text
* @param {string|(() => string)} text
* @param {() => void} action
* @param {number[][]} [icon]
* @param {boolean} [removeMenu]
*/
constructor(text, action, removeMenu = true) {
constructor(text, action, icon, removeMenu = true) {
this.text = text;
this.action = action;
this.icon = icon;
this.removeMenu = removeMenu;
}
}
@@ -28,10 +30,11 @@ export class ConditionalMenuItem extends MenuItem {
* @param {string} text
* @param {() => void} action
* @param {() => boolean} condition
* @param {number[][]} [icon]
* @param {boolean} [removeMenu]
*/
constructor(text, action, condition, removeMenu = true) {
super(text, action, removeMenu);
constructor(text, action, condition, icon, removeMenu = true) {
super(text, action, icon, removeMenu);
this.condition = condition;
}
}
@@ -42,7 +45,7 @@ export class DebugMenuItem extends ConditionalMenuItem {
* @param {() => void} action
*/
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
* @returns {HTMLElement}
*/
function makeMenuItem(item, removeMenuCallback) {
function createMenuItem(item, removeMenuCallback) {
if (item instanceof Separator) {
return makeElement("birb-window-separator");
}
let menuItem = makeElement("birb-menu-item", 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, () => {
if (item.removeMenu) {
removeMenuCallback();
@@ -89,7 +110,7 @@ export function insertMenu(menuItems, title, updateLocationCallback) {
const removeCallback = () => removeMenu();
for (const item of menuItems) {
if (!(item instanceof ConditionalMenuItem) || item.condition()) {
content.appendChild(makeMenuItem(item, removeCallback));
content.appendChild(createMenuItem(item, removeCallback));
}
}
menu.appendChild(header);
@@ -146,7 +167,7 @@ export function switchMenuItems(menuItems, updateLocationCallback) {
const removeCallback = () => removeMenu();
for (const item of menuItems) {
if (!(item instanceof ConditionalMenuItem) || item.condition()) {
content.appendChild(makeMenuItem(item, removeCallback));
content.appendChild(createMenuItem(item, removeCallback));
}
}
updateLocationCallback(menu);

View File

@@ -0,0 +1,4 @@
import { initializeApplication } from "../../application.js";
import { BrowserExtensionContext } from "../../context.js";
initializeApplication(new BrowserExtensionContext());

View File

@@ -0,0 +1,4 @@
import { initializeApplication } from "../../application.js";
import { ObsidianContext } from "../../context.js";
initializeApplication(new ObsidianContext());

View File

@@ -0,0 +1,4 @@
import { initializeApplication } from "../../application.js";
import { UserScriptContext } from "../../context.js";
initializeApplication(new UserScriptContext());

4
src/platforms/web/web.js Normal file
View File

@@ -0,0 +1,4 @@
import { initializeApplication } from "../../application.js";
import { LocalContext } from "../../context.js";
initializeApplication(new LocalContext());

View File

@@ -3,7 +3,8 @@ export const Directions = {
RIGHT: 1,
};
let debugMode = window.location.hostname === "127.0.0.1";
let debugMode = location.hostname === "127.0.0.1";
let context = null;
/**
* @returns {boolean} Whether debug mode is enabled
@@ -19,6 +20,17 @@ export function setDebug(value) {
debugMode = value;
}
export function getContext() {
if (!context) {
throw new Error("Context requested before being set");
}
return context;
}
export function setContext(newContext) {
context = newContext;
}
/**
* Create an HTML element with the specified parameters
* @param {string} className
@@ -181,7 +193,7 @@ export function error() {
* @param {number} width The width of each sprite
* @returns {string[][]}
*/
export function getLayer(spriteSheet, spriteIndex, width) {
export function getLayerPixels(spriteSheet, spriteIndex, width) {
// From an array of a horizontal sprite sheet, get the layer for a specific sprite
const layer = [];
for (let y = 0; y < width; y++) {

48
src/sound.js Normal file
View File

@@ -0,0 +1,48 @@
// @ts-check
export class Birdsong {
/**
* @type {AudioContext}
*/
audioContext;
chirp() {
const count = Math.floor(1 + Math.random() * 1.5);
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);
}
}
}

430
src/species.js Normal file
View 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"
}
}

View File

@@ -1,199 +0,0 @@
/** Indicators for parts of the base bird sprite sheet */
export const Sprite = {
THEME_HIGHLIGHT: "theme-highlight",
TRANSPARENT: "transparent",
OUTLINE: "outline",
BORDER: "border",
FOOT: "foot",
BEAK: "beak",
EYE: "eye",
FACE: "face",
HOOD: "hood",
NOSE: "nose",
BELLY: "belly",
UNDERBELLY: "underbelly",
WING: "wing",
WING_EDGE: "wing-edge",
HEART: "heart",
HEART_BORDER: "heart-border",
HEART_SHINE: "heart-shine",
FEATHER_SPINE: "feather-spine",
};
/** @type {Record<string, string>} */
export const SPRITE_SHEET_COLOR_MAP = {
"transparent": Sprite.TRANSPARENT,
"#ffffff": Sprite.BORDER,
"#000000": Sprite.OUTLINE,
"#010a19": Sprite.BEAK,
"#190301": Sprite.EYE,
"#af8e75": Sprite.FOOT,
"#639bff": Sprite.FACE,
"#99e550": Sprite.HOOD,
"#d95763": Sprite.NOSE,
"#f8b143": Sprite.BELLY,
"#ec8637": Sprite.UNDERBELLY,
"#578ae6": Sprite.WING,
"#326ed9": Sprite.WING_EDGE,
"#c82e2e": Sprite.HEART,
"#501a1a": Sprite.HEART_BORDER,
"#ff6b6b": Sprite.HEART_SHINE,
"#373737": Sprite.FEATHER_SPINE,
};
export class BirdType {
/**
* @param {string} name
* @param {string} description
* @param {Record<string, string>} colors
* @param {string[]} [tags]
*/
constructor(name, description, colors, tags = []) {
this.name = name;
this.description = description;
const defaultColors = {
[Sprite.TRANSPARENT]: "transparent",
[Sprite.OUTLINE]: "#000000",
[Sprite.BORDER]: "#ffffff",
[Sprite.BEAK]: "#000000",
[Sprite.EYE]: "#000000",
[Sprite.HEART]: "#c82e2e",
[Sprite.HEART_BORDER]: "#501a1a",
[Sprite.HEART_SHINE]: "#ff6b6b",
[Sprite.FEATHER_SPINE]: "#373737",
[Sprite.HOOD]: colors.face,
[Sprite.NOSE]: colors.face,
};
/** @type {Record<string, string>} */
this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face };
this.tags = tags;
}
}
/** @type {Record<string, BirdType>} */
export const SPECIES = {
bluebird: new BirdType("Eastern Bluebird",
"Native to North American and very social, though can be timid around people.", {
[Sprite.FOOT]: "#af8e75",
[Sprite.FACE]: "#639bff",
[Sprite.BELLY]: "#f8b143",
[Sprite.UNDERBELLY]: "#ec8637",
[Sprite.WING]: "#578ae6",
[Sprite.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.", {
[Sprite.FOOT]: "#af8e75",
[Sprite.FACE]: "#ffffff",
[Sprite.BELLY]: "#ebe9e8",
[Sprite.UNDERBELLY]: "#ebd9d0",
[Sprite.WING]: "#f3d3c1",
[Sprite.WING_EDGE]: "#2d2d2dff",
[Sprite.THEME_HIGHLIGHT]: "#d7ac93",
}),
tuftedTitmouse: new BirdType("Tufted Titmouse",
"Native to the eastern United States, full of personality, and notably my wife's favorite bird.", {
[Sprite.FOOT]: "#af8e75",
[Sprite.FACE]: "#c7cad7",
[Sprite.BELLY]: "#e4e5eb",
[Sprite.UNDERBELLY]: "#d7cfcb",
[Sprite.WING]: "#b1b5c5",
[Sprite.WING_EDGE]: "#9d9fa9",
}, ["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.", {
[Sprite.FOOT]: "#af8e75",
[Sprite.FACE]: "#ffaf34",
[Sprite.HOOD]: "#aaa094",
[Sprite.BELLY]: "#ffaf34",
[Sprite.UNDERBELLY]: "#babec2",
[Sprite.WING]: "#aaa094",
[Sprite.WING_EDGE]: "#888580",
[Sprite.THEME_HIGHLIGHT]: "#ffaf34",
}),
redCardinal: new BirdType("Red Cardinal",
"Native to the eastern United States, this strikingly red bird is hard to miss.", {
[Sprite.BEAK]: "#d93619",
[Sprite.FOOT]: "#af8e75",
[Sprite.FACE]: "#31353d",
[Sprite.HOOD]: "#e83a1b",
[Sprite.BELLY]: "#e83a1b",
[Sprite.UNDERBELLY]: "#dc3719",
[Sprite.WING]: "#d23215",
[Sprite.WING_EDGE]: "#b1321c",
}, ["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.", {
[Sprite.BEAK]: "#ffaf34",
[Sprite.FOOT]: "#af8e75",
[Sprite.FACE]: "#fff255",
[Sprite.NOSE]: "#383838",
[Sprite.HOOD]: "#383838",
[Sprite.BELLY]: "#fff255",
[Sprite.UNDERBELLY]: "#f5ea63",
[Sprite.WING]: "#e8e079",
[Sprite.WING_EDGE]: "#191919",
[Sprite.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.", {
[Sprite.FOOT]: "#af8e75",
[Sprite.FACE]: "#db7c4d",
[Sprite.BELLY]: "#f7e1c9",
[Sprite.UNDERBELLY]: "#ebc9a3",
[Sprite.WING]: "#2252a9",
[Sprite.WING_EDGE]: "#1c448b",
[Sprite.HOOD]: "#2252a9",
}),
mistletoebird: new BirdType("Mistletoebird",
"Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", {
[Sprite.FOOT]: "#6c6a7c",
[Sprite.FACE]: "#352e6d",
[Sprite.BELLY]: "#fd6833",
[Sprite.UNDERBELLY]: "#e6e1d8",
[Sprite.WING]: "#342b7c",
[Sprite.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.", {
[Sprite.BEAK]: "#f71919",
[Sprite.FOOT]: "#af7575",
[Sprite.FACE]: "#cb092b",
[Sprite.BELLY]: "#ae1724",
[Sprite.UNDERBELLY]: "#831b24",
[Sprite.WING]: "#7e3030",
[Sprite.WING_EDGE]: "#490f0f",
}),
scarletRobin: new BirdType("Scarlet Robin",
"Native to Australia, this striking robin can be found in Eucalyptus forests.", {
[Sprite.FOOT]: "#494949",
[Sprite.FACE]: "#3d3d3d",
[Sprite.BELLY]: "#fc5633",
[Sprite.UNDERBELLY]: "#dcdcdc",
[Sprite.WING]: "#2b2b2b",
[Sprite.WING_EDGE]: "#ebebeb",
[Sprite.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.", {
[Sprite.BEAK]: "#e89f30",
[Sprite.FOOT]: "#9f8075",
[Sprite.FACE]: "#2d2d2d",
[Sprite.BELLY]: "#eb7a3a",
[Sprite.UNDERBELLY]: "#eb7a3a",
[Sprite.WING]: "#444444",
[Sprite.WING_EDGE]: "#232323",
[Sprite.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.", {
[Sprite.FOOT]: "#af8e75",
[Sprite.FACE]: "#edc7a9",
[Sprite.NOSE]: "#f7eee5",
[Sprite.HOOD]: "#c58a5b",
[Sprite.BELLY]: "#e1b796",
[Sprite.UNDERBELLY]: "#c79e7c",
[Sprite.WING]: "#c58a5b",
[Sprite.WING_EDGE]: "#866348",
}),
};

View File

@@ -1,9 +1,9 @@
import {
getContext,
makeElement,
makeDraggable,
makeClosable
} from './shared.js';
import { getContext } from './context.js';
/**
* @typedef {Object} SavedStickyNote

Some files were not shown because too many files have changed in this diff Show More