Compare commits
135 Commits
2025.11.14
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5797c055ed | ||
|
|
96ff61625a | ||
|
|
85ade65a57 | ||
|
|
2f3d7958ea | ||
|
|
eab6086f4d | ||
|
|
c770e3d1f6 | ||
|
|
0d007f8c1e | ||
|
|
92c083138d | ||
|
|
5d6ea50c87 | ||
|
|
77a29c549f | ||
|
|
a8ba15489f | ||
|
|
39e84be775 | ||
|
|
f924343ac3 | ||
|
|
fd55924025 | ||
|
|
f891f8f06d | ||
|
|
9aee4eab1a | ||
|
|
763b50f34b | ||
|
|
b4c577a0ac | ||
|
|
5a3b555d3a | ||
|
|
74a776cd4f | ||
|
|
ebb9f92be2 | ||
|
|
b7d6ca63c1 | ||
|
|
7d16459a76 | ||
|
|
abe4439d5e | ||
|
|
6f88d386ec | ||
|
|
86a14d6dca | ||
|
|
30b9c86cca | ||
|
|
3765713fd0 | ||
|
|
0f90eb4492 | ||
|
|
30d6c2fee5 | ||
|
|
18fa5e8683 | ||
|
|
c43dd4c7b4 | ||
|
|
9e6f5feae1 | ||
|
|
b6e93088a8 | ||
|
|
b440f633a5 | ||
|
|
7b20a376ce | ||
|
|
61fbe89986 | ||
|
|
c1511aae71 | ||
|
|
0a11ebe87d | ||
|
|
5cf96da868 | ||
|
|
5d99142b74 | ||
|
|
2a7ad229be | ||
|
|
c880b99744 | ||
|
|
fe0310cb36 | ||
|
|
efddf12ba5 | ||
|
|
7aa9996857 | ||
|
|
7f334d789f | ||
|
|
a57615b3da | ||
|
|
37a8b6cc6e | ||
|
|
31a3f7cac9 | ||
|
|
9fb0ab3f3f | ||
|
|
736d01e015 | ||
|
|
dd3ef01bef | ||
|
|
3e48360632 | ||
|
|
3eda5ffc92 | ||
|
|
6cfd32270c | ||
|
|
1d4c1a000e | ||
|
|
71b74c9b6f | ||
|
|
80bcf60a07 | ||
|
|
a2dea8a17d | ||
|
|
fd09a35b51 | ||
|
|
11ea3c012b | ||
|
|
1bf82dfbad | ||
|
|
b04edbc2c5 | ||
|
|
927b287f98 | ||
|
|
45743d2caf | ||
|
|
953d2cde47 | ||
|
|
6309aed971 | ||
|
|
ea85c61955 | ||
|
|
cd06a886bd | ||
|
|
868cd06210 | ||
|
|
307d4a8895 | ||
|
|
5a33cef4d5 | ||
|
|
7b4ebf7ab8 | ||
|
|
e393013b27 | ||
|
|
db1a3dcbb6 | ||
|
|
8cd93bb623 | ||
|
|
a3a09c6819 | ||
|
|
912327a348 | ||
|
|
d54f208cc4 | ||
|
|
cb1f2f605f | ||
|
|
2ee6ea84a7 | ||
|
|
5e04727a1b | ||
|
|
7b1df9bc4f | ||
|
|
130fae6e0c | ||
|
|
3b2081943d | ||
|
|
f5742ac3a7 | ||
|
|
867d214292 | ||
|
|
d97e39449e | ||
|
|
7628ee2c87 | ||
|
|
3227167cb5 | ||
|
|
e0fae3781a | ||
|
|
2773538a6c | ||
|
|
2a90a56a2b | ||
|
|
94454a2338 | ||
|
|
cf968dfec4 | ||
|
|
4838457054 | ||
|
|
e09d4f9eea | ||
|
|
7c38bf9164 | ||
|
|
8263fadfba | ||
|
|
9f7d864e57 | ||
|
|
579967a302 | ||
|
|
ca1495a9f1 | ||
|
|
fd865cacb8 | ||
|
|
5e94998410 | ||
|
|
e13a67e967 | ||
|
|
e1759bc235 | ||
|
|
1d818d83cf | ||
|
|
47b418324c | ||
|
|
e5956426d5 | ||
|
|
0cc06a8856 | ||
|
|
dd4184f642 | ||
|
|
5a82ba858f | ||
|
|
b8de14bb94 | ||
|
|
e0ab21d608 | ||
|
|
86c254b2cb | ||
|
|
14ef2713d8 | ||
|
|
bc3f5cce02 | ||
|
|
c3ff3b39dc | ||
|
|
8b8aa50cae | ||
|
|
f4b598d2dc | ||
|
|
bccaa0aa15 | ||
|
|
37a30ea509 | ||
|
|
e7be2b7661 | ||
|
|
c750bf5560 | ||
|
|
c927ce23e4 | ||
|
|
6ee9efd5a8 | ||
|
|
a5e81e4265 | ||
|
|
76e55a3caa | ||
|
|
224fe4aaec | ||
|
|
764e8311fa | ||
|
|
b66521b7a2 | ||
|
|
72f2805fa0 | ||
|
|
1ca5094536 | ||
|
|
2ddf10bb01 |
@@ -1 +0,0 @@
|
|||||||
src/
|
|
||||||
5
.gitignore
vendored
@@ -3,3 +3,8 @@
|
|||||||
/dist/birb.bundled.js
|
/dist/birb.bundled.js
|
||||||
obsidian-test.sh
|
obsidian-test.sh
|
||||||
build-cache.json
|
build-cache.json
|
||||||
|
.vscode/settings.json
|
||||||
|
aseprite/birb-test.aseprite
|
||||||
|
aseprite/wren.aseprite
|
||||||
|
aseprite/birb-no-shoulder.aseprite
|
||||||
|
aseprite/birb-fat.aseprite
|
||||||
|
|||||||
31
README.md
@@ -13,8 +13,14 @@ It's a pet bird that hops around your computer, what more could you want?
|
|||||||
|
|
||||||
### Get it for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/pocket-bird/)
|
### 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!
|
#### Join the [Discord](https://discord.gg/6yxE9prcNc) to help me beta test new features and suggest ideas!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- A cute little pixel art bird hops around your apps and websites
|
- A cute little pixel art bird hops around your apps and websites
|
||||||
@@ -37,7 +43,11 @@ It's a pet bird that hops around your computer, what more could you want?
|
|||||||
|
|
||||||
### Obsidian
|
### Obsidian
|
||||||
|
|
||||||
_Coming soon!_
|
1. Install the [Beta Plugin Manager (BRAT)](https://obsidian.md/plugins?id=obsidian42-brat) plugin for Obsidian
|
||||||
|
2. Enable the BRAT plugin and open its settings
|
||||||
|
3. In the BRAT settings, click "Add Beta Plugin" and enter the following URL: `https://github.com/IdreesInc/PB-Obsidian-Releases`
|
||||||
|
4. Select "Latest version" and click "Add Plugin"
|
||||||
|
5. Enjoy a pet bird in your Obsidian notes!
|
||||||
|
|
||||||
### Userscript
|
### Userscript
|
||||||
|
|
||||||
@@ -48,6 +58,14 @@ _Coming soon!_
|
|||||||
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)
|
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!
|
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
|
## FAQ
|
||||||
|
|
||||||
### How do I pet the bird?
|
### How do I pet the bird?
|
||||||
@@ -74,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!
|
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
|
## 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!
|
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!
|
||||||
|
|||||||
BIN
aseprite/hats.aseprite
Normal file
206
build.js
@@ -12,25 +12,30 @@ const IMAGES_DIR = "./images";
|
|||||||
const FONTS_DIR = "./fonts";
|
const FONTS_DIR = "./fonts";
|
||||||
const DIST_DIR = "./dist";
|
const DIST_DIR = "./dist";
|
||||||
|
|
||||||
const BROWSER_MANIFEST = "./platform-specific/extension/manifest.json";
|
const WEB_DIR = DIST_DIR + "/web";
|
||||||
const OBSIDIAN_MANIFEST = "./platform-specific/obsidian/manifest.json";
|
|
||||||
const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt";
|
|
||||||
const OBSIDIAN_WRAPPER = "./platform-specific/obsidian/wrapper.js";
|
|
||||||
|
|
||||||
const USERSCRIPT_DIR = DIST_DIR + "/userscript";
|
const USERSCRIPT_DIR = DIST_DIR + "/userscript";
|
||||||
const EXTENSION_DIR = DIST_DIR + "/extension";
|
const EXTENSION_DIR = DIST_DIR + "/extension";
|
||||||
const OBSIDIAN_DIR = DIST_DIR + "/obsidian";
|
const OBSIDIAN_DIR = DIST_DIR + "/obsidian";
|
||||||
|
|
||||||
const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css";
|
const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css";
|
||||||
const APPLICATION_ENTRY = SRC_DIR + "/application.js";
|
|
||||||
const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js";
|
const WEB_ENTRY = SRC_DIR + "/platforms/web/web.js";
|
||||||
const BIRB_OUTPUT = DIST_DIR + "/birb.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 MONOCRAFT_URL = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
|
||||||
|
|
||||||
const VERSION_KEY = "__VERSION__";
|
const VERSION_KEY = "__VERSION__";
|
||||||
const STYLESHEET_KEY = "___STYLESHEET___";
|
const STYLESHEET_KEY = "___STYLESHEET___";
|
||||||
const MONOCRAFT_SRC_KEY = "__MONOCRAFT_SRC__";
|
const MONOCRAFT_URL_KEY = "__MONOCRAFT_URL__";
|
||||||
const CODE_KEY = "__CODE__";
|
const CODE_KEY = "__CODE__";
|
||||||
|
|
||||||
const spriteSheets = [
|
const spriteSheets = [
|
||||||
@@ -41,6 +46,10 @@ const spriteSheets = [
|
|||||||
{
|
{
|
||||||
key: "__FEATHER_SPRITE_SHEET__",
|
key: "__FEATHER_SPRITE_SHEET__",
|
||||||
path: SPRITES_DIR + "/feather.png"
|
path: SPRITES_DIR + "/feather.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "__HATS_SPRITE_SHEET__",
|
||||||
|
path: SPRITES_DIR + "/hats.png"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -67,119 +76,142 @@ if (buildCache.version && buildCache.version.startsWith(versionDate)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = `${versionDate}.${buildNumber}`;
|
// const version = `${versionDate}.${buildNumber}`;
|
||||||
|
const version = `${versionDate}`; // Disable build number for now
|
||||||
|
|
||||||
// Update build cache
|
// Update build cache
|
||||||
buildCache.version = version;
|
buildCache.version = version;
|
||||||
writeFileSync(BUILD_CACHE_PATH, JSON.stringify(buildCache), 'utf8');
|
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
|
await bundle.write({
|
||||||
const bundle = await rollup({
|
file: TEMP_BUNDLED_OUTPUT,
|
||||||
input: APPLICATION_ENTRY,
|
|
||||||
});
|
|
||||||
|
|
||||||
await bundle.write({
|
|
||||||
file: BUNDLED_OUTPUT,
|
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
});
|
});
|
||||||
|
|
||||||
await bundle.close();
|
await bundle.close();
|
||||||
|
|
||||||
let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8');
|
let birbJs = readFileSync(TEMP_BUNDLED_OUTPUT, 'utf8');
|
||||||
|
|
||||||
// Delete bundled file
|
// Delete bundled file
|
||||||
unlinkSync(BUNDLED_OUTPUT);
|
unlinkSync(TEMP_BUNDLED_OUTPUT);
|
||||||
|
|
||||||
// Replace version placeholder
|
// Replace version placeholder
|
||||||
birbJs = birbJs.replaceAll(VERSION_KEY, version);
|
birbJs = birbJs.replaceAll(VERSION_KEY, version);
|
||||||
|
|
||||||
// Compile and insert sprite sheets
|
// Replace CDN font URL placeholder
|
||||||
for (const spriteSheet of spriteSheets) {
|
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');
|
const dataUri = readFileSync(spriteSheet.path, 'base64');
|
||||||
birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`);
|
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
|
async function buildWeb() {
|
||||||
const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8');
|
const birbJs = await generateCode(WEB_ENTRY);
|
||||||
birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent).replace(MONOCRAFT_SRC_KEY, MONOCRAFT_URL);
|
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
|
// Get userscript header
|
||||||
writeFileSync(BIRB_OUTPUT, birbJs);
|
const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version);
|
||||||
|
|
||||||
// =============================================
|
mkdirSync(USERSCRIPT_DIR, { recursive: true });
|
||||||
// Build userscript
|
const userScript = userScriptHeader + "\n" + birbJs;
|
||||||
// =============================================
|
writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript);
|
||||||
|
}
|
||||||
|
|
||||||
// Get userscript header
|
async function buildExtension() {
|
||||||
const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version);
|
const birbJs = await generateCode(BROWSER_EXTENSION_ENTRY);
|
||||||
|
|
||||||
mkdirSync(USERSCRIPT_DIR, { recursive: true });
|
mkdirSync(EXTENSION_DIR, { recursive: true });
|
||||||
const userScript = userScriptHeader + "\n" + birbJs;
|
|
||||||
writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript);
|
|
||||||
|
|
||||||
// =============================================
|
// Copy birb.js
|
||||||
// Build browser extension
|
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
|
// Copy icons folder
|
||||||
writeFileSync(EXTENSION_DIR + '/birb.js', birbJs);
|
mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true });
|
||||||
|
cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true });
|
||||||
|
|
||||||
// Copy manifest.json
|
// Copy fonts folder
|
||||||
let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8');
|
mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true });
|
||||||
browserManifest = browserManifest.replace(VERSION_KEY, version);
|
cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true });
|
||||||
writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest);
|
|
||||||
|
|
||||||
// Copy icons folder
|
// Compress extension folder into zip
|
||||||
mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true });
|
const output = createWriteStream(DIST_DIR + "/extension.zip");
|
||||||
cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true });
|
const archive = archiver('zip');
|
||||||
|
|
||||||
// Copy fonts folder
|
output.on('close', () => {
|
||||||
mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true });
|
|
||||||
cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true });
|
|
||||||
|
|
||||||
// Compress extension folder into zip
|
|
||||||
const output = createWriteStream(DIST_DIR + "/extension.zip");
|
|
||||||
const archive = archiver('zip');
|
|
||||||
|
|
||||||
output.on('close', () => {
|
|
||||||
console.log(`Created zip file: ${archive.pointer()} total bytes`);
|
console.log(`Created zip file: ${archive.pointer()} total bytes`);
|
||||||
});
|
});
|
||||||
|
|
||||||
archive.on('error', (err) => {
|
archive.on('error', (err) => {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
archive.pipe(output);
|
archive.pipe(output);
|
||||||
archive.directory(EXTENSION_DIR + '/', false);
|
archive.directory(EXTENSION_DIR + '/', false);
|
||||||
archive.finalize();
|
archive.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================
|
async function buildObsidian() {
|
||||||
// Build Obsidian plugin
|
// embedFont=true: bakes the font as base64 so the plugin works fully offline.
|
||||||
// =============================================
|
const birbJs = await generateCode(OBSIDIAN_ENTRY, true);
|
||||||
|
|
||||||
mkdirSync(OBSIDIAN_DIR, { recursive: true });
|
mkdirSync(OBSIDIAN_DIR, { recursive: true });
|
||||||
|
|
||||||
// Wrap birb.js with plugin boilerplate
|
// Wrap birb.js with plugin boilerplate
|
||||||
let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs);
|
let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs);
|
||||||
|
|
||||||
// Encode font to data URI since Obsidian plugins can't have external font files
|
// Create main.js with plugin code
|
||||||
const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64');
|
writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin);
|
||||||
const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`;
|
|
||||||
obsidianPlugin = obsidianPlugin.replace(MONOCRAFT_URL, monocraftDataUri);
|
|
||||||
|
|
||||||
// Create main.js with plugin code
|
// Copy manifest.json
|
||||||
writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin);
|
let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8');
|
||||||
|
obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`);
|
||||||
|
writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest);
|
||||||
|
}
|
||||||
|
|
||||||
// Copy manifest.json
|
console.log("Starting build...");
|
||||||
let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8');
|
|
||||||
obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`);
|
|
||||||
writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest);
|
|
||||||
|
|
||||||
console.log(`Build complete: ${version}`);
|
await buildWeb();
|
||||||
|
await buildUserscript();
|
||||||
|
await buildExtension();
|
||||||
|
await buildObsidian();
|
||||||
|
|
||||||
|
console.log("Build completed successfully!");
|
||||||
BIN
dist/extension.zip
vendored
2265
dist/extension/birb.js
vendored
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
dist/extension/images/icons/transparent/16x16x1.png
vendored
|
Before Width: | Height: | Size: 635 B After Width: | Height: | Size: 662 B |
BIN
dist/extension/images/icons/transparent/16x16x2.png
vendored
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
BIN
dist/extension/images/icons/transparent/27x20x2.png
vendored
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 881 B |
BIN
dist/extension/images/icons/transparent/27x20x3.png
vendored
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 996 B |
BIN
dist/extension/images/icons/transparent/29x29x2.png
vendored
|
Before Width: | Height: | Size: 914 B After Width: | Height: | Size: 933 B |
BIN
dist/extension/images/icons/transparent/29x29x3.png
vendored
|
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1.0 KiB |
BIN
dist/extension/images/icons/transparent/32x24x2.png
vendored
|
Before Width: | Height: | Size: 881 B After Width: | Height: | Size: 933 B |
BIN
dist/extension/images/icons/transparent/32x24x3.png
vendored
|
Before Width: | Height: | Size: 953 B After Width: | Height: | Size: 1008 B |
BIN
dist/extension/images/icons/transparent/32x32x1.png
vendored
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 848 B |
BIN
dist/extension/images/icons/transparent/32x32x2.png
vendored
|
Before Width: | Height: | Size: 936 B After Width: | Height: | Size: 957 B |
BIN
dist/extension/images/icons/transparent/48x48x1.png
vendored
|
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 866 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
BIN
dist/extension/images/icons/transparent/60x45x2.png
vendored
|
Before Width: | Height: | Size: 1014 B After Width: | Height: | Size: 1.0 KiB |
BIN
dist/extension/images/icons/transparent/60x45x3.png
vendored
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
dist/extension/images/icons/transparent/67x50x2.png
vendored
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
dist/extension/images/icons/transparent/74x55x2.png
vendored
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
dist/extension/images/icons/transparent/96x96x1.png
vendored
|
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
2
dist/extension/manifest.json
vendored
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Pocket Bird",
|
"name": "Pocket Bird",
|
||||||
"description": "It's a pet bird in your browser, what more could you want?",
|
"description": "It's a pet bird in your browser, what more could you want?",
|
||||||
"version": "2025.11.14.205",
|
"version": "2026.4.6",
|
||||||
"homepage_url": "https://idreesinc.com",
|
"homepage_url": "https://idreesinc.com",
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "images/icons/transparent/48x48x1.png",
|
"48": "images/icons/transparent/48x48x1.png",
|
||||||
|
|||||||
2229
dist/obsidian/main.js
vendored
4
dist/obsidian/manifest.json
vendored
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"id": "pocket-bird",
|
"id": "pocket-bird",
|
||||||
"name": "Pocket Bird",
|
"name": "Pocket Bird",
|
||||||
"version": "2025.11.14.205",
|
"version": "2026.4.6",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "It's a pet bird in your Obsidian, what more could you want?",
|
"description": "Add a pet bird to fly around your notes and keep you company!",
|
||||||
"author": "Idrees Hassan",
|
"author": "Idrees Hassan",
|
||||||
"authorUrl": "https://idreesinc.com",
|
"authorUrl": "https://idreesinc.com",
|
||||||
"isDesktopOnly": false
|
"isDesktopOnly": false
|
||||||
|
|||||||
2272
dist/userscript/birb.user.js
vendored
3926
dist/web/birb.embed.js
vendored
Normal file
2284
dist/birb.js → dist/web/birb.js
vendored
380
editor/editor.js
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { SPRITE_SHEET_COLOR_MAP, PALETTE, DEFAULT_COLOR_OVERRIDES, loadSpriteSheetPixels } from '../src/animation/sprites.js';
|
||||||
|
import Layer, { TAG } from '../src/animation/layer.js';
|
||||||
|
import Frame from '../src/animation/frame.js';
|
||||||
|
import { Directions, getLayerPixels } from '../src/shared.js';
|
||||||
|
import species from '../src/species.js';
|
||||||
|
|
||||||
|
/** @typedef {import('../src/species.js').Species} Species */
|
||||||
|
|
||||||
|
const COLOR_MAP = SPRITE_SHEET_COLOR_MAP;
|
||||||
|
const SPRITE_PATH = "../sprites/birb.png";
|
||||||
|
const SPRITE_SIZE = 32;
|
||||||
|
const IGNORED_PARTS = new Set(
|
||||||
|
["transparent", "border", "heart", "heart-border", "heart-shine", "feather-spine"]
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @type {HTMLCanvasElement} */
|
||||||
|
// @ts-ignore
|
||||||
|
const canvas = document.getElementById("preview");
|
||||||
|
/** @type {CanvasRenderingContext2D} */
|
||||||
|
// @ts-ignore
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
// @ts-ignore
|
||||||
|
const editor = document.getElementById("editor");
|
||||||
|
const colorPickerInput = document.createElement("input");
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
// @ts-ignore
|
||||||
|
const jsonElement = document.getElementById("json");
|
||||||
|
/** @type {Record<string, HTMLElement>} */
|
||||||
|
const colorElements = {};
|
||||||
|
/** @type {string|null} */
|
||||||
|
let selectedPart = null;
|
||||||
|
/** @type {HTMLElement|null} */
|
||||||
|
let selectedColorElement = null;
|
||||||
|
|
||||||
|
const spriteCanvas = document.createElement('canvas');
|
||||||
|
spriteCanvas.width = canvas.width;
|
||||||
|
spriteCanvas.height = canvas.height;
|
||||||
|
/** @type {CanvasRenderingContext2D} */
|
||||||
|
// @ts-ignore
|
||||||
|
const spriteCtx = spriteCanvas.getContext('2d');
|
||||||
|
|
||||||
|
/** @type {Species} */
|
||||||
|
let currentSpecies = JSON.parse(JSON.stringify(species.bluebird));
|
||||||
|
let speciesHistory = [JSON.parse(JSON.stringify(currentSpecies))];
|
||||||
|
let historyIndex = 0;
|
||||||
|
/** @type {Frame|null} */
|
||||||
|
let baseFrame = null;
|
||||||
|
|
||||||
|
function drawBackground() {
|
||||||
|
const patternSize = 2;
|
||||||
|
const colors = ["#edf0f4", "#dadbe0"];
|
||||||
|
for (let y = 0; y < canvas.height; y += patternSize) {
|
||||||
|
for (let x = 0; x < canvas.width; x += patternSize) {
|
||||||
|
ctx.fillStyle = ((x / patternSize + y / patternSize) % 2 === 0) ? colors[0] : colors[1];
|
||||||
|
ctx.fillRect(x, y, patternSize, patternSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the full palette color scheme from the current species settings
|
||||||
|
* @returns {Record<string, string>}
|
||||||
|
*/
|
||||||
|
function buildColorScheme() {
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
const scheme = {};
|
||||||
|
for (const paletteName of Object.values(PALETTE)) {
|
||||||
|
scheme[paletteName] = getColor(paletteName);
|
||||||
|
}
|
||||||
|
return scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
if (!baseFrame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawBackground();
|
||||||
|
baseFrame.draw(spriteCtx, Directions.LEFT, 1, buildColorScheme(), currentSpecies.tags || []);
|
||||||
|
ctx.drawImage(spriteCanvas, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitChange() {
|
||||||
|
const previousSpecies = speciesHistory[historyIndex];
|
||||||
|
let changed = false;
|
||||||
|
// Check for changes in colors
|
||||||
|
for (const part of Object.keys(currentSpecies.colors)) {
|
||||||
|
if (currentSpecies.colors[part] !== previousSpecies.colors[part]) {
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changed) {
|
||||||
|
for (const part of Object.keys(previousSpecies.colors)) {
|
||||||
|
if (!(part in currentSpecies.colors)) {
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check for changes in tags
|
||||||
|
if (!changed) {
|
||||||
|
const prevTags = new Set(previousSpecies.tags || []);
|
||||||
|
const currTags = new Set(currentSpecies.tags || []);
|
||||||
|
for (const tag of prevTags) {
|
||||||
|
if (!currTags.has(tag)) {
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changed) {
|
||||||
|
for (const tag of currentSpecies.tags || []) {
|
||||||
|
if (!previousSpecies.tags || !previousSpecies.tags.includes(tag)) {
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
speciesHistory = speciesHistory.slice(0, historyIndex + 1);
|
||||||
|
speciesHistory.push(JSON.parse(JSON.stringify(currentSpecies)));
|
||||||
|
historyIndex++;
|
||||||
|
localStorage.setItem("speciesHistory", JSON.stringify(speciesHistory));
|
||||||
|
}
|
||||||
|
updateJson();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEditor() {
|
||||||
|
for (const [color, part] of Object.entries(COLOR_MAP)) {
|
||||||
|
if (IGNORED_PARTS.has(part)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const item = createColorSwatch(part, getColor(part) || color);
|
||||||
|
editor.appendChild(item);
|
||||||
|
}
|
||||||
|
for (const value of Object.values(TAG)) {
|
||||||
|
if (value === TAG.DEFAULT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
editor.appendChild(createTagToggle(value, getTag(value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} part
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
function getColor(part) {
|
||||||
|
if (currentSpecies.colors[part]) {
|
||||||
|
return currentSpecies.colors[part];
|
||||||
|
}
|
||||||
|
const override = DEFAULT_COLOR_OVERRIDES[/** @type {keyof typeof DEFAULT_COLOR_OVERRIDES} */ (part)];
|
||||||
|
if (override) {
|
||||||
|
return getColor(override);
|
||||||
|
}
|
||||||
|
for (const [color, partName] of Object.entries(COLOR_MAP)) {
|
||||||
|
if (partName === part) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "transparent";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} tag
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function getTag(tag) {
|
||||||
|
return currentSpecies.tags ? currentSpecies.tags.includes(tag) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} tag
|
||||||
|
* @param {boolean} enabled
|
||||||
|
*/
|
||||||
|
function setTag(tag, enabled) {
|
||||||
|
if (!currentSpecies.tags) {
|
||||||
|
currentSpecies.tags = [];
|
||||||
|
}
|
||||||
|
if (enabled) {
|
||||||
|
if (!currentSpecies.tags.includes(tag)) {
|
||||||
|
currentSpecies.tags.push(tag);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentSpecies.tags = currentSpecies.tags.filter(t => t !== tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createColorPicker() {
|
||||||
|
colorPickerInput.type = "text";
|
||||||
|
colorPickerInput.id = "color-picker-interceptor";
|
||||||
|
colorPickerInput.setAttribute("data-coloris", "");
|
||||||
|
document.body.appendChild(colorPickerInput);
|
||||||
|
|
||||||
|
colorPickerInput.addEventListener("input", () => {
|
||||||
|
if (selectedColorElement && selectedPart !== null) {
|
||||||
|
const newColor = colorPickerInput.value;
|
||||||
|
selectedColorElement.style.backgroundColor = newColor;
|
||||||
|
currentSpecies.colors[selectedPart] = newColor;
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("mouseup", () => {
|
||||||
|
if (selectedPart !== null && !jsonElement.contains(document.activeElement)) {
|
||||||
|
commitChange();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} label
|
||||||
|
* @param {string} color
|
||||||
|
* @returns {HTMLDivElement}
|
||||||
|
*/
|
||||||
|
function createColorSwatch(label, color) {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.classList.add("editor-item");
|
||||||
|
|
||||||
|
const colorElement = document.createElement("div");
|
||||||
|
colorElement.classList.add("color");
|
||||||
|
colorElement.style.backgroundColor = color;
|
||||||
|
colorElements[label] = colorElement;
|
||||||
|
item.appendChild(colorElement);
|
||||||
|
if (color !== "transparent") {
|
||||||
|
colorElement.addEventListener("click", () => {
|
||||||
|
selectedPart = label;
|
||||||
|
selectedColorElement = colorElement;
|
||||||
|
const rect = colorElement.getBoundingClientRect();
|
||||||
|
colorPickerInput.style.left = rect.left + "px";
|
||||||
|
colorPickerInput.style.top = (rect.bottom + window.scrollY) + "px";
|
||||||
|
|
||||||
|
colorPickerInput.value = currentSpecies.colors[label] || color;
|
||||||
|
colorPickerInput.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
colorElement.classList.add("color--transparent");
|
||||||
|
}
|
||||||
|
const labelElement = document.createElement("div");
|
||||||
|
const labelText = label.replaceAll("-", " ").toUpperCase();
|
||||||
|
labelElement.classList.add("label");
|
||||||
|
labelElement.textContent = labelText;
|
||||||
|
labelElement.title = "Click to remove from species";
|
||||||
|
labelElement.addEventListener("click", () => {
|
||||||
|
delete currentSpecies.colors[label];
|
||||||
|
colorElement.style.backgroundColor = getColor(label);
|
||||||
|
commitChange();
|
||||||
|
refreshEditor();
|
||||||
|
});
|
||||||
|
item.appendChild(labelElement);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} tag
|
||||||
|
* @param {boolean} enabled
|
||||||
|
* @returns {HTMLDivElement}
|
||||||
|
*/
|
||||||
|
function createTagToggle(tag, enabled) {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.classList.add("editor-item");
|
||||||
|
|
||||||
|
const toggle = document.createElement("button");
|
||||||
|
toggle.id = `tag-toggle-${tag}`;
|
||||||
|
toggle.classList.add("tag-toggle");
|
||||||
|
toggle.textContent = "✓";
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
setTag(tag, !getTag(tag));
|
||||||
|
toggle.classList.toggle("tag-toggle--active", getTag(tag));
|
||||||
|
commitChange();
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
item.appendChild(toggle);
|
||||||
|
|
||||||
|
const labelElement = document.createElement("div");
|
||||||
|
labelElement.classList.add("label");
|
||||||
|
labelElement.textContent = tag.toUpperCase();
|
||||||
|
item.appendChild(labelElement);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshEditor() {
|
||||||
|
for (const [, part] of Object.entries(COLOR_MAP)) {
|
||||||
|
const el = colorElements[part];
|
||||||
|
if (el && !el.classList.contains("color--transparent")) {
|
||||||
|
el.style.backgroundColor = getColor(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectedColorElement && selectedPart !== null) {
|
||||||
|
colorPickerInput.value = currentSpecies.colors[selectedPart] || "";
|
||||||
|
}
|
||||||
|
for (const value of Object.values(TAG)) {
|
||||||
|
const toggle = editor.querySelector(`#tag-toggle-${value}`);
|
||||||
|
if (toggle && toggle instanceof HTMLElement) {
|
||||||
|
toggle.classList.toggle("tag-toggle--active", getTag(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateJson() {
|
||||||
|
jsonElement.textContent = JSON.stringify(currentSpecies, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (!(e.metaKey || e.ctrlKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "z" && !e.shiftKey) {
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex--;
|
||||||
|
currentSpecies = JSON.parse(JSON.stringify(speciesHistory[historyIndex]));
|
||||||
|
refreshEditor();
|
||||||
|
updateJson();
|
||||||
|
draw();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if ((e.key === "z" && e.shiftKey) || e.key === "y") {
|
||||||
|
if (historyIndex < speciesHistory.length - 1) {
|
||||||
|
historyIndex++;
|
||||||
|
currentSpecies = JSON.parse(JSON.stringify(speciesHistory[historyIndex]));
|
||||||
|
refreshEditor();
|
||||||
|
updateJson();
|
||||||
|
draw();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jsonElement.addEventListener("input", () => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonElement.textContent || "");
|
||||||
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
||||||
|
currentSpecies = parsed;
|
||||||
|
refreshEditor();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jsonElement.addEventListener("blur", () => {
|
||||||
|
commitChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadSpeciesHistory() {
|
||||||
|
const storedHistory = localStorage.getItem("speciesHistory");
|
||||||
|
if (storedHistory) {
|
||||||
|
try {
|
||||||
|
const parsedHistory = JSON.parse(storedHistory);
|
||||||
|
if (Array.isArray(parsedHistory) && parsedHistory.length > 0) {
|
||||||
|
speciesHistory = parsedHistory;
|
||||||
|
currentSpecies = JSON.parse(JSON.stringify(speciesHistory[speciesHistory.length - 1]));
|
||||||
|
historyIndex = speciesHistory.length - 1;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to parse species history from localStorage:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshEditor();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
createColorPicker();
|
||||||
|
loadEditor();
|
||||||
|
loadSpeciesHistory();
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const pixels = await loadSpriteSheetPixels(SPRITE_PATH);
|
||||||
|
baseFrame = new Frame([
|
||||||
|
new Layer(getLayerPixels(pixels, 0, SPRITE_SIZE)),
|
||||||
|
new Layer(getLayerPixels(pixels, 5, SPRITE_SIZE), TAG.TUFT),
|
||||||
|
]);
|
||||||
|
updateJson();
|
||||||
|
draw();
|
||||||
|
})();
|
||||||
24
editor/index.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Birb Editor</title>
|
||||||
|
<link rel="stylesheet" href="stylesheet.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.css"/>
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="horizontal-container">
|
||||||
|
<canvas id="preview" width="32px" height="32px"></canvas>
|
||||||
|
<div id="editor"></div>
|
||||||
|
<pre id="json" contenteditable="true"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="editor.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
143
editor/stylesheet.css
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css);
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: linear-gradient(to top, #D2DAE9, white);
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preview {
|
||||||
|
width: 480px;
|
||||||
|
height: 480px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
filter: drop-shadow(0px 0px 40px rgba(0, 0, 0, 0.1));
|
||||||
|
box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor {
|
||||||
|
width: 460px;
|
||||||
|
height: 480px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 40px 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 30px 20px;
|
||||||
|
column-count: 2;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
#json {
|
||||||
|
width: 200px;
|
||||||
|
height: 480px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 20px;
|
||||||
|
gap: 20px;
|
||||||
|
text-align: left;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: 'Fira Code', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 18px;
|
||||||
|
font-family: 'Fira Code', monospace;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
background: red;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
transition: 0.1s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color--transparent {
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color-picker-interceptor {
|
||||||
|
position: fixed;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-toggle {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
background: #f1f1f1;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
color: transparent;
|
||||||
|
transition: background 0.15s, color 0.15s, transform 0.1s;
|
||||||
|
border: 3px solid #dadada;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-toggle:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
transition: 0.1s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-toggle--active {
|
||||||
|
background: #34c85a;
|
||||||
|
color: white;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 635 B After Width: | Height: | Size: 662 B |
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 881 B |
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 996 B |
|
Before Width: | Height: | Size: 914 B After Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 881 B After Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 953 B After Width: | Height: | Size: 1008 B |
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 936 B After Width: | Height: | Size: 957 B |
|
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 866 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 1014 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "pocket-bird",
|
|
||||||
"name": "Pocket Bird",
|
|
||||||
"version": "__VERSION__",
|
|
||||||
"minAppVersion": "0.15.0",
|
|
||||||
"description": "It's a pet bird in your Obsidian, what more could you want?",
|
|
||||||
"author": "Idrees Hassan",
|
|
||||||
"authorUrl": "https://idreesinc.com",
|
|
||||||
"isDesktopOnly": false
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="spacer"></div>
|
<div id="spacer"></div>
|
||||||
<!-- <script type="module" src="spritesheet-compiler.js"></script> -->
|
<!-- <script type="module" src="spritesheet-compiler.js"></script> -->
|
||||||
<script src="../dist/birb.js"></script>
|
<script src="../dist/web/birb.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
BIN
sprites/birb.png
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
sprites/hats.png
Normal file
|
After Width: | Height: | Size: 949 B |
76
src/Frame.js
@@ -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;
|
|
||||||
12
src/Layer.js
@@ -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;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Frame from "./frame.js";
|
import Frame from "./frame.js";
|
||||||
import { BirdType } from "./sprites";
|
import { BirdType } from "./sprites.js";
|
||||||
|
|
||||||
class Anim {
|
class Anim {
|
||||||
/**
|
/**
|
||||||
@@ -59,10 +59,11 @@ class Anim {
|
|||||||
* @param {number} direction
|
* @param {number} direction
|
||||||
* @param {number} timeStart The start time of the animation in milliseconds
|
* @param {number} timeStart The start time of the animation in milliseconds
|
||||||
* @param {number} canvasPixelSize The size of a canvas pixel in pixels
|
* @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
|
* @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
|
// Reset cache if animation was restarted
|
||||||
if (this.lastTimeStart !== timeStart) {
|
if (this.lastTimeStart !== timeStart) {
|
||||||
this.#clearCache();
|
this.#clearCache();
|
||||||
@@ -79,7 +80,7 @@ class Anim {
|
|||||||
const currentFrameIndex = this.getCurrentFrameIndex(time);
|
const currentFrameIndex = this.getCurrentFrameIndex(time);
|
||||||
|
|
||||||
if (this.#shouldRedraw(currentFrameIndex, direction)) {
|
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.lastFrameIndex = currentFrameIndex;
|
||||||
this.lastDirection = direction;
|
this.lastDirection = direction;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Directions } from './shared.js';
|
import { Directions } from '../shared.js';
|
||||||
import { Sprite, BirdType } from './sprites.js';
|
import { PALETTE, BirdType } from './sprites.js';
|
||||||
import Layer from './layer.js';
|
import Layer, { TAG } from './layer.js';
|
||||||
|
|
||||||
class Frame {
|
class Frame {
|
||||||
|
|
||||||
@@ -16,25 +16,25 @@ class Frame {
|
|||||||
for (let layer of layers) {
|
for (let layer of layers) {
|
||||||
tags.add(layer.tag);
|
tags.add(layer.tag);
|
||||||
}
|
}
|
||||||
tags.add("default");
|
tags.add(TAG.DEFAULT);
|
||||||
for (let tag of tags) {
|
for (let tag of tags) {
|
||||||
let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0);
|
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");
|
throw new Error("First layer must have the 'default' tag");
|
||||||
}
|
}
|
||||||
this.pixels = layers[0].pixels.map(row => row.slice());
|
this.pixels = layers[0].pixels.map(row => row.slice());
|
||||||
// Pad from top with transparent pixels
|
// Pad from top with transparent pixels
|
||||||
while (this.pixels.length < maxHeight) {
|
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
|
// Combine layers
|
||||||
for (let i = 1; i < layers.length; i++) {
|
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 layerPixels = layers[i].pixels;
|
||||||
let topMargin = maxHeight - layerPixels.length;
|
let topMargin = maxHeight - layerPixels.length;
|
||||||
for (let y = 0; y < layerPixels.length; y++) {
|
for (let y = 0; y < layerPixels.length; y++) {
|
||||||
for (let x = 0; x < layerPixels[y].length; x++) {
|
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[][]}
|
* @returns {string[][]}
|
||||||
*/
|
*/
|
||||||
getPixels(tag = "default") {
|
getPixels(tags = [TAG.DEFAULT]) {
|
||||||
return this.#pixelsByTag[tag] ?? this.#pixelsByTag["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 {CanvasRenderingContext2D} ctx
|
||||||
* @param {BirdType} [species]
|
|
||||||
* @param {number} direction
|
* @param {number} direction
|
||||||
* @param {number} canvasPixelSize
|
* @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
|
// Clear the canvas before drawing the new frame
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
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++) {
|
for (let y = 0; y < pixels.length; y++) {
|
||||||
const row = pixels[y];
|
const row = pixels[y];
|
||||||
for (let x = 0; x < pixels[y].length; x++) {
|
for (let x = 0; x < pixels[y].length; x++) {
|
||||||
const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1];
|
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);
|
ctx.fillRect(x * canvasPixelSize, y * canvasPixelSize, canvasPixelSize, canvasPixelSize);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
|
export const TAG = {
|
||||||
|
DEFAULT: "default",
|
||||||
|
TUFT: "tuft",
|
||||||
|
};
|
||||||
|
|
||||||
class Layer {
|
class Layer {
|
||||||
/**
|
/**
|
||||||
* @param {string[][]} pixels
|
* @param {string[][]} pixels
|
||||||
* @param {string} [tag]
|
* @param {string} [tag]
|
||||||
*/
|
*/
|
||||||
constructor(pixels, tag = "default") {
|
constructor(pixels, tag = TAG.DEFAULT) {
|
||||||
this.pixels = pixels;
|
this.pixels = pixels;
|
||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
}
|
}
|
||||||
224
src/animation/sprites.js
Normal 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)
|
||||||
|
]),
|
||||||
|
);
|
||||||
61
src/birb.js
@@ -1,8 +1,9 @@
|
|||||||
import { Directions, getLayer, getWindowHeight, getFixedWindowHeight } from './shared.js';
|
import { Directions, getLayerPixels, getWindowHeight, getFixedWindowHeight } from './shared.js';
|
||||||
import Layer from './layer.js';
|
import Layer from './animation/layer.js';
|
||||||
import Frame from './frame.js';
|
import Frame from './animation/frame.js';
|
||||||
import Anim from './anim.js';
|
import Anim from './animation/anim.js';
|
||||||
import { BirdType } from './sprites.js';
|
import { BirdType, PALETTE } from './animation/sprites.js';
|
||||||
|
import { createHatLayers } from './hats.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {keyof typeof Animations} AnimationType
|
* @typedef {keyof typeof Animations} AnimationType
|
||||||
@@ -31,8 +32,9 @@ export class Birb {
|
|||||||
* @param {string[][]} spriteSheet The loaded sprite sheet pixel data
|
* @param {string[][]} spriteSheet The loaded sprite sheet pixel data
|
||||||
* @param {number} spriteWidth
|
* @param {number} spriteWidth
|
||||||
* @param {number} spriteHeight
|
* @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.birbCssScale = birbCssScale;
|
||||||
this.canvasPixelSize = canvasPixelSize;
|
this.canvasPixelSize = canvasPixelSize;
|
||||||
this.windowPixelSize = canvasPixelSize * birbCssScale;
|
this.windowPixelSize = canvasPixelSize * birbCssScale;
|
||||||
@@ -41,28 +43,31 @@ export class Birb {
|
|||||||
|
|
||||||
// Build layers from sprite sheet
|
// Build layers from sprite sheet
|
||||||
this.layers = {
|
this.layers = {
|
||||||
base: new Layer(getLayer(spriteSheet, 0, this.spriteWidth)),
|
base: new Layer(getLayerPixels(spriteSheet, 0, this.spriteWidth)),
|
||||||
down: new Layer(getLayer(spriteSheet, 1, this.spriteWidth)),
|
down: new Layer(getLayerPixels(spriteSheet, 1, this.spriteWidth)),
|
||||||
heartOne: new Layer(getLayer(spriteSheet, 2, this.spriteWidth)),
|
heartOne: new Layer(getLayerPixels(spriteSheet, 2, this.spriteWidth)),
|
||||||
heartTwo: new Layer(getLayer(spriteSheet, 3, this.spriteWidth)),
|
heartTwo: new Layer(getLayerPixels(spriteSheet, 3, this.spriteWidth)),
|
||||||
heartThree: new Layer(getLayer(spriteSheet, 4, this.spriteWidth)),
|
heartThree: new Layer(getLayerPixels(spriteSheet, 4, this.spriteWidth)),
|
||||||
tuftBase: new Layer(getLayer(spriteSheet, 5, this.spriteWidth), "tuft"),
|
tuftBase: new Layer(getLayerPixels(spriteSheet, 5, this.spriteWidth), "tuft"),
|
||||||
tuftDown: new Layer(getLayer(spriteSheet, 6, this.spriteWidth), "tuft"),
|
tuftDown: new Layer(getLayerPixels(spriteSheet, 6, this.spriteWidth), "tuft"),
|
||||||
wingsUp: new Layer(getLayer(spriteSheet, 7, this.spriteWidth)),
|
wingsUp: new Layer(getLayerPixels(spriteSheet, 7, this.spriteWidth)),
|
||||||
wingsDown: new Layer(getLayer(spriteSheet, 8, this.spriteWidth)),
|
wingsDown: new Layer(getLayerPixels(spriteSheet, 8, this.spriteWidth)),
|
||||||
happyEye: new Layer(getLayer(spriteSheet, 9, this.spriteWidth)),
|
happyEye: new Layer(getLayerPixels(spriteSheet, 9, this.spriteWidth)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Build hat layers
|
||||||
|
const hatLayers = createHatLayers(hatSpriteSheet);
|
||||||
|
|
||||||
// Build frames from layers
|
// Build frames from layers
|
||||||
this.frames = {
|
this.frames = {
|
||||||
base: new Frame([this.layers.base, this.layers.tuftBase]),
|
base: new Frame([this.layers.base, this.layers.tuftBase, ...hatLayers.base]),
|
||||||
headDown: new Frame([this.layers.down, this.layers.tuftDown]),
|
headDown: new Frame([this.layers.down, this.layers.tuftDown, ...hatLayers.down]),
|
||||||
wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]),
|
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]),
|
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, this.layers.heartOne]),
|
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, this.layers.heartTwo]),
|
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, this.layers.heartThree]),
|
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, this.layers.heartTwo]),
|
heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, ...hatLayers.base, this.layers.heartTwo]),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build animations from frames
|
// Build animations from frames
|
||||||
@@ -121,14 +126,16 @@ export class Birb {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw the current animation frame
|
* 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)
|
* @returns {boolean} Whether the animation has completed (for non-looping animations)
|
||||||
*/
|
*/
|
||||||
draw(species) {
|
draw(species, hat) {
|
||||||
const anim = this.animations[this.currentAnimation];
|
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
|
* @returns {AnimationType} The current animation key
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { debug, log, error } from "./shared.js";
|
import { debug, log, error } from "./shared.js";
|
||||||
|
|
||||||
const SAVE_KEY = "birbSaveData";
|
export const SAVE_KEY = "birbSaveData";
|
||||||
const ROOT_PATH = "";
|
const ROOT_PATH = "";
|
||||||
|
const SET_CONTEXT = "__CONTEXT__"
|
||||||
|
const MONOCRAFT_URL = "__MONOCRAFT_URL__";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./application.js').BirbSaveData} BirbSaveData
|
* @typedef {import('./application.js').BirbSaveData} BirbSaveData
|
||||||
@@ -12,14 +14,6 @@ const ROOT_PATH = "";
|
|||||||
*/
|
*/
|
||||||
export class Context {
|
export class Context {
|
||||||
|
|
||||||
/**
|
|
||||||
* @abstract
|
|
||||||
* @returns {boolean} Whether this context is applicable
|
|
||||||
*/
|
|
||||||
isContextActive() {
|
|
||||||
throw new Error("Method not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @abstract
|
* @abstract
|
||||||
* @returns {Promise<BirbSaveData|{}>}
|
* @returns {Promise<BirbSaveData|{}>}
|
||||||
@@ -99,20 +93,17 @@ export class Context {
|
|||||||
areStickyNotesEnabled() {
|
areStickyNotesEnabled() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getFontStyles() {
|
||||||
|
return getFontFaceImport(MONOCRAFT_URL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LocalContext extends Context {
|
export class LocalContext extends Context {
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isContextActive() {
|
|
||||||
return window.location.hostname === "127.0.0.1"
|
|
||||||
|| window.location.hostname === "localhost"
|
|
||||||
|| window.location.hostname.startsWith("192.168.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
* @returns {Promise<BirbSaveData|{}>}
|
* @returns {Promise<BirbSaveData|{}>}
|
||||||
@@ -140,15 +131,6 @@ export class LocalContext extends Context {
|
|||||||
|
|
||||||
export class UserScriptContext extends Context {
|
export class UserScriptContext extends Context {
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isContextActive() {
|
|
||||||
// @ts-expect-error
|
|
||||||
return typeof GM_getValue === "function";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
* @returns {Promise<BirbSaveData|{}>}
|
* @returns {Promise<BirbSaveData|{}>}
|
||||||
@@ -180,16 +162,7 @@ export class UserScriptContext extends Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BrowserExtensionContext extends Context {
|
export class BrowserExtensionContext extends Context {
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isContextActive() {
|
|
||||||
// @ts-expect-error
|
|
||||||
return typeof chrome !== "undefined";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
@@ -216,9 +189,9 @@ class BrowserExtensionContext extends Context {
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
console.error(chrome.runtime.lastError);
|
error(chrome.runtime.lastError);
|
||||||
} else {
|
} else {
|
||||||
console.log("Settings saved successfully");
|
log("Settings saved successfully");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -229,17 +202,19 @@ class BrowserExtensionContext extends Context {
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
chrome.storage.sync.clear();
|
chrome.storage.sync.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getFontStyles() {
|
||||||
|
// Use extension bundled font file
|
||||||
|
// @ts-expect-error
|
||||||
|
return getFontFaceImport(chrome.runtime.getURL('fonts/Monocraft.otf'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ObsidianContext extends Context {
|
export class ObsidianContext extends Context {
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isContextActive() {
|
|
||||||
// @ts-expect-error
|
|
||||||
return typeof app !== "undefined" && typeof app.vault !== "undefined";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
@@ -319,21 +294,12 @@ export class ObsidianContext extends Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTEXTS = [
|
/**
|
||||||
new UserScriptContext(),
|
* @param {string} src
|
||||||
new ObsidianContext(),
|
* @returns {string}
|
||||||
new BrowserExtensionContext(),
|
*/
|
||||||
new LocalContext()
|
function getFontFaceImport(src) {
|
||||||
];
|
return `@font-face { font-family: 'Monocraft'; src: url("${src}") format('opentype'); font-weight: normal; font-style: normal; }`;
|
||||||
|
|
||||||
export function getContext() {
|
|
||||||
for (const context of CONTEXTS) {
|
|
||||||
if (context.isContextActive()) {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
error("No applicable context found, defaulting to LocalContext");
|
|
||||||
return new LocalContext();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
0
src/fieldGuide.js
Normal file
240
src/hats.js
Normal 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;
|
||||||
|
}
|
||||||
39
src/menu.js
@@ -12,13 +12,15 @@ export const MENU_EXIT_ID = "birb-menu-exit";
|
|||||||
|
|
||||||
export class MenuItem {
|
export class MenuItem {
|
||||||
/**
|
/**
|
||||||
* @param {string} text
|
* @param {string|(() => string)} text
|
||||||
* @param {() => void} action
|
* @param {() => void} action
|
||||||
|
* @param {number[][]} [icon]
|
||||||
* @param {boolean} [removeMenu]
|
* @param {boolean} [removeMenu]
|
||||||
*/
|
*/
|
||||||
constructor(text, action, removeMenu = true) {
|
constructor(text, action, icon, removeMenu = true) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
|
this.icon = icon;
|
||||||
this.removeMenu = removeMenu;
|
this.removeMenu = removeMenu;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,10 +30,11 @@ export class ConditionalMenuItem extends MenuItem {
|
|||||||
* @param {string} text
|
* @param {string} text
|
||||||
* @param {() => void} action
|
* @param {() => void} action
|
||||||
* @param {() => boolean} condition
|
* @param {() => boolean} condition
|
||||||
|
* @param {number[][]} [icon]
|
||||||
* @param {boolean} [removeMenu]
|
* @param {boolean} [removeMenu]
|
||||||
*/
|
*/
|
||||||
constructor(text, action, condition, removeMenu = true) {
|
constructor(text, action, condition, icon, removeMenu = true) {
|
||||||
super(text, action, removeMenu);
|
super(text, action, icon, removeMenu);
|
||||||
this.condition = condition;
|
this.condition = condition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +45,7 @@ export class DebugMenuItem extends ConditionalMenuItem {
|
|||||||
* @param {() => void} action
|
* @param {() => void} action
|
||||||
*/
|
*/
|
||||||
constructor(text, action, removeMenu = true) {
|
constructor(text, action, removeMenu = true) {
|
||||||
super(text, action, () => isDebug(), removeMenu);
|
super(text, action, () => isDebug(), undefined, removeMenu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +60,29 @@ export class Separator extends MenuItem {
|
|||||||
* @param {() => void} removeMenuCallback
|
* @param {() => void} removeMenuCallback
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
function makeMenuItem(item, removeMenuCallback) {
|
function createMenuItem(item, removeMenuCallback) {
|
||||||
if (item instanceof Separator) {
|
if (item instanceof Separator) {
|
||||||
return makeElement("birb-window-separator");
|
return makeElement("birb-window-separator");
|
||||||
}
|
}
|
||||||
let menuItem = makeElement("birb-menu-item", item.text);
|
let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text);
|
||||||
|
if (item.icon) {
|
||||||
|
const iconCanvas = document.createElement("canvas");
|
||||||
|
iconCanvas.width = 7;
|
||||||
|
iconCanvas.height = 6;
|
||||||
|
iconCanvas.classList.add("birb-menu-item-icon");
|
||||||
|
const ctx = iconCanvas.getContext("2d");
|
||||||
|
if (ctx) {
|
||||||
|
for (let row = 0; row < item.icon.length; row++) {
|
||||||
|
for (let col = 0; col < item.icon[row].length; col++) {
|
||||||
|
if (item.icon[row][col]) {
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.fillRect(col, row, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menuItem.prepend(iconCanvas);
|
||||||
|
}
|
||||||
onClick(menuItem, () => {
|
onClick(menuItem, () => {
|
||||||
if (item.removeMenu) {
|
if (item.removeMenu) {
|
||||||
removeMenuCallback();
|
removeMenuCallback();
|
||||||
@@ -89,7 +110,7 @@ export function insertMenu(menuItems, title, updateLocationCallback) {
|
|||||||
const removeCallback = () => removeMenu();
|
const removeCallback = () => removeMenu();
|
||||||
for (const item of menuItems) {
|
for (const item of menuItems) {
|
||||||
if (!(item instanceof ConditionalMenuItem) || item.condition()) {
|
if (!(item instanceof ConditionalMenuItem) || item.condition()) {
|
||||||
content.appendChild(makeMenuItem(item, removeCallback));
|
content.appendChild(createMenuItem(item, removeCallback));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
menu.appendChild(header);
|
menu.appendChild(header);
|
||||||
@@ -146,7 +167,7 @@ export function switchMenuItems(menuItems, updateLocationCallback) {
|
|||||||
const removeCallback = () => removeMenu();
|
const removeCallback = () => removeMenu();
|
||||||
for (const item of menuItems) {
|
for (const item of menuItems) {
|
||||||
if (!(item instanceof ConditionalMenuItem) || item.condition()) {
|
if (!(item instanceof ConditionalMenuItem) || item.condition()) {
|
||||||
content.appendChild(makeMenuItem(item, removeCallback));
|
content.appendChild(createMenuItem(item, removeCallback));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateLocationCallback(menu);
|
updateLocationCallback(menu);
|
||||||
|
|||||||
4
src/platforms/extension/extension.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { initializeApplication } from "../../application.js";
|
||||||
|
import { BrowserExtensionContext } from "../../context.js";
|
||||||
|
|
||||||
|
initializeApplication(new BrowserExtensionContext());
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "pocket-bird",
|
"id": "pocket-bird",
|
||||||
"name": "Pocket Bird",
|
"name": "Pocket Bird",
|
||||||
"version": "2025.11.14.205",
|
"version": "__VERSION__",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Add a pet bird to fly around your notes and keep you company!",
|
"description": "Add a pet bird to fly around your notes and keep you company!",
|
||||||
"author": "Idrees Hassan",
|
"author": "Idrees Hassan",
|
||||||
4
src/platforms/obsidian/obsidian.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { initializeApplication } from "../../application.js";
|
||||||
|
import { ObsidianContext } from "../../context.js";
|
||||||
|
|
||||||
|
initializeApplication(new ObsidianContext());
|
||||||
4
src/platforms/userscript/userscript.js
Normal 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
@@ -0,0 +1,4 @@
|
|||||||
|
import { initializeApplication } from "../../application.js";
|
||||||
|
import { LocalContext } from "../../context.js";
|
||||||
|
|
||||||
|
initializeApplication(new LocalContext());
|
||||||
@@ -4,6 +4,7 @@ export const Directions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let debugMode = location.hostname === "127.0.0.1";
|
let debugMode = location.hostname === "127.0.0.1";
|
||||||
|
let context = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} Whether debug mode is enabled
|
* @returns {boolean} Whether debug mode is enabled
|
||||||
@@ -19,6 +20,17 @@ export function setDebug(value) {
|
|||||||
debugMode = 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
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -181,7 +193,7 @@ export function error() {
|
|||||||
* @param {number} width The width of each sprite
|
* @param {number} width The width of each sprite
|
||||||
* @returns {string[][]}
|
* @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
|
// From an array of a horizontal sprite sheet, get the layer for a specific sprite
|
||||||
const layer = [];
|
const layer = [];
|
||||||
for (let y = 0; y < width; y++) {
|
for (let y = 0; y < width; y++) {
|
||||||
|
|||||||
48
src/sound.js
Normal 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
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
199
src/sprites.js
@@ -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",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
|
getContext,
|
||||||
makeElement,
|
makeElement,
|
||||||
makeDraggable,
|
makeDraggable,
|
||||||
makeClosable
|
makeClosable
|
||||||
} from './shared.js';
|
} from './shared.js';
|
||||||
import { getContext } from './context.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} SavedStickyNote
|
* @typedef {Object} SavedStickyNote
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Monocraft';
|
|
||||||
src: url("__MONOCRAFT_SRC__") format('opentype');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--birb-border-size: 2px;
|
--birb-border-size: 2px;
|
||||||
--birb-neg-border-size: calc(var(--birb-border-size) * -1);
|
--birb-neg-border-size: calc(var(--birb-border-size) * -1);
|
||||||
@@ -41,6 +34,22 @@
|
|||||||
z-index: 2147483630 !important;
|
z-index: 2147483630 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.birb-item {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
transform: scale(calc(var(--birb-scale) * 1.5)) !important;
|
||||||
|
transform-origin: bottom;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
z-index: 2147483630 !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birb-item:hover {
|
||||||
|
transform: scale(calc(var(--birb-scale) * 1.9)) !important;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
.birb-window {
|
.birb-window {
|
||||||
font-family: "Monocraft", monospace !important;
|
font-family: "Monocraft", monospace !important;
|
||||||
line-height: initial !important;
|
line-height: initial !important;
|
||||||
@@ -198,18 +207,21 @@
|
|||||||
|
|
||||||
.birb-menu-item {
|
.birb-menu-item {
|
||||||
width: calc(100% - var(--birb-double-border-size));
|
width: calc(100% - var(--birb-double-border-size));
|
||||||
|
white-space: nowrap;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
padding-left: 10px;
|
padding-left: 2px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
opacity: 0.7 !important;
|
opacity: 0.7 !important;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: left;
|
||||||
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: black !important;
|
color: black !important;
|
||||||
|
transition: background 0.1s, color 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.birb-menu-item:hover {
|
.birb-menu-item:hover {
|
||||||
@@ -221,6 +233,21 @@
|
|||||||
var(--birb-neg-border-size) 0 var(--birb-highlight),
|
var(--birb-neg-border-size) 0 var(--birb-highlight),
|
||||||
0 var(--birb-neg-border-size) var(--birb-highlight),
|
0 var(--birb-neg-border-size) var(--birb-highlight),
|
||||||
0 var(--birb-border-size) var(--birb-highlight);
|
0 var(--birb-border-size) var(--birb-highlight);
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birb-menu-item-icon {
|
||||||
|
width: calc(7 * var(--birb-border-size));
|
||||||
|
height: calc(6 * var(--birb-border-size));
|
||||||
|
padding-right: calc(5 * var(--birb-border-size));
|
||||||
|
flex-shrink: 0;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
color: var(--birb-highlight);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birb-menu-item:hover > .birb-menu-item-icon {
|
||||||
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.birb-menu-item-arrow {
|
.birb-menu-item-arrow {
|
||||||
@@ -237,14 +264,22 @@
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
#birb-field-guide {
|
#birb-field-guide, #birb-wardrobe {
|
||||||
width: 322px !important;
|
width: 322px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#birb-field-guide .birb-grid-content {
|
||||||
|
grid-template-columns: repeat(4, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
#birb-wardrobe .birb-grid-content {
|
||||||
|
grid-template-columns: repeat(4, auto);
|
||||||
|
grid-auto-flow: row;
|
||||||
|
}
|
||||||
|
|
||||||
.birb-grid-content {
|
.birb-grid-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: repeat(3, auto);
|
grid-auto-flow: row;
|
||||||
grid-auto-flow: column;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
@@ -263,10 +298,12 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: border-color 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.birb-grid-item:hover {
|
.birb-grid-item:hover {
|
||||||
border-color: var(--birb-highlight);
|
border-color: var(--birb-highlight);
|
||||||
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.birb-grid-item canvas {
|
.birb-grid-item canvas {
|
||||||
@@ -276,7 +313,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.birb-grid-item, .birb-field-guide-description, .birb-message-content {
|
.birb-grid-item, .birb-field-guide-description, .birb-message-content {
|
||||||
border: var(--birb-border-size) solid rgb(255, 207, 144);
|
border: var(--birb-border-size) solid #ffcf90;
|
||||||
box-shadow: 0 0 0 var(--birb-border-size) white;
|
box-shadow: 0 0 0 var(--birb-border-size) white;
|
||||||
background: rgba(255, 221, 177, 0.5);
|
background: rgba(255, 221, 177, 0.5);
|
||||||
}
|
}
|
||||||
@@ -295,6 +332,15 @@
|
|||||||
background: var(--birb-mix-color);
|
background: var(--birb-mix-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.birb-field-guide-section-label {
|
||||||
|
padding-top: 4px;
|
||||||
|
/* padding-left: calc(10px + var(--birb-border-size) / 2); */
|
||||||
|
color: #876c4e;
|
||||||
|
text-align: center;
|
||||||
|
/* Italics */
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.birb-field-guide-description {
|
.birb-field-guide-description {
|
||||||
max-width: calc(100% - 20px);
|
max-width: calc(100% - 20px);
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
@@ -306,7 +352,14 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: rgb(124, 108, 75);
|
color: #7c6c4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birb-field-guide-latin-name {
|
||||||
|
text-decoration: underline;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: bold;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#birb-feather {
|
#birb-feather {
|
||||||
@@ -319,7 +372,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: rgb(124, 108, 75);
|
color: #7c6c4b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.birb-sticky-note {
|
.birb-sticky-note {
|
||||||
@@ -357,6 +410,13 @@
|
|||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.birb-sticky-note-input::placeholder {
|
||||||
|
font-family: "Monocraft", monospace !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: rgba(0, 0, 0, 0.35) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.birb-sticky-note-input:focus {
|
.birb-sticky-note-input:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|||||||