36 Commits

Author SHA1 Message Date
Idrees Hassan
938b139298 Remove duplicate function 2025-10-26 18:03:15 -04:00
Idrees Hassan
fe497a4020 Move birb rendering to separate file 2025-10-26 17:56:49 -04:00
Idrees Hassan
e1c88120d3 Rename birb to application 2025-10-26 17:08:09 -04:00
Idrees Hassan
0873b4db1f Reorder constants 2025-10-26 17:00:35 -04:00
Idrees Hassan
129cd89340 Remove configs 2025-10-26 16:34:20 -04:00
Idrees Hassan
b7453d1a94 Remove decoration code 2025-10-26 16:21:52 -04:00
Idrees Hassan
476c75cecb Remove duplicate code 2025-10-26 16:05:00 -04:00
Idrees Hassan
7c146aa575 Move menu constants to menu section 2025-10-26 15:47:51 -04:00
Idrees Hassan
9f15703689 Separate menu components 2025-10-26 15:29:36 -04:00
Idrees Hassan
ef764153b9 Separate sticky note logic 2025-10-26 14:04:40 -04:00
Idrees Hassan
45ed91a531 Move anim class and stylesheet 2025-10-26 13:56:14 -04:00
Idrees Hassan
a9f386b6bd Rename sprite part constants 2025-10-26 13:52:54 -04:00
Idrees Hassan
b1a4dfac7c Add jsconfig file 2025-10-26 13:52:13 -04:00
Idrees Hassan
4d12eb46a2 Reorganize components 2025-10-26 13:47:22 -04:00
Idrees Hassan
eda3f9fbc1 Change files to use a consistent case 2025-10-26 13:22:24 -04:00
Idrees Hassan
8be8ab2858 Use rollup and split up files 2025-10-26 13:17:18 -04:00
Idrees
26105fc66d Merge pull request #1 from IdreesInc/extension
Add extension manifest
2025-10-26 12:36:24 -04:00
Idrees Hassan
4b206f638d Reformat promise 2025-10-26 12:19:25 -04:00
Idrees Hassan
6124fcd969 Group sticky note functions together 2025-10-25 23:49:55 -04:00
Idrees Hassan
b9304fe11b Clean up code 2025-10-25 23:46:50 -04:00
Idrees Hassan
7f9c388853 Allow clicking after short delay following petting 2025-10-25 22:55:55 -04:00
Idrees Hassan
3aa0308746 Move comments around 2025-10-25 20:41:10 -04:00
Idrees Hassan
f45eb0ce61 Fix petting on mobile 2025-10-25 20:40:25 -04:00
Idrees Hassan
e55f0e7412 Remove PICO-8 and Spotify windows 2025-10-25 19:03:14 -04:00
Idrees Hassan
ff98390e10 Fix manifest 2025-10-25 19:01:04 -04:00
Idrees Hassan
f73e29a723 Correct padding on field guide 2025-10-25 18:29:41 -04:00
Idrees Hassan
bf1bb74219 Update README.md 2025-10-25 15:41:49 -04:00
Idrees Hassan
ca3bc5be4b Merge branch 'main' into extension 2025-10-25 15:33:01 -04:00
Idrees Hassan
29f1766a95 Switch to absolute positioning when on element 2025-10-23 23:32:01 -04:00
Idrees Hassan
5298b7801b Treat ground as yet another element 2025-10-23 21:25:43 -04:00
Idrees Hassan
bed3d37940 Bump version 2025-10-22 22:38:04 -04:00
Idrees Hassan
bae44cc98c Adjust startY to account for scrolling 2025-10-22 22:33:27 -04:00
Idrees Hassan
2767caeb84 Update userscript name 2025-10-22 22:03:07 -04:00
Idrees Hassan
270367139d Don't focus on ground immediately for mobile 2025-10-22 22:00:01 -04:00
Idrees Hassan
9fca0b2046 Bump version 2025-10-22 21:56:11 -04:00
Idrees Hassan
5860171184 Remove afk delay on mobile 2025-10-22 21:46:12 -04:00
23 changed files with 6489 additions and 5560 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/node_modules /node_modules
.DS_Store .DS_Store
/dist/birb.bundled.js

View File

@@ -1,8 +1,8 @@
# Browser Bird (Work in Progress!) # Pocket Bird (Work in Progress!)
This project is still being worked on, but if you wish to help me beta test it, join the [Discord](https://discord.gg/6yxE9prcNc) and follow the installation instructions below! This project is still being worked on, but if you wish to help me beta test it, join the [Discord](https://discord.gg/6yxE9prcNc) and follow the installation instructions below!
1. Install [Tampermonkey](https://www.tampermonkey.net/) on your web browser 1. Install [Tampermonkey](https://www.tampermonkey.net/) on your web browser
2. Enable the Tampermonkey extension and give it the permissions requested 2. Enable the Tampermonkey extension and give it the permissions requested
3. Install my Browser Bird script by going to this link and clicking install: [https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js](https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/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/birb.user.js](https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/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!

1909
birb.js

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
// @ts-check // @ts-check
import { readFileSync, writeFileSync } from 'fs'; import { rollup } from 'rollup';
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
const spriteSheets = [ const spriteSheets = [
{ {
@@ -10,37 +11,53 @@ const spriteSheets = [
{ {
key: "__FEATHER_SPRITE_SHEET__", key: "__FEATHER_SPRITE_SHEET__",
path: "./sprites/feather.png" path: "./sprites/feather.png"
},
{
key: "__DECORATIONS_SPRITE_SHEET__",
path: "./sprites/decorations.png"
} }
]; ];
const STYLESHEET_PATH = "./stylesheet.css"; const STYLESHEET_PATH = "./src/stylesheet.css";
const STYLESHEET_KEY = "___STYLESHEET___"; const STYLESHEET_KEY = "___STYLESHEET___";
let version = "0.0.0"; const now = new Date();
// Try to read version from manifest.json const versionDate = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`;
// Get current build number from manifest.json
let buildNumber = 0;
try { try {
const manifest = JSON.parse(readFileSync('manifest.json', 'utf8')); const manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
if (manifest.version) { if (manifest.version) {
version = manifest.version; if (manifest.version.startsWith(versionDate)) {
// Same day, increment build number
const parts = manifest.version.split('.');
if (parts.length === 4) {
buildNumber = parseInt(parts[3], 10) + 1;
}
}
} }
} catch (e) { } catch (e) {
console.error("Could not read version from manifest.json"); console.error("Could not read version from manifest.json");
throw e; throw e;
} }
// Update manifest.json with new version
const version = `${versionDate}.${buildNumber}`;
try {
const manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
manifest.version = version;
writeFileSync('manifest.json', JSON.stringify(manifest, null, 4), 'utf8');
} catch (e) {
console.error("Could not update version in manifest.json");
throw e;
}
const userScriptHeader = const userScriptHeader =
`// ==UserScript== `// ==UserScript==
// @name Browser Bird // @name Pocket Bird
// @namespace https://idreesinc.com // @namespace https://idreesinc.com
// @version ${version} // @version ${version}
// @description birb // @description birb
// @author Idrees // @author Idrees
// @downloadURL https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
// @updateURL https://github.com/IdreesInc/Browser-Bird/raw/refs/heads/main/dist/birb.user.js // @updateURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
// @match *://*/* // @match *://*/*
// @grant GM_setValue // @grant GM_setValue
// @grant GM_getValue // @grant GM_getValue
@@ -49,8 +66,19 @@ const userScriptHeader =
`; `;
// Bundle with rollup
const bundle = await rollup({
input: 'src/application.js',
});
let birbJs = readFileSync('birb.js', 'utf8'); await bundle.write({
file: 'dist/birb.bundled.js',
format: 'iife',
});
await bundle.close();
let birbJs = readFileSync('dist/birb.bundled.js', 'utf8');
// Compile and insert sprite sheets // Compile and insert sprite sheets
for (const spriteSheet of spriteSheets) { for (const spriteSheet of spriteSheets) {
@@ -65,6 +93,9 @@ birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent);
// Build standard javascript file // Build standard javascript file
writeFileSync('./dist/birb.js', birbJs); writeFileSync('./dist/birb.js', birbJs);
// Delete bundled file
unlinkSync('./dist/birb.bundled.js');
// Build user script // Build user script
const userScript = userScriptHeader + birbJs; const userScript = userScriptHeader + birbJs;
writeFileSync('./dist/birb.user.js', userScript); writeFileSync('./dist/birb.user.js', userScript);

3714
dist/birb.js vendored

File diff suppressed because it is too large Load Diff

3722
dist/birb.user.js vendored

File diff suppressed because it is too large Load Diff

7
jsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"checkJs": true,
"target": "es2017"
},
"include": ["src"]
}

View File

@@ -1,29 +1,36 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Browser Bird", "name": "Pocket Bird",
"description": "It's a bird, in your browser. What more could you want?", "description": "It's a bird, in your browser. What more could you want?",
"version": "2025.9.16.1", "version": "2025.10.26.402",
"homepage_url": "https://idreesinc.com", "homepage_url": "https://idreesinc.com",
"content_scripts": [ "content_scripts": [
{ {
"matches": ["<all_urls>"], "matches": [
"js": ["birb.js"] "<all_urls>"
} ],
], "js": [
"permissions": [ "./dist/birb.js"
"storage", ]
"activeTab" }
], ],
"web_accessible_resources": [ "permissions": [
{ "storage",
"resources": ["images/*"], "activeTab"
"matches": ["<all_urls>"] ],
} "web_accessible_resources": [
], {
"browser_specific_settings": { "resources": [
"gecko": { "images/*"
"id": "birb@idreesinc.com" ],
} "matches": [
} "<all_urls>"
} ]
}
],
"browser_specific_settings": {
"gecko": {
"id": "birb@idreesinc.com"
}
}
}

360
package-lock.json generated
View File

@@ -9,9 +9,325 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10",
"rollup": "^4.52.5"
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": { "node_modules/anymatch": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -316,6 +632,48 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/rollup": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.52.5",
"@rollup/rollup-android-arm64": "4.52.5",
"@rollup/rollup-darwin-arm64": "4.52.5",
"@rollup/rollup-darwin-x64": "4.52.5",
"@rollup/rollup-freebsd-arm64": "4.52.5",
"@rollup/rollup-freebsd-x64": "4.52.5",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
"@rollup/rollup-linux-arm-musleabihf": "4.52.5",
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
"@rollup/rollup-linux-arm64-musl": "4.52.5",
"@rollup/rollup-linux-loong64-gnu": "4.52.5",
"@rollup/rollup-linux-ppc64-gnu": "4.52.5",
"@rollup/rollup-linux-riscv64-gnu": "4.52.5",
"@rollup/rollup-linux-riscv64-musl": "4.52.5",
"@rollup/rollup-linux-s390x-gnu": "4.52.5",
"@rollup/rollup-linux-x64-gnu": "4.52.5",
"@rollup/rollup-linux-x64-musl": "4.52.5",
"@rollup/rollup-openharmony-arm64": "4.52.5",
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
"@rollup/rollup-win32-ia32-msvc": "4.52.5",
"@rollup/rollup-win32-x64-gnu": "4.52.5",
"@rollup/rollup-win32-x64-msvc": "4.52.5",
"fsevents": "~2.3.2"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",

View File

@@ -7,9 +7,10 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "node build.js", "build": "node build.js",
"dev": "nodemon --watch birb.js --watch stylesheet.css --watch build.js --watch manifest.json --exec \"npm run build\"" "dev": "nodemon --watch src --watch stylesheet.css --watch build.js --exec \"npm run build\""
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10",
"rollup": "^4.52.5"
} }
} }

View File

@@ -12,7 +12,7 @@
} }
#spacer { #spacer {
height: 200vh; height: 100vh;
} }
</style> </style>
</head> </head>

73
src/Frame.js Normal file
View File

@@ -0,0 +1,73 @@
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) {
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 Normal file
View File

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

48
src/anim.js Normal file
View File

@@ -0,0 +1,48 @@
import Frame from "./frame.js";
import { BirdType } from "./sprites";
class Anim {
/**
* @param {Frame[]} frames
* @param {number[]} durations
* @param {boolean} loop
*/
constructor(frames, durations, loop = true) {
this.frames = frames;
this.durations = durations;
this.loop = loop;
}
getAnimationDuration() {
return this.durations.reduce((a, b) => a + b, 0);
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {number} direction
* @param {number} timeStart The start time of the animation in milliseconds
* @param {number} canvasPixelSize The size of a canvas pixel in pixels
* @param {BirdType} [species] The species to use for the animation
* @returns {boolean} Whether the animation is complete
*/
draw(ctx, direction, timeStart, canvasPixelSize, species) {
let time = Date.now() - timeStart;
const duration = this.getAnimationDuration();
if (this.loop) {
time %= duration;
}
let totalDuration = 0;
for (let i = 0; i < this.durations.length; i++) {
totalDuration += this.durations[i];
if (time < totalDuration) {
this.frames[i].draw(ctx, direction, canvasPixelSize, species);
return false;
}
}
// Draw the last frame if the animation is complete
this.frames[this.frames.length - 1].draw(ctx, direction, canvasPixelSize, species);
return true;
}
}
export default Anim;

1009
src/application.js Normal file

File diff suppressed because it is too large Load Diff

268
src/birb.js Normal file
View File

@@ -0,0 +1,268 @@
import { Directions, getLayer } from './shared.js';
import Layer from './layer.js';
import Frame from './frame.js';
import Anim from './anim.js';
import { BirdType } from './sprites.js';
/**
* @typedef {keyof typeof Animations} AnimationType
*/
export const Animations = /** @type {const} */ ({
STILL: "STILL",
BOB: "BOB",
FLYING: "FLYING",
HEART: "HEART"
});
export class Birb {
animStart = Date.now();
x = 0;
y = 0;
direction = Directions.RIGHT;
isAbsolutePositioned = false;
visible = true;
/** @type {AnimationType} */
currentAnimation = Animations.STILL;
/**
* @param {number} birbCssScale
* @param {number} canvasPixelSize
* @param {string[][]} spriteSheet The loaded sprite sheet pixel data
* @param {number} spriteWidth
* @param {number} spriteHeight
*/
constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) {
this.birbCssScale = birbCssScale;
this.canvasPixelSize = canvasPixelSize;
this.windowPixelSize = canvasPixelSize * birbCssScale;
this.spriteWidth = spriteWidth;
this.spriteHeight = spriteHeight;
// Build layers from sprite sheet
this.layers = {
base: new Layer(getLayer(spriteSheet, 0, this.spriteWidth)),
down: new Layer(getLayer(spriteSheet, 1, this.spriteWidth)),
heartOne: new Layer(getLayer(spriteSheet, 2, this.spriteWidth)),
heartTwo: new Layer(getLayer(spriteSheet, 3, this.spriteWidth)),
heartThree: new Layer(getLayer(spriteSheet, 4, this.spriteWidth)),
tuftBase: new Layer(getLayer(spriteSheet, 5, this.spriteWidth), "tuft"),
tuftDown: new Layer(getLayer(spriteSheet, 6, this.spriteWidth), "tuft"),
wingsUp: new Layer(getLayer(spriteSheet, 7, this.spriteWidth)),
wingsDown: new Layer(getLayer(spriteSheet, 8, this.spriteWidth)),
happyEye: new Layer(getLayer(spriteSheet, 9, this.spriteWidth)),
};
// Build frames from layers
this.frames = {
base: new Frame([this.layers.base, this.layers.tuftBase]),
headDown: new Frame([this.layers.down, this.layers.tuftDown]),
wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]),
wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]),
heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]),
heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]),
heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]),
};
// Build animations from frames
this.animations = {
[Animations.STILL]: new Anim([this.frames.base], [1000]),
[Animations.BOB]: new Anim([
this.frames.base,
this.frames.headDown
], [
420,
420
]),
[Animations.FLYING]: new Anim([
this.frames.base,
this.frames.wingsUp,
this.frames.headDown,
this.frames.wingsDown,
], [
30,
80,
30,
60,
]),
[Animations.HEART]: new Anim([
this.frames.heartOne,
this.frames.heartTwo,
this.frames.heartThree,
this.frames.heartFour,
this.frames.heartThree,
this.frames.heartFour,
this.frames.heartThree,
this.frames.heartFour,
], [
60,
80,
250,
250,
250,
250,
250,
250,
], false),
};
// Create canvas element
this.canvas = document.createElement("canvas");
this.canvas.id = "birb";
this.canvas.width = this.frames.base.getPixels()[0].length * canvasPixelSize;
this.canvas.height = spriteHeight * canvasPixelSize;
this.ctx = this.canvas.getContext("2d");
// Append to document
document.body.appendChild(this.canvas);
}
/**
* Draw the current animation frame
* @param {BirdType} species The species color data
* @returns {boolean} Whether the animation has completed (for non-looping animations)
*/
draw(species) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const anim = this.animations[this.currentAnimation];
return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species);
}
/**
* @returns {AnimationType} The current animation key
*/
getCurrentAnimation() {
return this.currentAnimation;
}
/**
* Set the current animation by name and reset the animation timer
* @param {AnimationType} animationName
*/
setAnimation(animationName) {
this.currentAnimation = animationName;
this.animStart = Date.now();
}
/**
* Get the frames object
* @returns {Record<string, Frame>}
*/
getFrames() {
return this.frames;
}
/**
* Get the canvas element
* @returns {HTMLCanvasElement}
*/
getElement() {
return this.canvas;
}
/**
* Get the canvas width in CSS pixels
* @returns {number}
*/
getElementWidth() {
return this.canvas.width * this.birbCssScale;
}
/**
* Get the canvas height in CSS pixels
* @returns {number}
*/
getElementHeight() {
return this.canvas.height * this.birbCssScale;
}
getElementTop() {
const rect = this.canvas.getBoundingClientRect();
return rect.top;
}
/**
* Set the X position
* @param {number} x
*/
setX(x) {
this.x = x;
let mod = this.getElementWidth() / -2 - (this.windowPixelSize * (this.direction === Directions.RIGHT ? 2 : -2));
this.canvas.style.left = `${x + mod}px`;
}
/**
* Set the Y position
* @param {number} y
*/
setY(y) {
this.y = y;
let bottom;
if (this.isAbsolutePositioned) {
// Position is absolute, convert from fixed
bottom = y - window.scrollY;
} else {
// Position is fixed
bottom = y;
}
this.canvas.style.bottom = `${bottom}px`;
}
/**
* Get the current X position
* @returns {number}
*/
getX() {
return this.x;
}
/**
* Get the current Y position
* @returns {number}
*/
getY() {
return this.y;
}
/**
* Set the direction the bird is facing
* @param {number} direction
*/
setDirection(direction) {
this.direction = direction;
}
/**
* Set whether the element should be absolutely positioned
* @param {boolean} absolute
*/
setAbsolutePositioned(absolute) {
this.isAbsolutePositioned = absolute;
if (absolute) {
this.canvas.classList.add("birb-absolute");
} else {
this.canvas.classList.remove("birb-absolute");
}
// Update Y position to apply the new positioning mode
this.setY(this.y);
}
/**
* Set visibility of the bird
* @param {boolean} visible
*/
setVisible(visible) {
this.visible = visible;
this.canvas.style.display = visible ? "" : "none";
}
/**
* Get visibility of the bird
* @returns {boolean}
*/
isVisible() {
return this.visible;
}
}

73
src/frame.js Normal file
View File

@@ -0,0 +1,73 @@
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) {
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 Normal file
View File

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

139
src/menu.js Normal file
View File

@@ -0,0 +1,139 @@
import {
isDebug,
makeElement,
onClick,
makeDraggable,
makeClosable,
error
} from './shared.js';
export const MENU_ID = "birb-menu";
export const MENU_EXIT_ID = "birb-menu-exit";
export class MenuItem {
/**
* @param {string} text
* @param {() => void} action
* @param {boolean} [removeMenu]
* @param {boolean} [isDebug]
*/
constructor(text, action, removeMenu = true, isDebug = false) {
this.text = text;
this.action = action;
this.removeMenu = removeMenu;
this.isDebug = isDebug;
}
}
export class DebugMenuItem extends MenuItem {
/**
* @param {string} text
* @param {() => void} action
*/
constructor(text, action, removeMenu = true) {
super(text, action, removeMenu, true);
}
}
export class Separator extends MenuItem {
constructor() {
super("", () => { });
}
}
/**
* @param {MenuItem} item
* @param {() => void} removeMenuCallback
* @returns {HTMLElement}
*/
function makeMenuItem(item, removeMenuCallback) {
if (item instanceof Separator) {
return makeElement("birb-window-separator");
}
let menuItem = makeElement("birb-menu-item", item.text);
onClick(menuItem, () => {
if (item.removeMenu) {
removeMenuCallback();
}
item.action();
});
return menuItem;
}
/**
* Add the menu to the page if it doesn't already exist
* @param {MenuItem[]} menuItems
* @param {string} title
* @param {(menu: HTMLElement) => void} updateLocationCallback
*/
export function insertMenu(menuItems, title, updateLocationCallback) {
if (document.querySelector("#" + MENU_ID)) {
return;
}
let menu = makeElement("birb-window", undefined, MENU_ID);
let header = makeElement("birb-window-header");
header.innerHTML = `<div class="birb-window-title">${title}</div>`;
let content = makeElement("birb-window-content");
const removeCallback = () => removeMenu();
for (const item of menuItems) {
if (!item.isDebug || isDebug()) {
content.appendChild(makeMenuItem(item, removeCallback));
}
}
menu.appendChild(header);
menu.appendChild(content);
document.body.appendChild(menu);
makeDraggable(document.querySelector(".birb-window-header"));
let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID);
onClick(menuExit, removeCallback);
document.body.appendChild(menuExit);
makeClosable(removeCallback);
updateLocationCallback(menu);
}
/**
* Remove the menu from the page
*/
export function removeMenu() {
const menu = document.querySelector("#" + MENU_ID);
if (menu) {
menu.remove();
}
const exitMenu = document.querySelector("#" + MENU_EXIT_ID);
if (exitMenu) {
exitMenu.remove();
}
}
/**
* @returns {boolean} Whether the menu element is on the page
*/
export function isMenuOpen() {
return document.querySelector("#" + MENU_ID) !== null;
}
/**
* @param {MenuItem[]} menuItems
* @param {(menu: HTMLElement) => void} updateLocationCallback
*/
export function switchMenuItems(menuItems, updateLocationCallback) {
const menu = document.querySelector("#" + MENU_ID);
if (!menu || !(menu instanceof HTMLElement)) {
return;
}
const content = menu.querySelector(".birb-window-content");
if (!content) {
error("Birb: Content not found");
return;
}
content.innerHTML = "";
const removeCallback = () => removeMenu();
for (const item of menuItems) {
if (!item.isDebug || isDebug()) {
content.appendChild(makeMenuItem(item, removeCallback));
}
}
updateLocationCallback(menu);
}

185
src/shared.js Normal file
View File

@@ -0,0 +1,185 @@
export const Directions = {
LEFT: -1,
RIGHT: 1,
};
let debugMode = location.hostname === "127.0.0.1";
/**
* @returns {boolean} Whether debug mode is enabled
*/
export function isDebug() {
return debugMode;
}
/**
* @param {boolean} value
*/
export function setDebug(value) {
debugMode = value;
}
/**
* Create an HTML element with the specified parameters
* @param {string} className
* @param {string} [textContent]
* @param {string} [id]
* @returns {HTMLElement}
*/
export function makeElement(className, textContent, id) {
const element = document.createElement("div");
element.classList.add(className);
if (textContent) {
element.textContent = textContent;
}
if (id) {
element.id = id;
}
return element;
}
/**
* @param {Document|Element} element
* @param {(e: Event) => void} action
*/
export function onClick(element, action) {
element.addEventListener("click", (e) => action(e));
element.addEventListener("touchend", (e) => {
if (e instanceof TouchEvent === false) {
return;
} else if (element instanceof HTMLElement === false) {
return;
}
const touch = e.changedTouches[0];
const rect = element.getBoundingClientRect();
if (
touch.clientX >= rect.left &&
touch.clientX <= rect.right &&
touch.clientY >= rect.top &&
touch.clientY <= rect.bottom
) {
action(e);
}
});
}
/**
* @param {HTMLElement|null} element The element to detect drag events on
* @param {boolean} [parent] Whether to move the parent element when the child is dragged
* @param {(top: number, left: number) => void} [callback] Callback for when element is moved
*/
export function makeDraggable(element, parent = true, callback = () => { }) {
if (!element) {
return;
}
let isMouseDown = false;
let offsetX = 0;
let offsetY = 0;
let elementToMove = parent ? element.parentElement : element;
if (!elementToMove) {
error("Birb: Parent element not found");
return;
}
element.addEventListener("mousedown", (e) => {
isMouseDown = true;
offsetX = e.clientX - elementToMove.offsetLeft;
offsetY = e.clientY - elementToMove.offsetTop;
});
element.addEventListener("touchstart", (e) => {
isMouseDown = true;
const touch = e.touches[0];
offsetX = touch.clientX - elementToMove.offsetLeft;
offsetY = touch.clientY - elementToMove.offsetTop;
e.preventDefault();
});
document.addEventListener("mouseup", (e) => {
if (isMouseDown) {
callback(elementToMove.offsetTop, elementToMove.offsetLeft);
e.preventDefault();
}
isMouseDown = false;
});
document.addEventListener("touchend", (e) => {
if (isMouseDown) {
callback(elementToMove.offsetTop, elementToMove.offsetLeft);
e.preventDefault();
}
isMouseDown = false;
});
document.addEventListener("mousemove", (e) => {
if (isMouseDown) {
elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`;
elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`;
}
});
document.addEventListener("touchmove", (e) => {
if (isMouseDown) {
const touch = e.touches[0];
elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`;
elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`;
}
});
}
/**
* @param {() => void} func
* @param {Element} [closeButton]
*/
export function makeClosable(func, closeButton) {
if (closeButton) {
onClick(closeButton, func);
}
document.addEventListener("keydown", (e) => {
if (closeButton && !document.body.contains(closeButton)) {
return;
}
if (e.key === "Escape") {
func();
}
});
}
/**
* @returns {boolean} Whether the user is on a mobile device
*/
export function isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
export function log() {
console.log("Birb: ", ...arguments);
}
export function debug() {
if (isDebug()) {
console.debug("Birb: ", ...arguments);
}
}
export function error() {
console.error("Birb: ", ...arguments);
}
/**
* Get a layer from a sprite sheet array
* @param {string[][]} spriteSheet The sprite sheet pixel array
* @param {number} spriteIndex The sprite index
* @param {number} width The width of each sprite
* @returns {string[][]}
*/
export function getLayer(spriteSheet, spriteIndex, width) {
// From an array of a horizontal sprite sheet, get the layer for a specific sprite
const layer = [];
for (let y = 0; y < width; y++) {
layer.push(spriteSheet[y].slice(spriteIndex * width, (spriteIndex + 1) * width));
}
return layer;
}

199
src/sprites.js Normal file
View File

@@ -0,0 +1,199 @@
/** 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",
}),
};

170
src/stickyNotes.js Normal file
View File

@@ -0,0 +1,170 @@
import {
makeElement,
makeDraggable,
makeClosable
} from './shared.js';
/**
* @typedef {Object} SavedStickyNote
* @property {string} id
* @property {string} site
* @property {string} content
* @property {number} top
* @property {number} left
*/
export class StickyNote {
/**
* @param {string} id
* @param {string} [site]
* @param {string} [content]
* @param {number} [top]
* @param {number} [left]
*/
constructor(id, site = "", content = "", top = 0, left = 0) {
this.id = id;
this.site = site;
this.content = content;
this.top = top;
this.left = left;
}
}
/**
* Parse URL parameters into a key-value map
* @param {string} url
* @returns {Record<string, string>}
*/
export function parseUrlParams(url) {
const queryString = url.split("?")[1];
if (!queryString) return {};
return queryString.split("&").reduce((params, param) => {
const [key, value] = param.split("=");
return { ...params, [key]: value };
}, {});
}
/**
* @param {StickyNote} stickyNote
* @returns {boolean} Whether the given sticky note is applicable to the current site/page
*/
export function isStickyNoteApplicable(stickyNote) {
const stickyNoteUrl = stickyNote.site;
const currentUrl = window.location.href;
const stickyNoteWebsite = stickyNoteUrl.split("?")[0];
const currentWebsite = currentUrl.split("?")[0];
if (stickyNoteWebsite !== currentWebsite) {
return false;
}
const stickyNoteParams = parseUrlParams(stickyNoteUrl);
const currentParams = parseUrlParams(currentUrl);
if (window.location.hostname === "www.youtube.com") {
if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) {
return false;
}
}
return true;
}
/**
* @param {StickyNote} stickyNote
* @param {() => void} onSave
* @param {() => void} onDelete
* @returns {HTMLElement}
*/
export function renderStickyNote(stickyNote, onSave, onDelete) {
let html = `
<div class="birb-window-header">
<div class="birb-window-title">Sticky Note</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
<textarea class="birb-sticky-note-input" style="width: 150px;" placeholder="Write your notes here and they'll stick to the page!">${stickyNote.content}</textarea>
</div>`
const noteElement = makeElement("birb-window");
noteElement.classList.add("birb-sticky-note");
noteElement.innerHTML = html;
noteElement.style.top = `${stickyNote.top}px`;
noteElement.style.left = `${stickyNote.left}px`;
document.body.appendChild(noteElement);
makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => {
stickyNote.top = top;
stickyNote.left = left;
onSave();
});
const closeButton = noteElement.querySelector(".birb-window-close");
if (closeButton) {
makeClosable(() => {
if (confirm("Are you sure you want to delete this sticky note?")) {
onDelete();
noteElement.remove();
}
}, closeButton);
}
const textarea = noteElement.querySelector(".birb-sticky-note-input");
if (textarea && textarea instanceof HTMLTextAreaElement) {
let saveTimeout;
// Save after debounce
textarea.addEventListener("input", () => {
stickyNote.content = textarea.value;
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveTimeout = setTimeout(() => {
onSave();
}, 250);
});
}
// On window resize
window.addEventListener("resize", () => {
const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`;
const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`;
noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`;
});
return noteElement;
}
/**
* @param {StickyNote[]} stickyNotes
* @param {() => void} onSave
* @param {(note: StickyNote) => void} onDelete
*/
export function drawStickyNotes(stickyNotes, onSave, onDelete) {
// Remove all existing sticky notes
const existingNotes = document.querySelectorAll(".birb-sticky-note");
existingNotes.forEach(note => note.remove());
// Render all sticky notes
for (let stickyNote of stickyNotes) {
if (isStickyNoteApplicable(stickyNote)) {
renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
}
}
}
/**
* @param {StickyNote[]} stickyNotes
* @param {() => void} onSave
* @param {(note: StickyNote) => void} onDelete
*/
export function createNewStickyNote(stickyNotes, onSave, onDelete) {
const id = Date.now().toString();
const site = window.location.href;
const stickyNote = new StickyNote(id, site, "");
const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`;
stickyNote.top = parseInt(element.style.top, 10);
stickyNote.left = parseInt(element.style.left, 10);
stickyNotes.push(stickyNote);
onSave();
}

View File

@@ -13,13 +13,18 @@
#birb { #birb {
image-rendering: pixelated; image-rendering: pixelated;
position: absolute; position: fixed;
bottom: 0;
transform: scale(var(--birb-scale)) !important; transform: scale(var(--birb-scale)) !important;
transform-origin: bottom; transform-origin: bottom;
z-index: 2147483638 !important; z-index: 2147483638 !important;
cursor: pointer; cursor: pointer;
} }
.birb-absolute {
position: absolute !important;
}
.birb-decoration { .birb-decoration {
image-rendering: pixelated; image-rendering: pixelated;
position: fixed; position: fixed;
@@ -236,8 +241,8 @@
flex-direction: row; flex-direction: row;
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
padding-left: 15px; padding-left: 10px;
padding-right: 15px; padding-right: 10px;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -284,12 +289,12 @@
} }
.birb-field-guide-description { .birb-field-guide-description {
width: calc(100% - 16px); width: calc(100% - 20px);
margin-top: 10px; margin-top: 5px;
padding: 8px; padding: 8px;
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
margin-bottom: 6px; margin-bottom: 10px;
font-size: 14px; font-size: 14px;
box-sizing: border-box; box-sizing: border-box;
color: rgb(124, 108, 75); color: rgb(124, 108, 75);