65 Commits

Author SHA1 Message Date
Idrees Hassan
45e53d7697 Update manifest.json 2025-11-15 14:56:31 -05:00
Idrees Hassan
bc794c99d5 Update distributed code to fix review issues 2025-11-15 14:53:41 -05:00
Idrees
45626fda25 Update README with Obsidian section
Added a section for Obsidian with a note about upcoming features.
2025-11-14 17:19:58 -05:00
Idrees Hassan
322b0d0b90 Add obsidian manifest to root 2025-11-14 17:08:22 -05:00
Idrees
8c2b9fe8ed Merge pull request #4 from IdreesInc/obsidian
Finish obsidian plugin
2025-11-14 16:44:10 -05:00
Idrees Hassan
db78557088 Update build and sticky notes 2025-11-14 16:43:49 -05:00
Idrees Hassan
1175c40fa2 Add conditional menu items and disable sticky notes at root 2025-11-14 00:28:14 -05:00
Idrees Hassan
7639c7c36a Add sticky note support to obsidian 2025-11-14 00:06:49 -05:00
Idrees Hassan
3ec124a1b3 Move sticky note element to context 2025-11-13 22:53:44 -05:00
Idrees Hassan
fbbfd25f30 Allow landing on fixed elements in obsidian 2025-11-13 20:44:11 -05:00
Idrees Hassan
042d0e3b07 Remove top margin override and add more elements 2025-11-13 19:53:10 -05:00
Idrees
bad4df8e4a Merge pull request #3 from IdreesInc/obsidian
Add support for Obsidian
2025-11-13 18:41:57 -05:00
Idrees Hassan
71bb8204e2 Add focusing on obsidian elements 2025-11-13 18:41:37 -05:00
Idrees Hassan
c312500f19 Expose obsidian plugin api and allow saving/loading 2025-11-13 18:14:44 -05:00
Idrees Hassan
d94e321b66 Add sticky note setting to context 2025-11-13 18:00:06 -05:00
Idrees Hassan
d7dca478d6 Move sticky note path checking to context 2025-11-13 17:46:03 -05:00
Idrees Hassan
40169dd474 Add obsidian plugin unloading 2025-11-13 17:20:04 -05:00
Idrees Hassan
e99a61a4c6 Create working obsidian plugin boilerplate 2025-11-13 17:10:50 -05:00
Idrees Hassan
807bf1019f Merge branch 'main' into obsidian 2025-11-12 20:31:29 -05:00
Idrees Hassan
36c71f49a2 Update README.md 2025-11-11 15:26:05 -05:00
Idrees Hassan
38edec9d8c Update README.md 2025-11-11 14:54:24 -05:00
Idrees Hassan
466cf06489 Update README.md 2025-11-11 14:52:22 -05:00
Idrees Hassan
e40b9c347a Update README.md 2025-11-11 14:52:07 -05:00
Idrees Hassan
217e882b9b Update README.md 2025-11-11 14:50:56 -05:00
Idrees Hassan
629bb2e545 Add README 2025-11-11 14:49:51 -05:00
Idrees
49b230d08c Add license 2025-11-11 14:26:59 -05:00
Idrees Hassan
ea0a14f08c Update build to use build cache 2025-11-03 22:05:25 -05:00
Idrees Hassan
2826077d7a Add obsidian context 2025-11-03 21:51:56 -05:00
Idrees Hassan
e50c2c8a1f Add obsidian build 2025-11-03 21:35:27 -05:00
Idrees Hassan
3fe52e5492 Use constants for paths 2025-11-03 21:21:29 -05:00
Idrees Hassan
3d31a9c9a6 Use constant for browser manifest path 2025-11-03 21:09:28 -05:00
Idrees Hassan
4d0649a3d1 Rename browser manifest file before compilation 2025-11-03 20:46:12 -05:00
Idrees Hassan
83683d70f3 Change build logging 2025-11-03 19:39:48 -05:00
Idrees Hassan
2d95496fd9 Compress extension in build 2025-11-02 21:40:25 -05:00
Idrees Hassan
6e40e658bf Add font directly to extension 2025-11-02 15:06:21 -05:00
Idrees Hassan
49b32eb934 Update descriptions 2025-11-02 15:02:58 -05:00
Idrees Hassan
1c70c0cc13 Build into separate folders 2025-11-02 14:56:37 -05:00
Idrees Hassan
8a6b1a584e Add browser extension context 2025-11-02 14:45:18 -05:00
Idrees Hassan
18749acff6 Add context management 2025-11-02 14:16:24 -05:00
Idrees Hassan
32a871b773 Add inset feather debug option 2025-10-29 19:31:09 -04:00
Idrees Hassan
92dfea998f Ensure bird doesn't fly up too far 2025-10-29 19:17:05 -04:00
Idrees Hassan
ea1f08c80d Filter out invisible elements 2025-10-28 23:28:12 -04:00
Idrees Hassan
1061f978b5 Add delay before hopping repeatedly 2025-10-28 23:16:57 -04:00
Idrees Hassan
66284b0af8 Fix bird not flying to right location when iOS address bar is minimized 2025-10-28 23:11:46 -04:00
Idrees Hassan
2c0438cc0a Increase mobile fly speed 2025-10-28 21:50:09 -04:00
Idrees Hassan
64a48d6cad Reduce flying arc and hop distance 2025-10-28 21:13:51 -04:00
Idrees Hassan
d86253c8bf Increase min focus top again 2025-10-28 20:45:47 -04:00
Idrees Hassan
4df4d710d1 Lower AFK time again 2025-10-28 20:43:38 -04:00
Idrees Hassan
a456b2269c Only draw frames when necessary 2025-10-28 20:42:08 -04:00
Idrees Hassan
72641cc818 Increase AFK time 2025-10-28 17:46:15 -04:00
Idrees Hassan
66ac614abe Reduce AFK time 2025-10-28 17:36:31 -04:00
Idrees Hassan
314ded2562 Allow bird to follow sticky notes 2025-10-28 17:15:19 -04:00
Idrees Hassan
c832011011 Add app icons 2025-10-27 22:19:16 -04:00
Idrees Hassan
d36e90dfc7 Allow bird to land on sticky notes 2025-10-26 23:43:09 -04:00
Idrees Hassan
0d1e63888d Don't let the bird land on sticky things 2025-10-26 21:37:01 -04:00
Idrees Hassan
56a999a235 Add back iframe detection 2025-10-26 21:26:40 -04:00
Idrees Hassan
cda160414f Only confirm for non-empty sticky notes 2025-10-26 20:47:36 -04:00
Idrees Hassan
1752189e4f Fix all innerHTML calls 2025-10-26 20:45:59 -04:00
Idrees Hassan
69c0d0c1bc Remove some innerHTML setting and add version 2025-10-26 20:23:47 -04:00
Idrees Hassan
5818aadad6 Remove logging message 2025-10-26 19:46:55 -04:00
Idrees Hassan
0441f1dc87 Partially fix field guide formatting 2025-10-26 19:46:13 -04:00
Idrees Hassan
f2496d5d0a Decrease width of field guide 2025-10-26 19:34:17 -04:00
Idrees Hassan
f422a699f5 Reduce redundant field guide code 2025-10-26 18:44:37 -04:00
Idrees Hassan
d623abee85 Rename sprite enum 2025-10-26 18:33:38 -04:00
Idrees
2c0e9018eb Merge pull request #2 from IdreesInc/rollup
Implement Rollup and split up code
2025-10-26 18:19:37 -04:00
108 changed files with 10036 additions and 1411 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
/node_modules
.DS_Store
/dist/birb.bundled.js
obsidian-test.sh
build-cache.json

373
LICENSE Normal file
View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -1,8 +1,81 @@
# Pocket Bird (Work in Progress!)
# Pocket Bird
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!
![License](https://img.shields.io/github/license/IdreesInc/Pocket-Bird)
[![Chrome Web Store Version](https://img.shields.io/chrome-web-store/v/lbbdngkbbgaecefacpnhnhleggabghak)](https://chromewebstore.google.com/detail/pocket-bird/lbbdngkbbgaecefacpnhnhleggabghak)
[![Mozilla Add-on Version](https://img.shields.io/amo/v/pocket-bird)](https://addons.mozilla.org/en-US/firefox/addon/pocket-bird/)
[![Discord](https://img.shields.io/discord/1398471368403583120?logo=discord&logoColor=fff&label=discord&color=5865F2)](https://discord.gg/6yxE9prcNc)
![](images/preview.png)
It's a pet bird that hops around your computer, what more could you want?
### Get it for [Google Chrome](https://chromewebstore.google.com/detail/pocket-bird/lbbdngkbbgaecefacpnhnhleggabghak)
### Get it for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/pocket-bird/)
#### Join the [Discord](https://discord.gg/6yxE9prcNc) to help me beta test new features and suggest ideas!
## Features
- A cute little pixel art bird hops around your apps and websites
- Collect rare falling feathers to unlock over 10+ different species of birds
- Add sticky notes that stay on the page even after you refresh
- And most importantly, you can pet the bird!
## Adoption Guide
### Google Chrome + Microsoft Edge
1. Go to the [Chrome Web Store page](https://chromewebstore.google.com/detail/pocket-bird/lbbdngkbbgaecefacpnhnhleggabghak)
2. Click "Add to Chrome" (or "Add to Edge" if using Microsoft Edge)
3. Confirm any permission prompts that appear
### Mozilla Firefox
1. Go to the [Mozilla Add-ons page](https://addons.mozilla.org/en-US/firefox/addon/pocket-bird/)
2. Click "Add to Firefox"
3. Confirm any permission prompts that appear
### Obsidian
_Coming soon!_
### Userscript
*Note that this is mainly used for beta testing new features, installation via browser extension is recommended for the best experience.*
1. Install [Tampermonkey](https://www.tampermonkey.net/) on your web browser
2. Enable the Tampermonkey extension and give it the permissions requested
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)
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!
## FAQ
### How do I pet the bird?
Simply move your cursor back and forth over your bird until a heart appears! You can also click the bird to open the menu and pet it from there. There may even be a slightly greater chance of finding a feather when your bird is well loved...
### How do I collect feathers?
Feathers will occasionally fall from the top of your window. Clicking on a feather will add a new species to your field guide, allowing you to change the appearance of your pet!
### How do I change my bird's appearance?
Once you've unlocked new species by collecting feathers, you can change your bird's appearance by opening the Pocket Bird menu (via clicking the bird) and selecting "Field Guide".
### How do I add sticky notes?
Open the Pocket Bird menu by clicking the bird and select "Sticky Note". From there, you can add, edit, and delete notes that will stay on the page even after refreshing.
### How do I hide the bird?
Open the Pocket Bird menu by clicking the bird and select "Settings". From there, you can toggle the bird's visibility on and off temporarily on the current page.
### Why does Pocket Bird need permission to read and change my data on websites I visit?
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!
## 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!
Also feel free to check out my other open-source projects like [Monocraft](https://github.com/IdreesInc/Monocraft), [PicoChat](https://github.com/IdreesInc/PicoChat), and more on [my website](https://idreesinc.com/)!

192
build.js
View File

@@ -1,84 +1,101 @@
// @ts-check
import { rollup } from 'rollup';
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, cpSync, createWriteStream } from 'fs';
import archiver from 'archiver';
// Path constants
const BUILD_CACHE_PATH = "./build-cache.json";
const SRC_DIR = "./src";
const SPRITES_DIR = "./sprites";
const IMAGES_DIR = "./images";
const FONTS_DIR = "./fonts";
const DIST_DIR = "./dist";
const BROWSER_MANIFEST = "./platform-specific/extension/manifest.json";
const OBSIDIAN_MANIFEST = "./platform-specific/obsidian/manifest.json";
const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt";
const OBSIDIAN_WRAPPER = "./platform-specific/obsidian/wrapper.js";
const USERSCRIPT_DIR = DIST_DIR + "/userscript";
const EXTENSION_DIR = DIST_DIR + "/extension";
const OBSIDIAN_DIR = DIST_DIR + "/obsidian";
const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css";
const APPLICATION_ENTRY = SRC_DIR + "/application.js";
const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js";
const BIRB_OUTPUT = DIST_DIR + "/birb.js";
const MONOCRAFT_URL = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
const VERSION_KEY = "__VERSION__";
const STYLESHEET_KEY = "___STYLESHEET___";
const MONOCRAFT_SRC_KEY = "__MONOCRAFT_SRC__";
const CODE_KEY = "__CODE__";
const spriteSheets = [
{
key: "__SPRITE_SHEET__",
path: "./sprites/birb.png"
path: SPRITES_DIR + "/birb.png"
},
{
key: "__FEATHER_SPRITE_SHEET__",
path: "./sprites/feather.png"
path: SPRITES_DIR + "/feather.png"
}
];
const STYLESHEET_PATH = "./src/stylesheet.css";
const STYLESHEET_KEY = "___STYLESHEET___";
/** @type {Record<string, any>} */
let buildCache = {};
try {
const cacheContent = readFileSync(BUILD_CACHE_PATH, 'utf8');
buildCache = JSON.parse(cacheContent);
} catch (e) {
console.warn("No build cache found, starting fresh");
}
const now = new Date();
const versionDate = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`;
// Get current build number from manifest.json
// Get current build number from the build cache
let buildNumber = 0;
try {
const manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
if (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;
}
}
if (buildCache.version && buildCache.version.startsWith(versionDate)) {
// Same day, increment build number
const parts = buildCache.version.split('.');
if (parts.length === 4) {
buildNumber = parseInt(parts[3], 10) + 1;
}
} catch (e) {
console.error("Could not read version from manifest.json");
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 =
`// ==UserScript==
// @name Pocket Bird
// @namespace https://idreesinc.com
// @version ${version}
// @description birb
// @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-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 *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// ==/UserScript==
// Update build cache
buildCache.version = version;
writeFileSync(BUILD_CACHE_PATH, JSON.stringify(buildCache), 'utf8');
`;
// =============================================
// Build JavaScript function
// =============================================
// Bundle with rollup
const bundle = await rollup({
input: 'src/application.js',
input: APPLICATION_ENTRY,
});
await bundle.write({
file: 'dist/birb.bundled.js',
file: BUNDLED_OUTPUT,
format: 'iife',
});
await bundle.close();
let birbJs = readFileSync('dist/birb.bundled.js', 'utf8');
let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8');
// Delete bundled file
unlinkSync(BUNDLED_OUTPUT);
// Replace version placeholder
birbJs = birbJs.replaceAll(VERSION_KEY, version);
// Compile and insert sprite sheets
for (const spriteSheet of spriteSheets) {
@@ -88,14 +105,81 @@ for (const spriteSheet of spriteSheets) {
// Insert stylesheet
const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8');
birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent);
birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent).replace(MONOCRAFT_SRC_KEY, MONOCRAFT_URL);
// Build standard javascript file
writeFileSync('./dist/birb.js', birbJs);
// Delete bundled file
unlinkSync('./dist/birb.bundled.js');
// Write bundled JavaScript function
writeFileSync(BIRB_OUTPUT, birbJs);
// Build user script
const userScript = userScriptHeader + birbJs;
writeFileSync('./dist/birb.user.js', userScript);
// =============================================
// Build userscript
// =============================================
// Get userscript header
const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version);
mkdirSync(USERSCRIPT_DIR, { recursive: true });
const userScript = userScriptHeader + "\n" + birbJs;
writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript);
// =============================================
// Build browser extension
// =============================================
mkdirSync(EXTENSION_DIR, { recursive: true });
// Copy birb.js
writeFileSync(EXTENSION_DIR + '/birb.js', birbJs);
// Copy manifest.json
let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8');
browserManifest = browserManifest.replace(VERSION_KEY, version);
writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest);
// Copy icons folder
mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true });
cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true });
// Copy fonts folder
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`);
});
archive.on('error', (err) => {
throw err;
});
archive.pipe(output);
archive.directory(EXTENSION_DIR + '/', false);
archive.finalize();
// =============================================
// Build Obsidian plugin
// =============================================
mkdirSync(OBSIDIAN_DIR, { recursive: true });
// Wrap birb.js with plugin boilerplate
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
const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64');
const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`;
obsidianPlugin = obsidianPlugin.replace(MONOCRAFT_URL, monocraftDataUri);
// Create main.js with plugin code
writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin);
// Copy manifest.json
let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8');
obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`);
writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest);
console.log(`Build complete: ${version}`);

1309
dist/birb.js vendored

File diff suppressed because it is too large Load Diff

BIN
dist/extension.zip vendored Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

BIN
dist/extension/fonts/Monocraft.otf vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

47
dist/extension/manifest.json vendored Normal file
View File

@@ -0,0 +1,47 @@
{
"manifest_version": 3,
"name": "Pocket Bird",
"description": "It's a pet bird in your browser, what more could you want?",
"version": "2025.11.14.205",
"homepage_url": "https://idreesinc.com",
"icons": {
"48": "images/icons/transparent/48x48x1.png",
"96": "images/icons/transparent/96x96x1.png",
"128": "images/icons/transparent/128x128x1.png"
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"birb.js"
]
}
],
"permissions": [
"storage",
"activeTab"
],
"web_accessible_resources": [
{
"resources": [
"images/*",
"fonts/Monocraft.otf"
],
"matches": [
"<all_urls>"
]
}
],
"browser_specific_settings": {
"gecko": {
"id": "pocket-bird@idreesinc.com",
"data_collection_permissions": {
"required": [
"none"
]
}
}
}
}

2427
dist/obsidian/main.js vendored Normal file

File diff suppressed because it is too large Load Diff

10
dist/obsidian/manifest.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"id": "pocket-bird",
"name": "Pocket Bird",
"version": "2025.11.14",
"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
}

372
dist/obsidian/styles.css vendored Normal file

File diff suppressed because one or more lines are too long

2832
dist/userscript/birb.user.js vendored Normal file

File diff suppressed because it is too large Load Diff

BIN
fonts/Monocraft.otf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

View File

@@ -1,36 +1,10 @@
{
"manifest_version": 3,
"id": "pocket-bird",
"name": "Pocket Bird",
"description": "It's a bird, in your browser. What more could you want?",
"version": "2025.10.26.402",
"homepage_url": "https://idreesinc.com",
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"./dist/birb.js"
]
}
],
"permissions": [
"storage",
"activeTab"
],
"web_accessible_resources": [
{
"resources": [
"images/*"
],
"matches": [
"<all_urls>"
]
}
],
"browser_specific_settings": {
"gecko": {
"id": "birb@idreesinc.com"
}
}
"version": "2025.11.14.205",
"minAppVersion": "0.15.0",
"description": "Add a pet bird to fly around your notes and keep you company!",
"author": "Idrees Hassan",
"authorUrl": "https://idreesinc.com",
"isDesktopOnly": false
}

1001
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,47 @@
{
"manifest_version": 3,
"name": "Pocket Bird",
"description": "It's a pet bird in your browser, what more could you want?",
"version": "__VERSION__",
"homepage_url": "https://idreesinc.com",
"icons": {
"48": "images/icons/transparent/48x48x1.png",
"96": "images/icons/transparent/96x96x1.png",
"128": "images/icons/transparent/128x128x1.png"
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"birb.js"
]
}
],
"permissions": [
"storage",
"activeTab"
],
"web_accessible_resources": [
{
"resources": [
"images/*",
"fonts/Monocraft.otf"
],
"matches": [
"<all_urls>"
]
}
],
"browser_specific_settings": {
"gecko": {
"id": "pocket-bird@idreesinc.com",
"data_collection_permissions": {
"required": [
"none"
]
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"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
}

View File

@@ -0,0 +1,15 @@
const { Plugin, Notice } = require('obsidian');
module.exports = class PocketBird extends Plugin {
onload() {
console.log("Loading Pocket Bird version __VERSION__...");
const OBSIDIAN_PLUGIN = this;
__CODE__
console.log("Pocket Bird loaded!");
}
onunload() {
// Remove the birb when the plugin is unloaded
document.getElementById('birb')?.remove();
console.log('Pocket Bird unloaded!');
}
};

View File

@@ -0,0 +1,13 @@
// ==UserScript==
// @name Pocket Bird
// @namespace https://idreesinc.com
// @version __VERSION__
// @description It's a pet bird in your browser, what more could you want?
// @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js
// @updateURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// ==/UserScript==

View File

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

View File

@@ -1,5 +1,5 @@
import { Directions } from './shared.js';
import { SPRITE, BirdType } from './sprites.js';
import { Sprite, BirdType } from './sprites.js';
import Layer from './layer.js';
class Frame {
@@ -25,7 +25,7 @@ class Frame {
this.pixels = layers[0].pixels.map(row => row.slice());
// Pad from top with transparent pixels
while (this.pixels.length < maxHeight) {
this.pixels.unshift(new Array(this.pixels[0].length).fill(SPRITE.TRANSPARENT));
this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT));
}
// Combine layers
for (let i = 1; i < layers.length; i++) {
@@ -34,7 +34,7 @@ class Frame {
let topMargin = maxHeight - layerPixels.length;
for (let y = 0; y < layerPixels.length; y++) {
for (let x = 0; x < layerPixels[y].length; x++) {
this.pixels[y + topMargin][x] = layerPixels[y][x] !== SPRITE.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x];
this.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x];
}
}
}
@@ -58,6 +58,9 @@ class Frame {
* @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];

View File

@@ -11,12 +11,49 @@ class Anim {
this.frames = frames;
this.durations = durations;
this.loop = loop;
this.lastFrameIndex = -1;
this.lastDirection = null;
this.lastTimeStart = null;
}
getAnimationDuration() {
return this.durations.reduce((a, b) => a + b, 0);
}
/**
* Get the current frame index based on elapsed time
* @param {number} time The elapsed time since animation start
* @returns {number} The index of the current frame
*/
getCurrentFrameIndex(time) {
let totalDuration = 0;
for (let i = 0; i < this.durations.length; i++) {
totalDuration += this.durations[i];
if (time < totalDuration) {
return i;
}
}
return this.frames.length - 1;
}
/**
* Clear the cached frame state
*/
#clearCache() {
this.lastFrameIndex = -1;
this.lastDirection = null;
}
/**
* Check if the frame needs to be redrawn
* @param {number} frameIndex The current frame index
* @param {number} direction The current direction
* @returns {boolean} Whether the frame needs to be redrawn
*/
#shouldRedraw(frameIndex, direction) {
return frameIndex !== this.lastFrameIndex || direction !== this.lastDirection;
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {number} direction
@@ -26,22 +63,29 @@ class Anim {
* @returns {boolean} Whether the animation is complete
*/
draw(ctx, direction, timeStart, canvasPixelSize, species) {
// Reset cache if animation was restarted
if (this.lastTimeStart !== timeStart) {
this.#clearCache();
this.lastTimeStart = timeStart;
}
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;
}
const currentFrameIndex = this.getCurrentFrameIndex(time);
if (this.#shouldRedraw(currentFrameIndex, direction)) {
this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, species);
this.lastFrameIndex = currentFrameIndex;
this.lastDirection = direction;
}
// Draw the last frame if the animation is complete
this.frames[this.frames.length - 1].draw(ctx, direction, canvasPixelSize, species);
return true;
// Return whether animation is complete (for non-looping animations)
return !this.loop && time >= duration;
}
}

View File

@@ -2,6 +2,7 @@ import Frame from './frame.js';
import Layer from './layer.js';
import Anim from './anim.js';
import { Birb, Animations } from './birb.js';
import { getContext, ObsidianContext } from './context.js';
import {
Directions,
@@ -15,10 +16,11 @@ import {
log,
debug,
error,
getLayer
getLayer,
getWindowHeight
} from './shared.js';
import {
SPRITE,
Sprite,
SPRITE_SHEET_COLOR_MAP,
SPECIES
} from './sprites.js';
@@ -29,6 +31,7 @@ import {
} from './stickyNotes.js';
import {
MenuItem,
ConditionalMenuItem,
DebugMenuItem,
Separator,
insertMenu,
@@ -80,18 +83,19 @@ const DEFAULT_BIRD = "bluebird";
// Birb movement
const HOP_SPEED = 0.07;
const FLY_SPEED = isMobile() ? 0.125 : 0.25;
const HOP_DISTANCE = 45;
const FLY_SPEED = isMobile() ? 0.175 : 0.25;
const HOP_DISTANCE = 35;
// Timing constants (in milliseconds)
const UPDATE_INTERVAL = 1000 / 60; // 60 FPS
const AFK_TIME = isDebug() ? 0 : 1000 * 30;
const AFK_TIME = isDebug() ? 0 : 1000 * 5;
const PET_BOOST_DURATION = 1000 * 60 * 5;
const PET_MENU_COOLDOWN = 1000;
const URL_CHECK_INTERVAL = 500;
const URL_CHECK_INTERVAL = 150;
const HOP_DELAY = 500;
// Random event chances per tick
const HOP_CHANCE = 1 / (60 * 3); // Every 3 seconds
const HOP_CHANCE = 1 / (60 * 2.5); // Every 2.5 seconds
const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds
const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours
@@ -101,7 +105,6 @@ const PET_FEATHER_BOOST = 2;
// Focus element constraints
const MIN_FOCUS_ELEMENT_WIDTH = 100;
const MIN_FOCUS_ELEMENT_TOP = 80;
/** @type {Partial<Settings>} */
let userSettings = {};
@@ -138,7 +141,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
const b = pixels[index + 2];
const a = pixels[index + 3];
if (a === 0) {
row.push(SPRITE.TRANSPARENT);
row.push(Sprite.TRANSPARENT);
continue;
}
const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
@@ -148,7 +151,7 @@ function loadSpriteSheetPixels(dataUri, templateColors = true) {
}
if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) {
error(`Unknown color: ${hex}`);
row.push(SPRITE.TRANSPARENT);
row.push(Sprite.TRANSPARENT);
}
row.push(SPRITE_SHEET_COLOR_MAP[hex]);
}
@@ -191,8 +194,8 @@ Promise.all([
const menuItems = [
new MenuItem(`Pet ${birdBirb()}`, pet),
new MenuItem("Field Guide", insertFieldGuide),
new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote)),
new MenuItem(`Hide ${birdBirb()}`, hideBirb),
new ConditionalMenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote), () => getContext().areStickyNotesEnabled()),
new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)),
new DebugMenuItem("Freeze/Unfreeze", () => {
frozen = !frozen;
}),
@@ -202,6 +205,9 @@ Promise.all([
unlockBird(type);
}
}),
new DebugMenuItem("Add Feather", () => {
activateFeather();
}),
new DebugMenuItem("Disable Debug", () => {
setDebug(false);
}),
@@ -215,8 +221,17 @@ Promise.all([
new MenuItem("Toggle Birb Mode", () => {
userSettings.birbMode = !userSettings.birbMode;
save();
insertModal(`${birdBirb()} Mode`, `Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"${userSettings.birbMode ? "\n\nWelcome back to 2012" : ""}`);
})
const message = makeElement("birb-message-content");
message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`));
if (userSettings.birbMode) {
message.appendChild(document.createElement("br"));
message.appendChild(document.createElement("br"));
message.appendChild(document.createTextNode("Welcome back to 2012"));
}
insertModal(`${birdBirb()} Mode`, message);
}),
new Separator(),
new MenuItem("__VERSION__", () => { alert("Thank you for using Pocket Bird! You are on version: __VERSION__") }, false),
];
const styleElement = document.createElement("style");
@@ -251,43 +266,18 @@ Promise.all([
let petStack = [];
let currentSpecies = DEFAULT_BIRD;
let unlockedSpecies = [DEFAULT_BIRD];
let visible = true;
// let visible = true;
let lastPetTimestamp = 0;
/** @type {StickyNote[]} */
let stickyNotes = [];
/**
* @returns {boolean} Whether the script is running in a userscript extension context
*/
function isUserScript() {
// @ts-expect-error
return typeof GM_getValue === "function";
}
function isTestEnvironment() {
return window.location.hostname === "127.0.0.1"
|| window.location.hostname === "localhost"
|| window.location.hostname.startsWith("192.168.");
}
function load() {
/** @type {Record<string, any>} */
let saveData = {};
if (isUserScript()) {
log("Loading save data from UserScript storage");
// @ts-expect-error
saveData = GM_getValue("birbSaveData", {}) ?? {};
} else if (isTestEnvironment()) {
log("Test environment detected, loading save data from localStorage");
saveData = JSON.parse(localStorage.getItem("birbSaveData") ?? "{}");
} else {
log("Not a UserScript");
}
async function load() {
/** @type {BirbSaveData|Object} */
let saveData = await getContext().getSaveData();
debug("Loaded data: " + JSON.stringify(saveData));
if (!saveData.settings) {
if (!('settings' in saveData)) {
log("No user settings found in save data, starting fresh");
}
@@ -326,29 +316,11 @@ Promise.all([
}));
}
if (isUserScript()) {
log("Saving data to UserScript storage");
// @ts-expect-error
GM_setValue("birbSaveData", saveData);
} else if (isTestEnvironment()) {
log("Test environment detected, saving data to localStorage");
localStorage.setItem("birbSaveData", JSON.stringify(saveData));
} else {
log("Not a UserScript");
}
getContext().putSaveData(saveData);
}
function resetSaveData() {
if (isUserScript()) {
log("Resetting save data in UserScript storage");
// @ts-expect-error
GM_deleteValue("birbSaveData");
} else if (isTestEnvironment()) {
log("Test environment detected, resetting save data in localStorage");
localStorage.removeItem("birbSaveData");
} else {
log("Not a UserScript");
}
getContext().resetSaveData();
load();
}
@@ -368,36 +340,19 @@ Promise.all([
}
function init() {
log("Sprite sheets loaded successfully, initializing bird...");
if (window !== window.top) {
// Skip installation if within an iframe
log("In iframe, skipping Birb script initialization");
return;
}
log("Sprite sheets loaded successfully, initializing bird...");
// Preload font
const MONOCRAFT_SRC = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
const fontLink = document.createElement("link");
fontLink.rel = "stylesheet";
fontLink.href = `url(${MONOCRAFT_SRC}) format('opentype')`;
document.head.appendChild(fontLink);
load().then(onLoad);
}
// Add stylesheet font-face
const fontFace = `
@font-face {
font-family: 'Monocraft';
src: url(${MONOCRAFT_SRC}) format('opentype');
font-weight: normal;
font-style: normal;
}
`;
const fontStyle = document.createElement("style");
fontStyle.innerHTML = fontFace;
document.head.appendChild(fontStyle);
load();
styleElement.innerHTML = STYLESHEET;
function onLoad() {
styleElement.textContent = STYLESHEET;
document.head.appendChild(styleElement);
birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT);
@@ -446,17 +401,19 @@ Promise.all([
drawStickyNotes(stickyNotes, save, deleteStickyNote);
let lastUrl = (window.location.href ?? "").split("?")[0];
let lastPath = getContext().getPath().split("?")[0];
setInterval(() => {
const currentUrl = (window.location.href ?? "").split("?")[0];
if (currentUrl !== lastUrl) {
log("URL changed, updating sticky notes");
lastUrl = currentUrl;
const currentPath = getContext().getPath().split("?")[0];
if (currentPath !== lastPath) {
log("Path changed, updating sticky notes: " + currentPath);
lastPath = currentPath;
drawStickyNotes(stickyNotes, save, deleteStickyNote);
}
}, URL_CHECK_INTERVAL);
setInterval(update, UPDATE_INTERVAL);
focusOnElement(true);
}
function update() {
@@ -464,12 +421,12 @@ Promise.all([
// Hide bird if the browser is fullscreen
if (document.fullscreenElement) {
hideBirb();
birb.setVisible(false);
// Won't be restored on fullscreen exit
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== Animations.HEART) {
if (Date.now() - stateStart > HOP_DELAY && Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
// Idle for a while, do something
@@ -491,7 +448,7 @@ Promise.all([
// Double the chance of a feather if recently pet
const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1;
if (visible && Math.random() < FEATHER_CHANCE * petMod) {
if (birb.isVisible() && Math.random() < FEATHER_CHANCE * petMod) {
lastPetTimestamp = 0;
activateFeather();
}
@@ -501,7 +458,7 @@ Promise.all([
function draw() {
requestAnimationFrame(draw);
if (!birb.isVisible()) {
if (!birb || !birb.isVisible()) {
return;
}
@@ -510,12 +467,12 @@ Promise.all([
// Update the bird's position
if (currentState === States.IDLE) {
if (focusedElement && !isWithinHorizontalBounds()) {
focusOnGround();
flySomewhere();
}
birdY = getFocusedY();
} else if (currentState === States.FLYING) {
// Fly to target location (even if in the air)
if (updateParabolicPath(FLY_SPEED)) {
if (updateParabolicPath(FLY_SPEED, 2)) {
setState(States.IDLE);
}
}
@@ -524,15 +481,21 @@ Promise.all([
targetY = getFocusedY();
// Adjust startY to account for scrolling
startY += targetY - oldTargetY;
if (targetY < 0 || targetY > window.innerHeight) {
// Fly to ground if the focused element moves out of bounds
focusOnGround();
if (targetY < 0 || targetY > getWindowHeight()) {
// Fly to another element or the ground if the focused element moves out of bounds
flySomewhere();
}
if (birb.draw(SPECIES[currentSpecies])) {
birb.setAnimation(Animations.STILL);
}
// Clamp startY, birdY, targetY to a bit above the top of the window
const maxY = getWindowHeight() * 1.5;
startY = Math.min(startY, maxY);
birdY = Math.min(birdY, maxY);
targetY = Math.min(targetY, maxY);
// Update HTML element position
birb.setX(birdX);
birb.setY(birdY);
@@ -550,34 +513,37 @@ Promise.all([
* Create a window element with header and content
* @param {string} id
* @param {string} title
* @param {string} contentHtml
* @param {HTMLElement} contentElement
* @param {() => void} [onClose]
* @returns {HTMLElement}
*/
function createWindow(id, title, contentHtml, onClose) {
function createWindow(id, title, contentElement, onClose) {
const window = makeElement("birb-window", undefined, id);
window.innerHTML = `
<div class="birb-window-header">
<div class="birb-window-title">${title}</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
${contentHtml}
</div>
`;
const header = makeElement("birb-window-header");
const titleElement = makeElement("birb-window-title");
titleElement.textContent = title;
const closeButton = makeElement("birb-window-close");
closeButton.textContent = "x";
header.appendChild(titleElement);
header.appendChild(closeButton);
const contentWrapper = makeElement("birb-window-content");
contentWrapper.appendChild(contentElement);
window.appendChild(header);
window.appendChild(contentWrapper);
document.body.appendChild(window);
makeDraggable(window.querySelector(".birb-window-header"));
makeDraggable(header);
const closeButton = window.querySelector(".birb-window-close");
if (closeButton) {
makeClosable(() => {
if (onClose) {
onClose();
}
window.remove();
}, closeButton);
}
makeClosable(() => {
if (onClose) {
onClose();
}
window.remove();
}, closeButton);
return window;
}
@@ -637,7 +603,13 @@ Promise.all([
function unlockBird(birdType) {
if (!unlockedSpecies.includes(birdType)) {
unlockedSpecies.push(birdType);
insertModal("New Bird Unlocked!", `You've found a <b>${SPECIES[birdType].name}</b> feather! Use the Field Guide to switch your bird's species.`);
const message = makeElement("birb-message-content");
message.appendChild(document.createTextNode("You've found a "));
const bold = document.createElement("b");
bold.textContent = SPECIES[birdType].name;
message.appendChild(bold);
message.appendChild(document.createTextNode(" feather! Use the Field Guide to switch your bird's species."));
insertModal("New Bird Unlocked!", message);
}
save();
}
@@ -648,8 +620,8 @@ Promise.all([
return;
}
const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED;
feather.style.top = `${Math.min(y, window.innerHeight - feather.offsetHeight)}px`;
if (y < window.innerHeight - feather.offsetHeight) {
feather.style.top = `${Math.min(y, getWindowHeight() - feather.offsetHeight)}px`;
if (y < getWindowHeight() - feather.offsetHeight) {
feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`;
}
}
@@ -659,23 +631,19 @@ Promise.all([
*/
function centerElement(element) {
element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
element.style.top = `${window.innerHeight / 2 - element.offsetHeight / 2}px`;
element.style.top = `${getWindowHeight() / 2 - element.offsetHeight / 2}px`;
}
/**
* @param {string} title
* @param {string} message
* @param {HTMLElement} content
*/
function insertModal(title, message) {
function insertModal(title, content) {
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
return;
}
const modal = createWindow("birb-modal", title, `
<div class="birb-message-content">
${message}
</div>
`);
const modal = createWindow("birb-modal", title, content);
modal.style.width = "270px";
centerElement(modal);
@@ -695,7 +663,7 @@ Promise.all([
// Right side
x -= (menu.offsetWidth + offset) * UI_CSS_SCALE;
}
if (y > window.innerHeight / 2) {
if (y > getWindowHeight() / 2) {
// Top side
y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE;
} else {
@@ -710,44 +678,40 @@ Promise.all([
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
return;
}
let html = `
<div class="birb-window-header">
<div class="birb-window-title">Field Guide</div>
<div class="birb-window-close">x</div>
</div>
<div class="birb-window-content">
<div class="birb-grid-content"></div>
<div class="birb-field-guide-description"></div>
</div>`
const fieldGuide = makeElement("birb-window", undefined, FIELD_GUIDE_ID);
fieldGuide.innerHTML = html;
document.body.appendChild(fieldGuide);
makeDraggable(fieldGuide.querySelector(".birb-window-header"));
const closeButton = fieldGuide.querySelector(".birb-window-close");
if (closeButton) {
makeClosable(() => {
fieldGuide.remove();
}, closeButton);
}
const contentContainer = document.createElement("div");
const content = makeElement("birb-grid-content");
const description = makeElement("birb-field-guide-description");
contentContainer.appendChild(content);
contentContainer.appendChild(description);
const content = fieldGuide.querySelector(".birb-grid-content");
if (!content) {
return;
}
content.innerHTML = "";
const fieldGuide = createWindow(
FIELD_GUIDE_ID,
"Field Guide",
contentContainer
);
const generateDescription = (/** @type {string} */ speciesId) => {
const type = SPECIES[speciesId];
const unlocked = unlockedSpecies.includes(speciesId);
return "<b>" + type.name + "</b><div style='height: 0.3em'></div>" + (!unlocked ? "Not yet unlocked" : type.description);
const boldName = document.createElement("b");
boldName.textContent = type.name;
const spacer = document.createElement("div");
spacer.style.height = "0.3em";
const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description);
const fragment = document.createDocumentFragment();
fragment.appendChild(boldName);
fragment.appendChild(spacer);
fragment.appendChild(descText);
return fragment;
};
const description = fieldGuide.querySelector(".birb-field-guide-description");
if (!description) {
return;
}
description.innerHTML = generateDescription(currentSpecies);
description.appendChild(generateDescription(currentSpecies));
for (const [id, type] of Object.entries(SPECIES)) {
const unlocked = unlockedSpecies.includes(id);
const speciesElement = makeElement("birb-grid-item");
@@ -776,11 +740,12 @@ Promise.all([
speciesElement.classList.add("birb-grid-item-locked");
}
speciesElement.addEventListener("mouseover", () => {
log("mouseover");
description.innerHTML = generateDescription(id);
description.textContent = "";
description.appendChild(generateDescription(id));
});
speciesElement.addEventListener("mouseout", () => {
description.innerHTML = generateDescription(currentSpecies);
description.textContent = "";
description.appendChild(generateDescription(currentSpecies));
});
}
centerElement(fieldGuide);
@@ -799,7 +764,7 @@ Promise.all([
function switchSpecies(type) {
currentSpecies = type;
// Update CSS variable --birb-highlight to be wing color
document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[SPRITE.THEME_HIGHLIGHT]);
document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[Sprite.THEME_HIGHLIGHT]);
save();
}
@@ -814,7 +779,7 @@ Promise.all([
const dy = targetY - startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const time = Date.now() - stateStart;
if (distance > Math.max(window.innerWidth, window.innerHeight) / 2) {
if (distance > Math.max(window.innerWidth, getWindowHeight()) / 2) {
speed *= 1.3;
}
const amount = Math.min(1, time / (distance / speed));
@@ -840,60 +805,114 @@ Promise.all([
}
function getFocusedY() {
return getFullWindowHeight() - focusedBounds.top;
return getWindowHeight() - focusedBounds.top;
}
/**
* @returns The render-safe height of the inner browser window
* Fly to either an element or the ground
*/
function getSafeWindowHeight() {
// Necessary because iOS 26 Safari is terrible and won't render
// fixed elements behind the address bar
return window.innerHeight;
}
/**
* @returns The true height of the inner browser window
*/
function getFullWindowHeight() {
return document.documentElement.clientHeight;
function flySomewhere() {
// On mobile, always prefer to focus on an element
// If not mobile, 50% chance to focus on ground
// if ((!isMobile() && coinFlip()) || !focusOnElement()) {
// focusOnGround();
// }
if (!focusOnElement()) {
focusOnGround();
}
}
function focusOnGround() {
focusedElement = null;
focusedBounds = { left: 0, right: window.innerWidth, top: getSafeWindowHeight() };
updateFocusedElementBounds();
flyTo(Math.random() * window.innerWidth, 0);
}
function focusOnElement() {
/**
* Focus on an element within the viewport
* @param {boolean} [teleport] Whether to teleport to the element instead of flying
* @returns Whether an element to focus on was found
*/
function focusOnElement(teleport = false) {
if (frozen) {
return;
return false;
}
const elements = document.querySelectorAll("img, video");
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
const inWindow = Array.from(elements).filter((img) => {
const rect = img.getBoundingClientRect();
return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= window.innerHeight;
return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= getWindowHeight();
});
const visible = Array.from(inWindow).filter((img) => {
const style = window.getComputedStyle(img);
if (style.display === "none" || style.visibility === "hidden" || (style.opacity && parseFloat(style.opacity) < 0.25)) {
return false;
}
return true;
});
/** @type {HTMLElement[]} */
// @ts-expect-error
const largeElements = Array.from(inWindow).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
if (largeElements.length === 0) {
return;
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
// Ensure the bird doesn't land on fixed or sticky elements
const fixedAllowed = getContext() instanceof ObsidianContext;
const nonFixedElements = largeElements.filter((el) => {
if (fixedAllowed) {
return true;
}
const style = window.getComputedStyle(el);
return style.position !== "fixed" && style.position !== "sticky";
});
if (nonFixedElements.length === 0) {
return false;
}
const randomElement = largeElements[Math.floor(Math.random() * largeElements.length)];
const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)];
focusedElement = randomElement;
log("Focusing on element: ", focusedElement);
updateFocusedElementBounds();
flyTo(getFocusedElementRandomX(), getFocusedY());
if (teleport) {
teleportTo(getFocusedElementRandomX(), getFocusedY());
} else {
flyTo(getFocusedElementRandomX(), getFocusedY());
}
return randomElement !== null;
}
/**
* @param {number} x
* @param {number} y
*/
function teleportTo(x, y) {
birdX = x;
birdY = y;
setState(States.IDLE);
}
function updateFocusedElementBounds() {
if (focusedElement === null) {
// Update ground location to bottom of window
focusedBounds = { left: 0, right: window.innerWidth, top: getFullWindowHeight() };
focusedBounds = { left: 0, right: window.innerWidth, top: getWindowHeight() };
return;
}
const { left, right, top } = focusedElement.getBoundingClientRect();
let { left, right, top } = focusedElement.getBoundingClientRect();
if (focusedElement.classList.contains("birb-sticky-note")) {
top -= 4.5 * UI_CSS_SCALE;
if (focusedBounds.left !== left) {
// Sticky note has moved
const oldWidth = focusedBounds.right - focusedBounds.left;
const newWidth = right - left;
if (oldWidth === newWidth) {
// Move bird along with note
if (currentState === States.IDLE) {
birdX += left - focusedBounds.left;
} else if (currentState === States.HOP) {
startX += left - focusedBounds.left;
startY += top - focusedBounds.top;
targetX += left - focusedBounds.left;
targetY += top - focusedBounds.top;
}
}
}
}
focusedBounds = { left, right, top };
}
@@ -924,11 +943,6 @@ Promise.all([
}
}
function hideBirb() {
birb.setVisible(false);
visible = false;
}
/**
* @param {number} x
* @param {number} y
@@ -960,21 +974,11 @@ Promise.all([
birb.setAnimation(Animations.BOB);
}
birb.setAbsolutePositioned(isAbsolute());
setY(birdY);
birb.setY(birdY);
}
/**
* @param {number} x
*/
function setX(x) {
birb.setX(x);
}
/**
* @param {number} y
*/
function setY(y) {
birb.setY(y);
function coinFlip() {
return Math.random() < 0.5;
}
// Helper functions

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