From e230618f3b4512a1359593f8c44782f19cf0db19 Mon Sep 17 00:00:00 2001
From: AloeSapling
Date: Sun, 10 Aug 2025 21:14:38 +0200
Subject: [PATCH 01/13] Added ignore files to stop popular code formatters from
messing with the code style
---
.eslintignore | 1 +
.prettierignore | 1 +
2 files changed, 2 insertions(+)
create mode 100644 .eslintignore
create mode 100644 .prettierignore
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..f59ec20
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+*
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..f59ec20
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+*
\ No newline at end of file
From d3e821101765fed452cfadec51b4f0ed70f00c53 Mon Sep 17 00:00:00 2001
From: Endrik Tombak
Date: Fri, 8 Aug 2025 21:59:18 +0300
Subject: [PATCH 02/13] Change transform easing to 0s
---
src/overlay.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/overlay.css b/src/overlay.css
index 7a0a12e..1bc9429 100644
--- a/src/overlay.css
+++ b/src/overlay.css
@@ -8,7 +8,7 @@
padding: 10px;
border-radius: 8px;
z-index: 9000;
- transition: all 0.3s ease;
+ transition: all 0.3s ease, transform 0s;
max-width: 300px;
width: auto;
/* Performance optimizations for smooth dragging */
From 676bd85c6dae1d57f66ee213982923898d50972b Mon Sep 17 00:00:00 2001
From: KrunchyKrisp
Date: Sat, 9 Aug 2025 15:24:38 +0200
Subject: [PATCH 03/13] Added a translucent gray checkerboard render for
#deface
---
src/Template.js | 27 ++++++++++++++++-----------
1 file changed, 16 insertions(+), 11 deletions(-)
diff --git a/src/Template.js b/src/Template.js
index 4c63ee7..b0a1136 100644
--- a/src/Template.js
+++ b/src/Template.js
@@ -128,18 +128,23 @@ export default class Template {
for (let y = 0; y < canvasHeight; y++) {
for (let x = 0; x < canvasWidth; x++) {
// For every pixel...
-
- // ... Make it transparent unless it is the "center"
- if (x % shreadSize !== 1 || y % shreadSize !== 1) {
- const pixelIndex = (y * canvasWidth + x) * 4; // Find the pixel index in an array where every 4 indexes are 1 pixel
+ const pixelIndex = (y * canvasWidth + x) * 4; // Find the pixel index in an array where every 4 indexes are 1 pixel
+ // If the pixel is the color #deface, draw a translucent gray checkerboard pattern
+ if (
+ imageData.data[pixelIndex] === 222 &&
+ imageData.data[pixelIndex + 1] === 250 &&
+ imageData.data[pixelIndex + 2] === 206
+ ) {
+ if ((x + y) % 2 === 0) { // Formula for checkerboard pattern
+ imageData.data[pixelIndex] = 0;
+ imageData.data[pixelIndex + 1] = 0;
+ imageData.data[pixelIndex + 2] = 0;
+ imageData.data[pixelIndex + 3] = 32; // Translucent black
+ } else { // Transparent negative space
+ imageData.data[pixelIndex + 3] = 0;
+ }
+ } else if (x % shreadSize !== 1 || y % shreadSize !== 1) { // Otherwise only draw the middle pixel
imageData.data[pixelIndex + 3] = 0; // Make the pixel transparent on the alpha channel
-
- // if (!!imageData.data[pixelIndex + 3]) {
- // imageData.data[pixelIndex + 3] = 50; // Alpha
- // imageData.data[pixelIndex] = 30; // Red
- // imageData.data[pixelIndex + 1] = 30; // Green
- // imageData.data[pixelIndex + 2] = 30; // Blue
- // }
}
}
}
From dc99db309e7d409802119fa5abf1f162b6c578ff Mon Sep 17 00:00:00 2001
From: SwingTheVine
Date: Sun, 10 Aug 2025 21:53:28 -0400
Subject: [PATCH 04/13] Added workflow to check what branch PR came from
---
.github/workflows/pr-branch-check.yml | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
create mode 100644 .github/workflows/pr-branch-check.yml
diff --git a/.github/workflows/pr-branch-check.yml b/.github/workflows/pr-branch-check.yml
new file mode 100644
index 0000000..475b39a
--- /dev/null
+++ b/.github/workflows/pr-branch-check.yml
@@ -0,0 +1,18 @@
+name: Enforce allowed branches for PRs to main
+
+on:
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ check-branch:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check PR source branch
+ run: |
+ echo "Source branch: ${{ github.head_ref }}"
+ if [[ "${{ github.head_ref }}" != "documentation" && "${{ github.head_ref }}" != "code" ]]; then
+ echo "Error: PRs to main must come from 'documentation' or 'code' branches only."
+ exit 1
+ fi
From 05893df35c554cdd2661e5467193c6f353c9d223 Mon Sep 17 00:00:00 2001
From: SwingTheVine
Date: Mon, 11 Aug 2025 23:51:30 -0400
Subject: [PATCH 05/13] Update to 0.80.0 (#108)
* Create CONTRIBUTING.md
* Update CONTRIBUTING.md
* Update CONTRIBUTING.md
* Update CONTRIBUTING.md
* Added some install instructions
* Finished CONTRIBUTING.md
* Fixed CONTRIBUTING table
* Added SECURITY.md
* Added Computer Edge instructions
* Added Computer FireFox instructions
* Clarified where the userscript can be downloaded from
* Update to Wiki Docs
* Simplify installation instructions with one-click install links
This is because Tampermonkey automagically detect whether raw js files are being opened and redirect user to installation page. We might need a custom build action to update the links though.
- Replace manual download and drag process with direct install links
- Remove unnecessary screenshots and dashboard steps
* Fix stuff
* Fixed again. Sorry I was looking at the wrong branch T-T
* .
* Added color palette to src/utils.js
* Updated Shields to match HEAD of main
* Added build.yml RegEx for v0.0.0 version updating in README.md
* Branch sync
* Update CONTRIBUTING.md
* Added the quick guide
* Added wplace status shield
* Moved wiki to its own branch
* Added Shields from #61 and #58
* Fixed PR template
* Squashed commit of the following:
commit aca7df4189e2a0846688f95c4f1dfeb203bde659
Author: SwingTheVine
Date: Sat Aug 9 20:52:22 2025 -0400
Added color palette to src/utils.js
commit 13ff8fbe33c3bac3727db85a742a7af32265ccc3
Merge: 70eb0a2 f2d34d8
Author: SwingTheVine
Date: Sat Aug 9 20:49:26 2025 -0400
Merge branch 'main' of https://github.com/SwingTheVine/Wplace-BlueMarble
commit 70eb0a26faa0dc419b994ad8c9a7a8e8f1a10596
Author: SwingTheVine
Date: Fri Aug 8 19:38:49 2025 -0400
Update to Wiki Docs
* Fixed bug in JSDoc generation in build.js
* v0.79.0; Merge pull request #98 from SwingTheVine/documentation
Updated documentation
* Added missing dependency for minami
* Added brief description about what Blue Marble does
* v0.80.0; Added brief description about what Blue Marble does
* v0.81.0; Merge branch 'code' into main
---------
Co-authored-by: thatfrozenfrog <101154752+thatfrozenfrog@users.noreply.github.com>
Co-authored-by: github-actions[bot]
---
.github/ISSUE_TEMPLATE/config.yml | 7 +-
.../PULL_REQUEST_TEMPLATE/pull-request.yml | 60 -
.../pull_request_template.md | 31 +
.github/workflows/build.yml | 60 +-
build/build.js | 20 +-
dist/BlueMarble.user.css | 2 +-
dist/BlueMarble.user.js | 8 +-
docs/CONTRIBUTING.md | 3 +-
docs/Overlay.js.html | 748 --
docs/README.md | 103 +-
docs/Template.js.html | 230 -
docs/apiManager.js.html | 193 -
docs/assets/Showcase1.png | Bin 0 -> 4403 bytes
docs/fonts/OpenSans-Bold-webfont.eot | Bin 19544 -> 0 bytes
docs/fonts/OpenSans-Bold-webfont.svg | 1830 ----
docs/fonts/OpenSans-Bold-webfont.woff | Bin 22432 -> 0 bytes
docs/fonts/OpenSans-BoldItalic-webfont.eot | Bin 20133 -> 0 bytes
docs/fonts/OpenSans-BoldItalic-webfont.svg | 1830 ----
docs/fonts/OpenSans-BoldItalic-webfont.woff | Bin 23048 -> 0 bytes
docs/fonts/OpenSans-Italic-webfont.eot | Bin 20265 -> 0 bytes
docs/fonts/OpenSans-Italic-webfont.svg | 1830 ----
docs/fonts/OpenSans-Italic-webfont.woff | Bin 23188 -> 0 bytes
docs/fonts/OpenSans-Light-webfont.eot | Bin 19514 -> 0 bytes
docs/fonts/OpenSans-Light-webfont.svg | 1831 ----
docs/fonts/OpenSans-Light-webfont.woff | Bin 22248 -> 0 bytes
docs/fonts/OpenSans-LightItalic-webfont.eot | Bin 20535 -> 0 bytes
docs/fonts/OpenSans-LightItalic-webfont.svg | 1835 ----
docs/fonts/OpenSans-LightItalic-webfont.woff | Bin 23400 -> 0 bytes
docs/fonts/OpenSans-Regular-webfont.eot | Bin 19836 -> 0 bytes
docs/fonts/OpenSans-Regular-webfont.svg | 1831 ----
docs/fonts/OpenSans-Regular-webfont.woff | Bin 22660 -> 0 bytes
docs/global.html | 7664 -----------------
docs/index.html | 65 -
docs/main.js.html | 618 --
docs/module.exports.html | 1130 ---
docs/module.exports_module.exports.html | 222 -
docs/observers.js.html | 111 -
docs/scripts/linenumber.js | 25 -
docs/scripts/prettify/Apache-License-2.0.txt | 202 -
docs/scripts/prettify/lang-css.js | 2 -
docs/scripts/prettify/prettify.js | 28 -
docs/styles/jsdoc-default.css | 358 -
docs/styles/prettify-jsdoc.css | 111 -
docs/styles/prettify-tomorrow.css | 132 -
docs/templateManager.js.html | 444 -
docs/utils.js.html | 178 -
jsdoc.json | 17 +
package-lock.json | 11 +-
package.json | 3 +-
src/BlueMarble.meta.js | 6 +-
src/Overlay.js | 1 +
src/Template.js | 1 +
src/apiManager.js | 1 +
src/main.js | 2 +-
src/observers.js | 1 +
src/templateManager.js | 1 +
src/utils.js | 268 +-
57 files changed, 482 insertions(+), 23572 deletions(-)
delete mode 100644 .github/PULL_REQUEST_TEMPLATE/pull-request.yml
create mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md
delete mode 100644 docs/Overlay.js.html
delete mode 100644 docs/Template.js.html
delete mode 100644 docs/apiManager.js.html
create mode 100644 docs/assets/Showcase1.png
delete mode 100644 docs/fonts/OpenSans-Bold-webfont.eot
delete mode 100644 docs/fonts/OpenSans-Bold-webfont.svg
delete mode 100644 docs/fonts/OpenSans-Bold-webfont.woff
delete mode 100644 docs/fonts/OpenSans-BoldItalic-webfont.eot
delete mode 100644 docs/fonts/OpenSans-BoldItalic-webfont.svg
delete mode 100644 docs/fonts/OpenSans-BoldItalic-webfont.woff
delete mode 100644 docs/fonts/OpenSans-Italic-webfont.eot
delete mode 100644 docs/fonts/OpenSans-Italic-webfont.svg
delete mode 100644 docs/fonts/OpenSans-Italic-webfont.woff
delete mode 100644 docs/fonts/OpenSans-Light-webfont.eot
delete mode 100644 docs/fonts/OpenSans-Light-webfont.svg
delete mode 100644 docs/fonts/OpenSans-Light-webfont.woff
delete mode 100644 docs/fonts/OpenSans-LightItalic-webfont.eot
delete mode 100644 docs/fonts/OpenSans-LightItalic-webfont.svg
delete mode 100644 docs/fonts/OpenSans-LightItalic-webfont.woff
delete mode 100644 docs/fonts/OpenSans-Regular-webfont.eot
delete mode 100644 docs/fonts/OpenSans-Regular-webfont.svg
delete mode 100644 docs/fonts/OpenSans-Regular-webfont.woff
delete mode 100644 docs/global.html
delete mode 100644 docs/index.html
delete mode 100644 docs/main.js.html
delete mode 100644 docs/module.exports.html
delete mode 100644 docs/module.exports_module.exports.html
delete mode 100644 docs/observers.js.html
delete mode 100644 docs/scripts/linenumber.js
delete mode 100644 docs/scripts/prettify/Apache-License-2.0.txt
delete mode 100644 docs/scripts/prettify/lang-css.js
delete mode 100644 docs/scripts/prettify/prettify.js
delete mode 100644 docs/styles/jsdoc-default.css
delete mode 100644 docs/styles/prettify-jsdoc.css
delete mode 100644 docs/styles/prettify-tomorrow.css
delete mode 100644 docs/templateManager.js.html
delete mode 100644 docs/utils.js.html
create mode 100644 jsdoc.json
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 5df46e1..df309c1 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,11 @@
blank_issues_enabled: false
contact_links:
- - name: Community Support & Questions
+ - name: Community Support & Questions (Discord)
url: https://discord.gg/tpeBPy46hf
- about: Join the Discord if you have questions or want to discuss this mod with the community.
+ about: Join the Discord if you have questions or want to discuss Blue Marble with the community.
+ - name: Community Support & Questions (GitHub)
+ url: https://github.com/SwingTheVine/Wplace-BlueMarble/discussions
+ about: Go to the "Discussion" tab if you have questions or want to discuss Blue Marble with the community.
- name: Partnership Request
url: https://discord.com/channels/796124137042608188/1257365507812888589
about: Open a ticket in the Discord server to discuss a partnership.
diff --git a/.github/PULL_REQUEST_TEMPLATE/pull-request.yml b/.github/PULL_REQUEST_TEMPLATE/pull-request.yml
deleted file mode 100644
index 28f8ea8..0000000
--- a/.github/PULL_REQUEST_TEMPLATE/pull-request.yml
+++ /dev/null
@@ -1,60 +0,0 @@
-name: "Pull Request"
-description: "Fill out the following details to submit your PR."
-title: "[PR]: "
-body:
- - type: markdown
- attributes:
- value: |
- ## Summary
- Please briefly describe the changes.
-
- - type: textarea
- id: summary
- attributes:
- label: Summary
- description: Briefly describe what this PR does.
- placeholder: |
- E.g. Fixes display bug with templates.
- E.g. Adds a template tab that users can manage all templates through.
- validations:
- required: true
-
- - type: textarea
- id: related-issues
- attributes:
- label: Related Issue(s)
- description: Link related issues
- placeholder: |
- E.g. Fixes #14
- E.g. Adds #4
- validations:
- required: false
-
- - type: checkboxes
- id: changes
- attributes:
- label: Type of Changes
- options:
- - label: Feature
- - label: Bug fix
- - label: Documentation
- - label: Refactoring
- - label: Build
- - label: Other
-
- - type: checkboxes
- id: checklist
- attributes:
- label: Checklist
- options:
- - label: The author of this PR has read the CONTRIBUTING guidelines.
- - label: This PR follows the Code of Conduct.
- - label: This PR follows the project's style of coding and documentation.
- - label: Documentation related to this PR has been updated.
- - label: Blue Marble has been verified to work correctly for this PR.
-
- - type: textarea
- id: additional-notes
- attributes:
- label: Additional Notes
- description: Anything else reviewers should know?
diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
new file mode 100644
index 0000000..ce769bd
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -0,0 +1,31 @@
+# Pull Request
+Fill out the following details to submit your PR.
+
+## Summary
+Please briefly describe the changes in your PR.
+E.g. Fixes display bug with templates.
+E.g. Adds a template tab that users can manage all templates through.
+
+## Related Issue(s)
+Link to the related issues your PR would solve here.
+E.g. Fixes #14
+E.g. Adds #4
+
+## Changes
+Select the type of change your PR is:
+- [ ] Feature
+- [ ] Bug fix
+- [ ] Documentation
+- [ ] Refactoring
+- [ ] Build
+- [ ] Other
+
+## Checklist
+- [ ] The author of this PR has read the CONTRIBUTING guidelines.
+- [ ] This PR follows the Code of Conduct.
+- [ ] This PR follows the project's style of coding and documentation.
+- [ ] Documentation related to this PR has been updated.
+- [ ] Blue Marble has been verified to work correctly for this PR.
+
+## Additional Notes
+Anything else reviewers should know?
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d82bbcd..68bd95d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -69,16 +69,28 @@ jobs:
current_version=$(jq -r '.version' package.json)
echo "Current version: $current_version"
echo "current_version=$current_version" >> $GITHUB_OUTPUT
-
+
+ - name: Get latest release tag (no "v" prefix)
+ id: get_latest_tag
+ run: |
+ latest_tag=$(gh release list --limit 1 --exclude-drafts --exclude-pre-releases=false | head -n 1 | awk '{print $1}')
+ latest_tag_no_v=${latest_tag#v}
+ echo "latest_tag_no_v=$latest_tag_no_v" >> $GITHUB_OUTPUT
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
- name: Update static version numbers
run: |
- current_version=${{ steps.get_version.outputs.current_version }}
+ current_version="${{ steps.get_latest_tag.outputs.latest_tag_no_v }}"
if [ -f "docs/README.md" ]; then
echo "README.md exists. Modifying..."
+ sed -i \
+ -e 's|\(Latest_Version-\)[^-\ ]*\(-lightblue\)|\1'"$current_version"'\2|' \
+ -e 's|v[0-9]\+\.[0-9]\+\.[0-9]\+|v'"$current_version"'|g' \
+ docs/README.md
else
echo "README.md was not found. Skipping..."
fi
- sed -i 's|\(Latest_Version-\)[^-\ ]*\(-lightblue\)|\1'$current_version'\2|' docs/README.md
- name: Update compression badge
run: |
@@ -171,3 +183,45 @@ jobs:
git merge --squash origin/auto
git commit -m "v${{ needs.build.outputs.CURRENT_VERSION }}; ${{ needs.build.outputs.TITLE }}" -m "${{ needs.build.outputs.BODY }}" || echo "No changes to commit"
git push origin main
+
+ update-wiki:
+
+ permissions:
+ contents: write
+
+ runs-on: ubuntu-latest
+
+ needs: [update-auto, build, update-requirements] # Needs the update-auto, build, and update-requirements jobs to finish first
+
+ steps:
+ - name: Checkout main branch
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Force update wiki branch
+ run: |
+ git fetch origin
+ git branch -f wiki origin/main
+ git push origin wiki --force
+
+ - name: Checkout wiki branch
+ run: git checkout wiki
+
+ - name: Install dependencies
+ run: |
+ npm ci
+ npm install minami taffydb
+
+ - name: Generate JSDoc from jsdoc.json
+ run: |
+ npx jsdoc -c jsdoc.json
+
+ - name: Commit and push to wiki branch
+ run: |
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
+ git add docs
+ git commit -m "Update wiki via JSDoc" || echo "No changes to commit"
+ git push origin wiki
diff --git a/build/build.js b/build/build.js
index 2e3f45b..6ea6059 100644
--- a/build/build.js
+++ b/build/build.js
@@ -25,16 +25,16 @@ const isGitHub = !!process.env?.GITHUB_ACTIONS; // Is this running in a GitHub A
console.log(`${consoleStyle.BLUE}Starting build...${consoleStyle.RESET}`);
// Tries to build the wiki if build.js is run in a GitHub Workflow
-if (isGitHub) {
- try {
- console.log(`Generating JSDoc...`);
- execSync(`npx jsdoc src/ -r -d docs`, { stdio: "inherit" });
- console.log(`JSDoc built ${consoleStyle.GREEN}successfully${consoleStyle.RESET}`);
- } catch (error) {
- console.error(`${consoleStyle.RED + consoleStyle.BOLD}Failed to generate JSDoc${consoleStyle.RESET}:`, error);
- process.exit(1);
- }
-}
+// if (isGitHub) {
+// try {
+// console.log(`Generating JSDoc...`);
+// execSync(`npx jsdoc src/ -r -d docs -t node_modules/minami`, { stdio: "inherit" });
+// console.log(`JSDoc built ${consoleStyle.GREEN}successfully${consoleStyle.RESET}`);
+// } catch (error) {
+// console.error(`${consoleStyle.RED + consoleStyle.BOLD}Failed to generate JSDoc${consoleStyle.RESET}:`, error);
+// process.exit(1);
+// }
+// }
// Tries to bump the version
try {
diff --git a/dist/BlueMarble.user.css b/dist/BlueMarble.user.css
index ca07ab9..d8f9d80 100644
--- a/dist/BlueMarble.user.css
+++ b/dist/BlueMarble.user.css
@@ -1 +1 @@
-#bm-n{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000;transition:all .3s ease;max-width:300px;width:auto;will-change:transform;backface-visibility:hidden;-webkit-backface-visibility:hidden;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}#bm-4,#bm-n hr,#bm-3,#bm-1{transition:opacity .2s ease,height .2s ease}div#bm-n{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-i{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,') repeat;cursor:grab;width:100%;height:1em}#bm-i.dragging{cursor:grabbing}#bm-n:has(#bm-i.dragging){pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#bm-i.dragging{pointer-events:auto}#bm-7{margin-bottom:.5em}#bm-7[style*="text-align: center"]{display:flex;flex-direction:column;align-items:center;justify-content:center}#bm-n[style*="padding: 5px"]{width:auto!important;max-width:300px;min-width:200px}#bm-n img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle;transition:opacity .2s ease}#bm-7[style*="text-align: center"] img{display:block;margin:0 auto}#bm-i{transition:margin-bottom .2s ease}#bm-n h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-3 input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-3 label{margin-right:.5ch}.bm-q{border:white 1px solid;height:1.5em;width:1.5em;margin-top:2px;text-align:center;line-height:1em;padding:0!important}#bm-d{vertical-align:middle}#bm-d svg{width:50%;margin:0 auto;fill:#111}div:has(>#bm-button-teleport){display:flex;gap:.5ch}#bm-button-favorite svg,#bm-button-template svg{height:1em;margin:2px auto 0;text-align:center;line-height:1em;vertical-align:bottom}#bm-8 input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-8 input[type=number]::-webkit-outer-spin-button,#bm-8 input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-0{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-2)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-2,input[type=file][id*=template]{display:none!important;visibility:hidden!important;position:absolute!important;left:-9999px!important;top:-9999px!important;width:0!important;height:0!important;opacity:0!important;z-index:-9999!important;pointer-events:none!important}#bm-b{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-1{display:flex;justify-content:space-between}#bm-n small{font-size:x-small;color:#d3d3d3}#bm-4,#bm-3,#bm-8,#bm-0,div:has(>#bm-2),#bm-b{margin-top:.5em}#bm-n button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-n button:hover,#bm-n button:focus-visible{background-color:#1061e5}#bm-n button:active,#bm-n button:disabled{background-color:#2e97ff}#bm-n button:disabled{text-decoration:line-through}
+#bm-n{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000;transition:all .3s ease,transform 0s;max-width:300px;width:auto;will-change:transform;backface-visibility:hidden;-webkit-backface-visibility:hidden;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}#bm-4,#bm-n hr,#bm-3,#bm-1{transition:opacity .2s ease,height .2s ease}div#bm-n{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-i{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,') repeat;cursor:grab;width:100%;height:1em}#bm-i.dragging{cursor:grabbing}#bm-n:has(#bm-i.dragging){pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#bm-i.dragging{pointer-events:auto}#bm-7{margin-bottom:.5em}#bm-7[style*="text-align: center"]{display:flex;flex-direction:column;align-items:center;justify-content:center}#bm-n[style*="padding: 5px"]{width:auto!important;max-width:300px;min-width:200px}#bm-n img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle;transition:opacity .2s ease}#bm-7[style*="text-align: center"] img{display:block;margin:0 auto}#bm-i{transition:margin-bottom .2s ease}#bm-n h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-3 input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-3 label{margin-right:.5ch}.bm-q{border:white 1px solid;height:1.5em;width:1.5em;margin-top:2px;text-align:center;line-height:1em;padding:0!important}#bm-d{vertical-align:middle}#bm-d svg{width:50%;margin:0 auto;fill:#111}div:has(>#bm-button-teleport){display:flex;gap:.5ch}#bm-button-favorite svg,#bm-button-template svg{height:1em;margin:2px auto 0;text-align:center;line-height:1em;vertical-align:bottom}#bm-8 input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-8 input[type=number]::-webkit-outer-spin-button,#bm-8 input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-0{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-2)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-2,input[type=file][id*=template]{display:none!important;visibility:hidden!important;position:absolute!important;left:-9999px!important;top:-9999px!important;width:0!important;height:0!important;opacity:0!important;z-index:-9999!important;pointer-events:none!important}#bm-b{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-1{display:flex;justify-content:space-between}#bm-n small{font-size:x-small;color:#d3d3d3}#bm-4,#bm-3,#bm-8,#bm-0,div:has(>#bm-2),#bm-b{margin-top:.5em}#bm-n button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-n button:hover,#bm-n button:focus-visible{background-color:#1061e5}#bm-n button:active,#bm-n button:disabled{background-color:#2e97ff}#bm-n button:disabled{text-decoration:line-through}
diff --git a/dist/BlueMarble.user.js b/dist/BlueMarble.user.js
index d94878a..2e8ad4e 100644
--- a/dist/BlueMarble.user.js
+++ b/dist/BlueMarble.user.js
@@ -1,13 +1,13 @@
// ==UserScript==
// @name Blue Marble
// @namespace https://github.com/SwingTheVine/
-// @version 0.78.0
+// @version 0.81.0
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine
// @license MPL-2.0
// @supportURL https://discord.gg/tpeBPy46hf
// @homepageURL https://github.com/SwingTheVine/Wplace-BlueMarble
-// @icon https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/0d5b710473581e449b16a1e77c75ed287286881a/dist/assets/Favicon.png
+// @icon https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/a3b4a288514dc48a9232b1aeeb6b377af6fdfe7c/dist/assets/Favicon.png
// @updateURL https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/BlueMarble.user.js
// @downloadURL https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/BlueMarble.user.js
// @run-at document-start
@@ -16,10 +16,10 @@
// @grant GM_addStyle
// @grant GM.setValue
// @grant GM_getValue
-// @resource CSS-BM-File https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/0d5b710473581e449b16a1e77c75ed287286881a/dist/BlueMarble.user.css
+// @resource CSS-BM-File https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/a3b4a288514dc48a9232b1aeeb6b377af6fdfe7c/dist/BlueMarble.user.css
// ==/UserScript==
// Wplace --> https://wplace.live
// License --> https://www.mozilla.org/en-US/MPL/2.0/
-(()=>{var t,e,n=t=>{throw TypeError(t)},i=(t,e,i)=>e.has(t)?n("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,i),s=(t,e,i)=>(((t,e)=>{e.has(t)||n("Cannot access private method")})(t,e),i),o=class{constructor(e,n){i(this,t),this.name=e,this.version=n,this.t=null,this.i="bm-b",this.o=null,this.l=null,this.h=[]}u(t){this.t=t}m(){return this.h.length>0&&(this.l=this.h.pop()),this}p(t){t?.appendChild(this.o),this.o=null,this.l=null,this.h=[]}v(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"div",{},n)),this}M(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"p",{},n)),this}$(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"small",{},n)),this}C(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"img",{},n)),this}D(n,i={},o=()=>{}){return o(this,s(this,t,e).call(this,"h"+n,{},i)),this}T(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"hr",{},n)),this}I(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"br",{},n)),this}k(n={},i=()=>{}){const o=s(this,t,e).call(this,"label",{textContent:n.textContent??""});delete n.textContent;const a=s(this,t,e).call(this,"input",{type:"checkbox"},n);return o.insertBefore(a,o.firstChild),this.m(),i(this,o,a),this}N(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"button",{},n)),this}S(n={},i=()=>{}){const o=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${o}`;const a={textContent:"?",className:"bm-q",onclick:()=>{this.B(this.i,o)}};return i(this,s(this,t,e).call(this,"button",a,n)),this}O(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"input",{},n)),this}L(n={},i=()=>{}){const o=n.textContent??"";delete n.textContent;const a=s(this,t,e).call(this,"div"),r=s(this,t,e).call(this,"input",{type:"file",style:"display: none !important; visibility: hidden !important; position: absolute !important; left: -9999px !important; width: 0 !important; height: 0 !important; opacity: 0 !important;"},n);this.m();const c=s(this,t,e).call(this,"button",{textContent:o});return this.m(),this.m(),r.setAttribute("tabindex","-1"),r.setAttribute("aria-hidden","true"),c.addEventListener("click",()=>{r.click()}),r.addEventListener("change",()=>{c.style.maxWidth=`${c.offsetWidth}px`,r.files.length>0?c.textContent=r.files[0].name:c.textContent=o}),i(this,a,r,c),this}H(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"textarea",{},n)),this}B(t,e,n=!1){const i=document.getElementById(t.replace(/^#/,""));i&&(i instanceof HTMLInputElement?i.value=e:n?i.textContent=e:i.innerHTML=e)}j(t,e){let n,i=!1,s=0,o=null,a=0,r=0,c=0,l=0;if(t=document.querySelector("#"==t?.[0]?t:"#"+t),e=document.querySelector("#"==e?.[0]?e:"#"+e),!t||!e)return void this.q(`Can not drag! ${t?"":"moveMe"} ${t||e?"":"and "}${e?"":"iMoveThings "}was not found!`);const h=()=>{if(i){const e=Math.abs(a-c),n=Math.abs(r-l);(e>.5||n>.5)&&(a=c,r=l,t.style.transform=`translate(${a}px, ${r}px)`,t.style.left="0px",t.style.top="0px",t.style.right=""),o=requestAnimationFrame(h)}};let u=null;const m=(m,d)=>{i=!0,u=t.getBoundingClientRect(),n=m-u.left,s=d-u.top;const p=window.getComputedStyle(t).transform;if(p&&"none"!==p){const t=new DOMMatrix(p);a=t.m41,r=t.m42}else a=u.left,r=u.top;c=a,l=r,document.body.style.userSelect="none",e.classList.add("dragging"),o&&cancelAnimationFrame(o),h()},d=()=>{i=!1,o&&(cancelAnimationFrame(o),o=null),document.body.style.userSelect="",e.classList.remove("dragging")};e.addEventListener("mousedown",function(t){t.preventDefault(),m(t.clientX,t.clientY)}),e.addEventListener("touchstart",function(t){const e=t?.touches?.[0];e&&(m(e.clientX,e.clientY),t.preventDefault())},{passive:!1}),document.addEventListener("mousemove",function(t){i&&u&&(c=t.clientX-n,l=t.clientY-s)},{passive:!0}),document.addEventListener("touchmove",function(t){if(i&&u){const e=t?.touches?.[0];if(!e)return;c=e.clientX-n,l=e.clientY-s,t.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",d),document.addEventListener("touchend",d),document.addEventListener("touchcancel",d)}A(t){(0,console.info)(`${this.name}: ${t}`),this.B(this.i,"Status: "+t,!0)}q(t){(0,console.error)(`${this.name}: ${t}`),this.B(this.i,"Error: "+t,!0)}};function a(t,e){if(0===t)return e[0];let n="";const i=e.length;for(;t>0;)n=e[t%i]+n,t=Math.floor(t/i);return n}function r(t){let e="";for(let n=0;n0)for(const t in e){const n=t,i=e[t];if(e.hasOwnProperty(t)){const t=n.split(" "),e=Number(t?.[0]),s=t?.[1]||"0",o=i.name||`Template ${e||""}`,a=i.tiles,r={};for(const t in a)if(a.hasOwnProperty(t)){const e=c(a[t]),n=new Blob([e],{type:"image/png"}),i=await createImageBitmap(n);r[t]=i}const l=new m({displayName:o,_:e||this.X?.length||0,F:s||""});l.P=r,this.X.push(l)}}};var d=GM_info.script.name.toString(),p=GM_info.script.version.toString();!function(t){const e=document.createElement("script");e.setAttribute("bm-r",d),e.setAttribute("bm-o","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement?.appendChild(e),e.remove()}(()=>{const t=document.currentScript,e=t?.getAttribute("bm-r")||"Blue Marble",n=t?.getAttribute("bm-o")||"",i=new Map;window.addEventListener("message",t=>{const{source:s,endpoint:o,blobID:a,blobData:r,blink:c}=t.data;if(Date.now(),"blue-marble"==s&&a&&r&&!o){const t=i.get(a);"function"==typeof t?t(r):function(...t){(0,console.warn)(...t)}(`%c${e}%c: Attempted to retrieve a blob (%s) from queue, but the blobID was not a function! Skipping...`,n,"",a),i.delete(a)}});const s=window.fetch;window.fetch=async function(...t){const e=await s.apply(this,t),n=e.clone(),o=(t[0]instanceof Request?t[0]?.url:t[0])||"ignore",a=n.headers.get("content-type")||"";if(a.includes("application/json"))n.json().then(t=>{window.postMessage({source:"blue-marble",endpoint:o,jsonData:t},"*")}).catch(t=>{});else if(a.includes("image/")&&!o.includes("openfreemap")&&!o.includes("maps")){const t=Date.now(),e=await n.blob();return new Promise(s=>{const a=crypto.randomUUID();i.set(a,t=>{s(new Response(t,{headers:n.headers,status:n.status,statusText:n.statusText}))}),window.postMessage({source:"blue-marble",endpoint:o,blobID:a,blobData:e,blink:t})}).catch(t=>{Date.now()})}return e}});var b=GM_getResourceText("CSS-BM-File");GM_addStyle(b);var f=document.createElement("link");f.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",f.rel="preload",f.as="style",f.onload=function(){this.onload=null,this.rel="stylesheet"},document.head?.appendChild(f),new class{constructor(){this.Z=null,this.K=null,this.tt="#bm-5"}et(t){return this.K=t,this.Z=new MutationObserver(t=>{for(const e of t)for(const t of e.addedNodes)t instanceof HTMLElement&&t.matches?.(this.tt)}),this}nt(){return this.Z}observe(t,e=!1,n=!1){t.observe(this.K,{childList:e,subtree:n})}};var w=new o(d,p),v=(new o(d,p),new class{constructor(t,e,n){i(this,l),this.name=t,this.version=e,this.o=n,this.it="1.0.0",this.st=null,this.ot="!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",this.R=1e3,this.rt=3,this.ct=null,this.lt=null,this.ht="bm-p",this.ut="div#map canvas.maplibregl-canvas",this.dt=null,this.bt="",this.X=[],this.W=null,this.ft=!0}wt(){if(document.body.contains(this.ct))return this.ct;document.getElementById(this.ht)?.remove();const t=document.querySelector(this.ut),e=document.createElement("canvas");return e.id=this.ht,e.className="maplibregl-canvas",e.style.position="absolute",e.style.top="0",e.style.left="0",e.style.height=t?.clientHeight*(window.devicePixelRatio||1)+"px",e.style.width=t?.clientWidth*(window.devicePixelRatio||1)+"px",e.height=t?.clientHeight*(window.devicePixelRatio||1),e.width=t?.clientWidth*(window.devicePixelRatio||1),e.style.zIndex="8999",e.style.pointerEvents="none",t?.parentElement?.appendChild(e),this.ct=e,window.addEventListener("move",this.vt),window.addEventListener("zoom",this.yt),window.addEventListener("resize",this.xt),this.ct}async gt(){return{whoami:this.name.replace(" ",""),scriptVersion:this.version,schemaVersion:this.it,templates:{}}}async Mt(t,e,n){this.W||(this.W=await this.gt()),this.o.A(`Creating template at ${n.join(", ")}...`);const i=new m({displayName:e,_:0,F:a(this.st||0,this.ot),file:t,coords:n}),{Y:o,J:r}=await i.U(this.R);i.P=o,this.W.templates[`${i._} ${i.F}`]={name:i.displayName,coords:n.join(", "),enabled:!0,tiles:r},this.X=[],this.X.push(i);const c=(new Intl.NumberFormat).format(i.G);this.o.A(`Template created at ${n.join(", ")}! Total pixels: ${c}`),await s(this,l,h).call(this)}$t(){}async Ct(){this.W||(this.W=await this.gt())}async Dt(t,e){if(!this.ft)return t;const n=this.R*this.rt;e=e[0].toString().padStart(4,"0")+","+e[1].toString().padStart(4,"0");const i=this.X;i.sort((t,e)=>t._-e._);const s=i.map(t=>{const n=Object.keys(t.P).filter(t=>t.startsWith(e));if(0===n.length)return null;const i=n.map(e=>{const n=e.split(",");return{Tt:t.P[e],It:[n[0],n[1]],kt:[n[2],n[3]]}});return i?.[0]}).filter(Boolean),o=s?.length||0;if(o>0){const t=i.filter(t=>Object.keys(t.P).filter(t=>t.startsWith(e)).length>0).reduce((t,e)=>t+(e.G||0),0),n=(new Intl.NumberFormat).format(t);this.o.A(`Displaying ${o} template${1==o?"":"s"}.\nTotal pixels: ${n}`)}else this.o.A(`Displaying ${o} templates.`);const a=await createImageBitmap(t),r=new OffscreenCanvas(n,n),c=r.getContext("2d");c.imageSmoothingEnabled=!1,c.beginPath(),c.rect(0,0,n,n),c.clip(),c.clearRect(0,0,n,n),c.drawImage(a,0,0,n,n);for(const t of s)c.drawImage(t.Tt,Number(t.kt[0])*this.rt,Number(t.kt[1])*this.rt);return await r.convertToBlob({type:"image/png"})}Nt(t){"BlueMarble"==t?.whoami&&s(this,l,u).call(this,t)}St(t){this.ft=t}}(d,p,w)),y=new class{constructor(t){this.Bt=t,this.Ot=!1,this.Lt=[],this.zt=[]}Ht(t){window.addEventListener("message",async e=>{const n=e.data,i=n.jsonData;if(!n||"blue-marble"!==n.source)return;if(!n.endpoint)return;const s=n.endpoint?.split("?")[0].split("/").filter(t=>t&&isNaN(Number(t))).filter(t=>t&&!t.includes(".")).pop();switch(s){case"me":if(i.status&&"2"!=i.status?.toString()[0])return void t.q("You are not logged in!\nCould not fetch userdata.");const e=Math.ceil(Math.pow(Math.floor(i.level)*Math.pow(30,.65),1/.65)-i.pixelsPainted);i.id||i.id,this.Bt.st=i.id,t.B("bm-h",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(i.name)}`),t.B("bm-c",`Droplets: ${(new Intl.NumberFormat).format(i.droplets)}`),t.B("bm-6",`Next level in ${(new Intl.NumberFormat).format(e)} pixel${1==e?"":"s"}`);break;case"pixel":const s=n.endpoint.split("?")[0].split("/").filter(t=>t&&!isNaN(Number(t))),r=new URLSearchParams(n.endpoint.split("?")[1]),c=[r.get("x"),r.get("y")];if(this.Lt.length&&(!s.length||!c.length))return void t.q("Coordinates are malformed!\nDid you try clicking the canvas first?");this.Lt=[...s,...c];const l=(o=s,a=c,[parseInt(o[0])%4*1e3+parseInt(a[0]),parseInt(o[1])%4*1e3+parseInt(a[1])]),h=document.querySelectorAll("span");for(const t of h)if(t.textContent.trim().includes(`${l[0]}, ${l[1]}`)){let e=document.querySelector("#bm-5");const n=`(Tl X: ${s[0]}, Tl Y: ${s[1]}, Px X: ${c[0]}, Px Y: ${c[1]})`;e?e.textContent=n:(e=document.createElement("span"),e.id="bm-5",e.textContent=n,e.style="margin-left: calc(var(--spacing)*3); font-size: small;",t.parentNode.parentNode.parentNode.insertAdjacentElement("afterend",e))}break;case"tiles":let u=n.endpoint.split("/");u=[parseInt(u[u.length-2]),parseInt(u[u.length-1].replace(".png",""))];const m=n.blobID,d=n.blobData,p=await this.Bt.Dt(d,u);window.postMessage({source:"blue-marble",blobID:m,blobData:p,blink:n.blink});break;case"robots":this.Ot="false"==i.userscript?.toString().toLowerCase()}var o,a})}}(v);w.u(y);var x=JSON.parse(GM_getValue("bmTemplates","{}"));v.Nt(x),function(){let t=!1;w.v({id:"bm-n",style:"top: 10px; right: 75px;"}).v({id:"bm-7"}).v({id:"bm-i"}).m().C({alt:"Blue Marble Icon - Click to minimize/maximize",src:"https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png",style:"cursor: pointer;"},(e,n)=>{n.addEventListener("click",()=>{t=!t;const i=document.querySelector("#bm-n"),s=document.querySelector("#bm-7"),o=document.querySelector("#bm-i"),a=document.querySelector("#bm-8"),r=document.querySelector("#bm-d"),c=document.querySelector("#bm-e"),l=document.querySelector("#bm-f"),h=document.querySelector("#bm-9"),u=document.querySelectorAll("#bm-8 input");t||(i.style.width="auto",i.style.maxWidth="300px",i.style.minWidth="200px",i.style.padding="10px"),["#bm-n h1","#bm-4","#bm-n hr","#bm-3 > *:not(#bm-8)","#bm-2","#bm-1",`#${e.i}`].forEach(e=>{document.querySelectorAll(e).forEach(e=>{e.style.display=t?"none":""})}),t?(a&&(a.style.display="none"),r&&(r.style.display="none"),c&&(c.style.display="none"),l&&(l.style.display="none"),h&&(h.style.display="none"),u.forEach(t=>{t.style.display="none"}),i.style.width="60px",i.style.height="76px",i.style.maxWidth="60px",i.style.minWidth="60px",i.style.padding="8px",n.style.marginLeft="3px",s.style.textAlign="center",s.style.margin="0",s.style.marginBottom="0",o&&(o.style.display="",o.style.marginBottom="0.25em")):(a&&(a.style.display="",a.style.flexDirection="",a.style.justifyContent="",a.style.alignItems="",a.style.gap="",a.style.textAlign="",a.style.margin=""),r&&(r.style.display=""),c&&(c.style.display="",c.style.marginTop=""),l&&(l.style.display="",l.style.marginTop=""),h&&(h.style.display="",h.style.marginTop=""),u.forEach(t=>{t.style.display=""}),n.style.marginLeft="",i.style.padding="10px",s.style.textAlign="",s.style.margin="",s.style.marginBottom="",o&&(o.style.marginBottom="0.5em"),i.style.width="",i.style.height=""),n.alt=t?"Blue Marble Icon - Minimized (Click to maximize)":"Blue Marble Icon - Maximized (Click to minimize)"})}).m().D(1,{textContent:d}).m().m().T().m().v({id:"bm-4"}).M({id:"bm-h",textContent:"Username:"}).m().M({id:"bm-c",textContent:"Droplets:"}).m().M({id:"bm-6",textContent:"Next level in..."}).m().m().T().m().v({id:"bm-3"}).v({id:"bm-8"}).N({id:"bm-d",className:"bm-q",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.t?.Lt;e?.[0]?(t.B("bm-j",e?.[0]||""),t.B("bm-k",e?.[1]||""),t.B("bm-l",e?.[2]||""),t.B("bm-m",e?.[3]||"")):t.q("Coordinates are malformed! Did you try clicking on the canvas first?")}}).m().O({type:"number",id:"bm-j",placeholder:"Tl X",min:0,max:2047,step:1,required:!0}).m().O({type:"number",id:"bm-k",placeholder:"Tl Y",min:0,max:2047,step:1,required:!0}).m().O({type:"number",id:"bm-l",placeholder:"Px X",min:0,max:2047,step:1,required:!0}).m().O({type:"number",id:"bm-m",placeholder:"Px Y",min:0,max:2047,step:1,required:!0}).m().m().L({id:"bm-2",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).m().v({id:"bm-0"}).N({id:"bm-f",textContent:"Enable"},(t,e)=>{e.onclick=()=>{t.t?.Bt?.St(!0),t.A("Enabled templates!")}}).m().N({id:"bm-e",textContent:"Create"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-2"),n=document.querySelector("#bm-j");if(!n.checkValidity())return n.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");const i=document.querySelector("#bm-k");if(!i.checkValidity())return i.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");const s=document.querySelector("#bm-l");if(!s.checkValidity())return s.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");const o=document.querySelector("#bm-m");if(!o.checkValidity())return o.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");e?.files[0]?(v.Mt(e.files[0],e.files[0]?.name.replace(/\.[^/.]+$/,""),[Number(n.value),Number(i.value),Number(s.value),Number(o.value)]),t.A("Drew to canvas!")):t.q("No file selected!")}}).m().N({id:"bm-9",textContent:"Disable"},(t,e)=>{e.onclick=()=>{t.t?.Bt?.St(!1),t.A("Disabled templates!")}}).m().m().H({id:w.i,placeholder:`Status: Sleeping...\nVersion: ${p}`,readOnly:!0}).m().v({id:"bm-1"}).v().N({id:"bm-a",className:"bm-q",innerHTML:"🎨",title:"Template Color Converter"},(t,e)=>{e.addEventListener("click",()=>{window.open("https://pepoafonso.github.io/color_converter_wplace/","_blank","noopener noreferrer")})}).m().m().$({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).m().m().m().p(document.body)}(),w.j("#bm-n","#bm-i"),y.Ht(w),new MutationObserver((t,e)=>{const n=document.querySelector("#color-1");if(!n)return;let i=document.querySelector("#bm-g");if(!i){i=document.createElement("button"),i.id="bm-g",i.textContent="Move ↑",i.className="btn btn-soft",i.onclick=function(){const t=this.parentNode.parentNode.parentNode.parentNode,e="Move ↑"==this.textContent;t.parentNode.className=t.parentNode.className.replace(e?"bottom":"top",e?"top":"bottom"),t.style.borderTopLeftRadius=e?"0px":"var(--radius-box)",t.style.borderTopRightRadius=e?"0px":"var(--radius-box)",t.style.borderBottomLeftRadius=e?"var(--radius-box)":"0px",t.style.borderBottomRightRadius=e?"var(--radius-box)":"0px",this.textContent=e?"Move ↓":"Move ↑"};const t=n.parentNode.parentNode.parentNode.parentNode.querySelector("h2");t.parentNode?.appendChild(i)}}).observe(document.body,{childList:!0,subtree:!0}),function(...t){(0,console.log)(...t)}(`%c${d}%c (${p}) userscript has loaded!`,"color: cornflowerblue;","")})();
\ No newline at end of file
+(()=>{var t,e,n=t=>{throw TypeError(t)},i=(t,e,i)=>e.has(t)?n("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,i),s=(t,e,i)=>(((t,e)=>{e.has(t)||n("Cannot access private method")})(t,e),i),o=class{constructor(e,n){i(this,t),this.name=e,this.version=n,this.t=null,this.i="bm-b",this.o=null,this.l=null,this.h=[]}u(t){this.t=t}m(){return this.h.length>0&&(this.l=this.h.pop()),this}p(t){t?.appendChild(this.o),this.o=null,this.l=null,this.h=[]}v(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"div",{},n)),this}M(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"p",{},n)),this}$(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"small",{},n)),this}C(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"img",{},n)),this}D(n,i={},o=()=>{}){return o(this,s(this,t,e).call(this,"h"+n,{},i)),this}T(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"hr",{},n)),this}I(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"br",{},n)),this}k(n={},i=()=>{}){const o=s(this,t,e).call(this,"label",{textContent:n.textContent??""});delete n.textContent;const a=s(this,t,e).call(this,"input",{type:"checkbox"},n);return o.insertBefore(a,o.firstChild),this.m(),i(this,o,a),this}N(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"button",{},n)),this}S(n={},i=()=>{}){const o=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${o}`;const a={textContent:"?",className:"bm-q",onclick:()=>{this.B(this.i,o)}};return i(this,s(this,t,e).call(this,"button",a,n)),this}O(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"input",{},n)),this}L(n={},i=()=>{}){const o=n.textContent??"";delete n.textContent;const a=s(this,t,e).call(this,"div"),r=s(this,t,e).call(this,"input",{type:"file",style:"display: none !important; visibility: hidden !important; position: absolute !important; left: -9999px !important; width: 0 !important; height: 0 !important; opacity: 0 !important;"},n);this.m();const c=s(this,t,e).call(this,"button",{textContent:o});return this.m(),this.m(),r.setAttribute("tabindex","-1"),r.setAttribute("aria-hidden","true"),c.addEventListener("click",()=>{r.click()}),r.addEventListener("change",()=>{c.style.maxWidth=`${c.offsetWidth}px`,r.files.length>0?c.textContent=r.files[0].name:c.textContent=o}),i(this,a,r,c),this}H(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"textarea",{},n)),this}B(t,e,n=!1){const i=document.getElementById(t.replace(/^#/,""));i&&(i instanceof HTMLInputElement?i.value=e:n?i.textContent=e:i.innerHTML=e)}j(t,e){let n,i=!1,s=0,o=null,a=0,r=0,c=0,l=0;if(t=document.querySelector("#"==t?.[0]?t:"#"+t),e=document.querySelector("#"==e?.[0]?e:"#"+e),!t||!e)return void this.q(`Can not drag! ${t?"":"moveMe"} ${t||e?"":"and "}${e?"":"iMoveThings "}was not found!`);const h=()=>{if(i){const e=Math.abs(a-c),n=Math.abs(r-l);(e>.5||n>.5)&&(a=c,r=l,t.style.transform=`translate(${a}px, ${r}px)`,t.style.left="0px",t.style.top="0px",t.style.right=""),o=requestAnimationFrame(h)}};let u=null;const m=(m,d)=>{i=!0,u=t.getBoundingClientRect(),n=m-u.left,s=d-u.top;const p=window.getComputedStyle(t).transform;if(p&&"none"!==p){const t=new DOMMatrix(p);a=t.m41,r=t.m42}else a=u.left,r=u.top;c=a,l=r,document.body.style.userSelect="none",e.classList.add("dragging"),o&&cancelAnimationFrame(o),h()},d=()=>{i=!1,o&&(cancelAnimationFrame(o),o=null),document.body.style.userSelect="",e.classList.remove("dragging")};e.addEventListener("mousedown",function(t){t.preventDefault(),m(t.clientX,t.clientY)}),e.addEventListener("touchstart",function(t){const e=t?.touches?.[0];e&&(m(e.clientX,e.clientY),t.preventDefault())},{passive:!1}),document.addEventListener("mousemove",function(t){i&&u&&(c=t.clientX-n,l=t.clientY-s)},{passive:!0}),document.addEventListener("touchmove",function(t){if(i&&u){const e=t?.touches?.[0];if(!e)return;c=e.clientX-n,l=e.clientY-s,t.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",d),document.addEventListener("touchend",d),document.addEventListener("touchcancel",d)}A(t){(0,console.info)(`${this.name}: ${t}`),this.B(this.i,"Status: "+t,!0)}q(t){(0,console.error)(`${this.name}: ${t}`),this.B(this.i,"Error: "+t,!0)}};function a(t,e){if(0===t)return e[0];let n="";const i=e.length;for(;t>0;)n=e[t%i]+n,t=Math.floor(t/i);return n}function r(t){let e="";for(let n=0;n0)for(const t in e){const n=t,i=e[t];if(e.hasOwnProperty(t)){const t=n.split(" "),e=Number(t?.[0]),s=t?.[1]||"0",o=i.name||`Template ${e||""}`,a=i.tiles,r={};for(const t in a)if(a.hasOwnProperty(t)){const e=c(a[t]),n=new Blob([e],{type:"image/png"}),i=await createImageBitmap(n);r[t]=i}const l=new m({displayName:o,_:e||this.X?.length||0,F:s||""});l.P=r,this.X.push(l)}}};var d=GM_info.script.name.toString(),p=GM_info.script.version.toString();!function(t){const e=document.createElement("script");e.setAttribute("bm-r",d),e.setAttribute("bm-o","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement?.appendChild(e),e.remove()}(()=>{const t=document.currentScript,e=t?.getAttribute("bm-r")||"Blue Marble",n=t?.getAttribute("bm-o")||"",i=new Map;window.addEventListener("message",t=>{const{source:s,endpoint:o,blobID:a,blobData:r,blink:c}=t.data;if(Date.now(),"blue-marble"==s&&a&&r&&!o){const t=i.get(a);"function"==typeof t?t(r):function(...t){(0,console.warn)(...t)}(`%c${e}%c: Attempted to retrieve a blob (%s) from queue, but the blobID was not a function! Skipping...`,n,"",a),i.delete(a)}});const s=window.fetch;window.fetch=async function(...t){const e=await s.apply(this,t),n=e.clone(),o=(t[0]instanceof Request?t[0]?.url:t[0])||"ignore",a=n.headers.get("content-type")||"";if(a.includes("application/json"))n.json().then(t=>{window.postMessage({source:"blue-marble",endpoint:o,jsonData:t},"*")}).catch(t=>{});else if(a.includes("image/")&&!o.includes("openfreemap")&&!o.includes("maps")){const t=Date.now(),e=await n.blob();return new Promise(s=>{const a=crypto.randomUUID();i.set(a,t=>{s(new Response(t,{headers:n.headers,status:n.status,statusText:n.statusText}))}),window.postMessage({source:"blue-marble",endpoint:o,blobID:a,blobData:e,blink:t})}).catch(t=>{Date.now()})}return e}});var b=GM_getResourceText("CSS-BM-File");GM_addStyle(b);var f=document.createElement("link");f.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",f.rel="preload",f.as="style",f.onload=function(){this.onload=null,this.rel="stylesheet"},document.head?.appendChild(f),new class{constructor(){this.Z=null,this.K=null,this.tt="#bm-5"}et(t){return this.K=t,this.Z=new MutationObserver(t=>{for(const e of t)for(const t of e.addedNodes)t instanceof HTMLElement&&t.matches?.(this.tt)}),this}nt(){return this.Z}observe(t,e=!1,n=!1){t.observe(this.K,{childList:e,subtree:n})}};var w=new o(d,p),v=(new o(d,p),new class{constructor(t,e,n){i(this,l),this.name=t,this.version=e,this.o=n,this.it="1.0.0",this.st=null,this.ot="!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",this.R=1e3,this.rt=3,this.ct=null,this.lt=null,this.ht="bm-p",this.ut="div#map canvas.maplibregl-canvas",this.dt=null,this.bt="",this.X=[],this.W=null,this.ft=!0}wt(){if(document.body.contains(this.ct))return this.ct;document.getElementById(this.ht)?.remove();const t=document.querySelector(this.ut),e=document.createElement("canvas");return e.id=this.ht,e.className="maplibregl-canvas",e.style.position="absolute",e.style.top="0",e.style.left="0",e.style.height=t?.clientHeight*(window.devicePixelRatio||1)+"px",e.style.width=t?.clientWidth*(window.devicePixelRatio||1)+"px",e.height=t?.clientHeight*(window.devicePixelRatio||1),e.width=t?.clientWidth*(window.devicePixelRatio||1),e.style.zIndex="8999",e.style.pointerEvents="none",t?.parentElement?.appendChild(e),this.ct=e,window.addEventListener("move",this.vt),window.addEventListener("zoom",this.yt),window.addEventListener("resize",this.xt),this.ct}async gt(){return{whoami:this.name.replace(" ",""),scriptVersion:this.version,schemaVersion:this.it,templates:{}}}async Mt(t,e,n){this.W||(this.W=await this.gt()),this.o.A(`Creating template at ${n.join(", ")}...`);const i=new m({displayName:e,_:0,F:a(this.st||0,this.ot),file:t,coords:n}),{Y:o,J:r}=await i.U(this.R);i.P=o,this.W.templates[`${i._} ${i.F}`]={name:i.displayName,coords:n.join(", "),enabled:!0,tiles:r},this.X=[],this.X.push(i);const c=(new Intl.NumberFormat).format(i.G);this.o.A(`Template created at ${n.join(", ")}! Total pixels: ${c}`),await s(this,l,h).call(this)}$t(){}async Ct(){this.W||(this.W=await this.gt())}async Dt(t,e){if(!this.ft)return t;const n=this.R*this.rt;e=e[0].toString().padStart(4,"0")+","+e[1].toString().padStart(4,"0");const i=this.X;i.sort((t,e)=>t._-e._);const s=i.map(t=>{const n=Object.keys(t.P).filter(t=>t.startsWith(e));if(0===n.length)return null;const i=n.map(e=>{const n=e.split(",");return{Tt:t.P[e],It:[n[0],n[1]],kt:[n[2],n[3]]}});return i?.[0]}).filter(Boolean),o=s?.length||0;if(o>0){const t=i.filter(t=>Object.keys(t.P).filter(t=>t.startsWith(e)).length>0).reduce((t,e)=>t+(e.G||0),0),n=(new Intl.NumberFormat).format(t);this.o.A(`Displaying ${o} template${1==o?"":"s"}.\nTotal pixels: ${n}`)}else this.o.A(`Displaying ${o} templates.`);const a=await createImageBitmap(t),r=new OffscreenCanvas(n,n),c=r.getContext("2d");c.imageSmoothingEnabled=!1,c.beginPath(),c.rect(0,0,n,n),c.clip(),c.clearRect(0,0,n,n),c.drawImage(a,0,0,n,n);for(const t of s)c.drawImage(t.Tt,Number(t.kt[0])*this.rt,Number(t.kt[1])*this.rt);return await r.convertToBlob({type:"image/png"})}Nt(t){"BlueMarble"==t?.whoami&&s(this,l,u).call(this,t)}St(t){this.ft=t}}(d,p,w)),y=new class{constructor(t){this.Bt=t,this.Ot=!1,this.Lt=[],this.zt=[]}Ht(t){window.addEventListener("message",async e=>{const n=e.data,i=n.jsonData;if(!n||"blue-marble"!==n.source)return;if(!n.endpoint)return;const s=n.endpoint?.split("?")[0].split("/").filter(t=>t&&isNaN(Number(t))).filter(t=>t&&!t.includes(".")).pop();switch(s){case"me":if(i.status&&"2"!=i.status?.toString()[0])return void t.q("You are not logged in!\nCould not fetch userdata.");const e=Math.ceil(Math.pow(Math.floor(i.level)*Math.pow(30,.65),1/.65)-i.pixelsPainted);i.id||i.id,this.Bt.st=i.id,t.B("bm-h",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(i.name)}`),t.B("bm-c",`Droplets: ${(new Intl.NumberFormat).format(i.droplets)}`),t.B("bm-6",`Next level in ${(new Intl.NumberFormat).format(e)} pixel${1==e?"":"s"}`);break;case"pixel":const s=n.endpoint.split("?")[0].split("/").filter(t=>t&&!isNaN(Number(t))),r=new URLSearchParams(n.endpoint.split("?")[1]),c=[r.get("x"),r.get("y")];if(this.Lt.length&&(!s.length||!c.length))return void t.q("Coordinates are malformed!\nDid you try clicking the canvas first?");this.Lt=[...s,...c];const l=(o=s,a=c,[parseInt(o[0])%4*1e3+parseInt(a[0]),parseInt(o[1])%4*1e3+parseInt(a[1])]),h=document.querySelectorAll("span");for(const t of h)if(t.textContent.trim().includes(`${l[0]}, ${l[1]}`)){let e=document.querySelector("#bm-5");const n=`(Tl X: ${s[0]}, Tl Y: ${s[1]}, Px X: ${c[0]}, Px Y: ${c[1]})`;e?e.textContent=n:(e=document.createElement("span"),e.id="bm-5",e.textContent=n,e.style="margin-left: calc(var(--spacing)*3); font-size: small;",t.parentNode.parentNode.parentNode.insertAdjacentElement("afterend",e))}break;case"tiles":let u=n.endpoint.split("/");u=[parseInt(u[u.length-2]),parseInt(u[u.length-1].replace(".png",""))];const m=n.blobID,d=n.blobData,p=await this.Bt.Dt(d,u);window.postMessage({source:"blue-marble",blobID:m,blobData:p,blink:n.blink});break;case"robots":this.Ot="false"==i.userscript?.toString().toLowerCase()}var o,a})}}(v);w.u(y);var x=JSON.parse(GM_getValue("bmTemplates","{}"));v.Nt(x),function(){let t=!1;w.v({id:"bm-n",style:"top: 10px; right: 75px;"}).v({id:"bm-7"}).v({id:"bm-i"}).m().C({alt:"Blue Marble Icon - Click to minimize/maximize",src:"https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png",style:"cursor: pointer;"},(e,n)=>{n.addEventListener("click",()=>{t=!t;const i=document.querySelector("#bm-n"),s=document.querySelector("#bm-7"),o=document.querySelector("#bm-i"),a=document.querySelector("#bm-8"),r=document.querySelector("#bm-d"),c=document.querySelector("#bm-e"),l=document.querySelector("#bm-f"),h=document.querySelector("#bm-9"),u=document.querySelectorAll("#bm-8 input");t||(i.style.width="auto",i.style.maxWidth="300px",i.style.minWidth="200px",i.style.padding="10px"),["#bm-n h1","#bm-4","#bm-n hr","#bm-3 > *:not(#bm-8)","#bm-2","#bm-1",`#${e.i}`].forEach(e=>{document.querySelectorAll(e).forEach(e=>{e.style.display=t?"none":""})}),t?(a&&(a.style.display="none"),r&&(r.style.display="none"),c&&(c.style.display="none"),l&&(l.style.display="none"),h&&(h.style.display="none"),u.forEach(t=>{t.style.display="none"}),i.style.width="60px",i.style.height="76px",i.style.maxWidth="60px",i.style.minWidth="60px",i.style.padding="8px",n.style.marginLeft="3px",s.style.textAlign="center",s.style.margin="0",s.style.marginBottom="0",o&&(o.style.display="",o.style.marginBottom="0.25em")):(a&&(a.style.display="",a.style.flexDirection="",a.style.justifyContent="",a.style.alignItems="",a.style.gap="",a.style.textAlign="",a.style.margin=""),r&&(r.style.display=""),c&&(c.style.display="",c.style.marginTop=""),l&&(l.style.display="",l.style.marginTop=""),h&&(h.style.display="",h.style.marginTop=""),u.forEach(t=>{t.style.display=""}),n.style.marginLeft="",i.style.padding="10px",s.style.textAlign="",s.style.margin="",s.style.marginBottom="",o&&(o.style.marginBottom="0.5em"),i.style.width="",i.style.height=""),n.alt=t?"Blue Marble Icon - Minimized (Click to maximize)":"Blue Marble Icon - Maximized (Click to minimize)"})}).m().D(1,{textContent:d}).m().m().T().m().v({id:"bm-4"}).M({id:"bm-h",textContent:"Username:"}).m().M({id:"bm-c",textContent:"Droplets:"}).m().M({id:"bm-6",textContent:"Next level in..."}).m().m().T().m().v({id:"bm-3"}).v({id:"bm-8"}).N({id:"bm-d",className:"bm-q",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.t?.Lt;e?.[0]?(t.B("bm-j",e?.[0]||""),t.B("bm-k",e?.[1]||""),t.B("bm-l",e?.[2]||""),t.B("bm-m",e?.[3]||"")):t.q("Coordinates are malformed! Did you try clicking on the canvas first?")}}).m().O({type:"number",id:"bm-j",placeholder:"Tl X",min:0,max:2047,step:1,required:!0}).m().O({type:"number",id:"bm-k",placeholder:"Tl Y",min:0,max:2047,step:1,required:!0}).m().O({type:"number",id:"bm-l",placeholder:"Px X",min:0,max:2047,step:1,required:!0}).m().O({type:"number",id:"bm-m",placeholder:"Px Y",min:0,max:2047,step:1,required:!0}).m().m().L({id:"bm-2",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).m().v({id:"bm-0"}).N({id:"bm-f",textContent:"Enable"},(t,e)=>{e.onclick=()=>{t.t?.Bt?.St(!0),t.A("Enabled templates!")}}).m().N({id:"bm-e",textContent:"Create"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-2"),n=document.querySelector("#bm-j");if(!n.checkValidity())return n.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");const i=document.querySelector("#bm-k");if(!i.checkValidity())return i.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");const s=document.querySelector("#bm-l");if(!s.checkValidity())return s.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");const o=document.querySelector("#bm-m");if(!o.checkValidity())return o.reportValidity(),void t.q("Coordinates are malformed! Did you try clicking on the canvas first?");e?.files[0]?(v.Mt(e.files[0],e.files[0]?.name.replace(/\.[^/.]+$/,""),[Number(n.value),Number(i.value),Number(s.value),Number(o.value)]),t.A("Drew to canvas!")):t.q("No file selected!")}}).m().N({id:"bm-9",textContent:"Disable"},(t,e)=>{e.onclick=()=>{t.t?.Bt?.St(!1),t.A("Disabled templates!")}}).m().m().H({id:w.i,placeholder:`Status: Sleeping...\nVersion: ${p}`,readOnly:!0}).m().v({id:"bm-1"}).v().N({id:"bm-a",className:"bm-q",innerHTML:"🎨",title:"Template Color Converter"},(t,e)=>{e.addEventListener("click",()=>{window.open("https://pepoafonso.github.io/color_converter_wplace/","_blank","noopener noreferrer")})}).m().m().$({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).m().m().m().p(document.body)}(),w.j("#bm-n","#bm-i"),y.Ht(w),new MutationObserver((t,e)=>{const n=document.querySelector("#color-1");if(!n)return;let i=document.querySelector("#bm-g");if(!i){i=document.createElement("button"),i.id="bm-g",i.textContent="Move ↑",i.className="btn btn-soft",i.onclick=function(){const t=this.parentNode.parentNode.parentNode.parentNode,e="Move ↑"==this.textContent;t.parentNode.className=t.parentNode.className.replace(e?"bottom":"top",e?"top":"bottom"),t.style.borderTopLeftRadius=e?"0px":"var(--radius-box)",t.style.borderTopRightRadius=e?"0px":"var(--radius-box)",t.style.borderBottomLeftRadius=e?"var(--radius-box)":"0px",t.style.borderBottomRightRadius=e?"var(--radius-box)":"0px",this.textContent=e?"Move ↓":"Move ↑"};const t=n.parentNode.parentNode.parentNode.parentNode.querySelector("h2");t.parentNode?.appendChild(i)}}).observe(document.body,{childList:!0,subtree:!0}),function(...t){(0,console.log)(...t)}(`%c${d}%c (${p}) userscript has loaded!`,"color: cornflowerblue;","")})();
\ No newline at end of file
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 692e07d..3aa6adb 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -61,6 +61,7 @@
I don't want to waste your time, so double check with me before starting a big change like adding a new feature. For example, imagine you spend 50 hours making a bot that automatically places pixels, then your pull request was rejected because a bot that automatically places pixles does not align with the "Mission" of Blue Marble. That would be sad :(
Follow the style of the project. E.g., if all overlays are made by calling `Overlay()`, and you want to make a new overlay, you should probably call `Overlay()` as well.
- Most of the work to be done in this userscript is related to programming. It is helpful to have a background in programming, but not required. If you are looking to learn JavaScript and its syntax, check out this roadmap for learning JavaScript. We strongly recommend that you understand functions, methods, classes, and Object-Oriented-Programming if you plan to implement a brand new feature. More technical knowledge like method chaining and lambda expressions are useful but not required.
+ Most of the work to be done in this userscript is related to programming. It is helpful to have a background in programming, but not required. If you are looking to learn JavaScript and its syntax, check out this roadmap for learning JavaScript. We strongly recommend that you understand functions, methods, classes, and Object-Oriented-Programming if you plan to implement a brand new feature. More technical knowledge like method chaining and lambda expressions are useful but not required. You can find the documentation for Blue Marble here.
/** The overlay builder for the Blue Marble script.
- * @description This class handles the overlay UI for the Blue Marble script.
- * @since 0.0.2
- * @example
- * const overlay = new Overlay();
- * overlay.addDiv({ 'id': 'overlay' })
- * .addDiv({ 'id': 'header' })
- * .addHeader(1, {'textContent': 'Your Overlay'}).buildElement()
- * .addP({'textContent': 'This is your overlay. It is versatile.'}).buildElement()
- * .buildElement() // Marks the end of the header <div>
- * .addHr().buildElement()
- * .buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <div id="overlay">
- * <div id="header">
- * <h1>Your Overlay</h1>
- * <p>This is your overlay. It is versatile.</p>
- * </div>
- * <hr>
- * </div>
- * </body>
-*/
-export default class Overlay {
-
- /** Constructor for the Overlay class.
- * @param {string} name - The name of the userscript
- * @param {string} version - The version of the userscript
- * @since 0.0.2
- * @see {@link Overlay}
- */
- constructor(name, version) {
- this.name = name; // Name of userscript
- this.version = version; // Version of userscript
-
- this.apiManager = null; // The API manager instance. Later populated when setApiManager is called
-
- this.outputStatusId = 'bm-output-status'; // ID for status element
-
- this.overlay = null; // The overlay root DOM HTMLElement
- this.currentParent = null; // The current parent HTMLElement in the overlay
- this.parentStack = []; // Tracks the parent elements BEFORE the currentParent so we can nest elements
- }
-
- /** Populates the apiManager variable with the apiManager class.
- * @param {apiManager} apiManager - The apiManager class instance
- * @since 0.41.4
- */
- setApiManager(apiManager) {this.apiManager = apiManager;}
-
- /** Creates an element.
- * For **internal use** of the {@link Overlay} class.
- * @param {string} tag - The tag name as a string.
- * @param {Object.<string, any>} [properties={}] - The DOM properties of the element.
- * @returns {HTMLElement} HTML Element
- * @since 0.43.2
- */
- #createElement(tag, properties = {}, additionalProperties={}) {
-
- const element = document.createElement(tag); // Creates the element
-
- // If this is the first element made...
- if (!this.overlay) {
- this.overlay = element; // Declare it the highest overlay element
- this.currentParent = element;
- } else {
- this.currentParent?.appendChild(element); // ...else delcare it the child of the last element
- this.parentStack.push(this.currentParent);
- this.currentParent = element;
- }
-
- // For every passed in property (shared by all like-elements), apply the it to the element
- for (const [property, value] of Object.entries(properties)) {
- element[property] = value;
- }
-
- // For every passed in additional property, apply the it to the element
- for (const [property, value] of Object.entries(additionalProperties)) {
- element[property] = value;
- }
-
- return element;
- }
-
- /** Finishes building an element.
- * Call this after you are finished adding children.
- * If the element will have no children, call it anyways.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.2
- * @example
- * overlay
- * .addDiv()
- * .addHeader(1).buildElement() // Breaks out of the <h1>
- * .addP().buildElement() // Breaks out of the <p>
- * .buildElement() // Breaks out of the <div>
- * .addHr() // Since there are no more elements, calling buildElement() is optional
- * .buildOverlay(document.body);
- */
- buildElement() {
- if (this.parentStack.length > 0) {
- this.currentParent = this.parentStack.pop();
- }
- return this;
- }
-
- /** Finishes building the overlay and displays it.
- * Call this when you are done chaining methods.
- * @param {HTMLElement} parent - The parent HTMLElement this overlay should be appended to as a child.
- * @since 0.43.2
- * @example
- * overlay
- * .addDiv()
- * .addP().buildElement()
- * .buildElement()
- * .buildOverlay(document.body); // Adds DOM structure to document body
- * // <div><p></p></div>
- */
- buildOverlay(parent) {
- parent?.appendChild(this.overlay);
-
- // Resets the class-bound variables of this class instance back to default so overlay can be build again later
- this.overlay = null;
- this.currentParent = null;
- this.parentStack = [];
- }
-
- /** Adds a `div` to the overlay.
- * This `div` element will have properties shared between all `div` elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `div` that are NOT shared between all overlay `div` elements. These should be camelCase.
- * @param {function(Overlay, HTMLDivElement):void} [callback=()=>{}] - Additional JS modification to the `div`.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.2
- * @example
- * // Assume all <div> elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addDiv({'id': 'foo'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <div id="foo" class="bar"></div>
- * </body>
- */
- addDiv(additionalProperties = {}, callback = () => {}) {
-
- const properties = {}; // Shared <div> DOM properties
-
- const div = this.#createElement('div', properties, additionalProperties); // Creates the <div> element
- callback(this, div); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a `p` to the overlay.
- * This `p` element will have properties shared between all `p` elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `p` that are NOT shared between all overlay `p` elements. These should be camelCase.
- * @param {function(Overlay, HTMLParagraphElement):void} [callback=()=>{}] - Additional JS modification to the `p`.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.2
- * @example
- * // Assume all <p> elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addP({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <p id="foo" class="bar">Foobar.</p>
- * </body>
- */
- addP(additionalProperties = {}, callback = () => {}) {
-
- const properties = {}; // Shared <p> DOM properties
-
- const p = this.#createElement('p', properties, additionalProperties); // Creates the <p> element
- callback(this, p); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a `small` to the overlay.
- * This `small` element will have properties shared between all `small` elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `small` that are NOT shared between all overlay `small` elements. These should be camelCase.
- * @param {function(Overlay, HTMLParagraphElement):void} [callback=()=>{}] - Additional JS modification to the `small`.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.55.8
- * @example
- * // Assume all <small> elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addSmall({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <small id="foo" class="bar">Foobar.</small>
- * </body>
- */
- addSmall(additionalProperties = {}, callback = () => {}) {
-
- const properties = {}; // Shared <small> DOM properties
-
- const small = this.#createElement('small', properties, additionalProperties); // Creates the <small> element
- callback(this, small); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a `img` to the overlay.
- * This `img` element will have properties shared between all `img` elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `img` that are NOT shared between all overlay `img` elements. These should be camelCase.
- * @param {function(Overlay, HTMLImageElement):void} [callback=()=>{}] - Additional JS modification to the `img`.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.2
- * @example
- * // Assume all <img> elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addimg({'id': 'foo', 'src': './img.png'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <img id="foo" src="./img.png" class="bar">
- * </body>
- */
- addImg(additionalProperties = {}, callback = () => {}) {
-
- const properties = {}; // Shared <img> DOM properties
-
- const img = this.#createElement('img', properties, additionalProperties); // Creates the <img> element
- callback(this, img); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a header to the overlay.
- * This header element will have properties shared between all header elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {number} level - The header level. Must be between 1 and 6 (inclusive)
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the header that are NOT shared between all overlay header elements. These should be camelCase.
- * @param {function(Overlay, HTMLHeadingElement):void} [callback=()=>{}] - Additional JS modification to the header.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.7
- * @example
- * // Assume all header elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addHeader(6, {'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <h6 id="foo" class="bar">Foobar.</h6>
- * </body>
- */
- addHeader(level, additionalProperties = {}, callback = () => {}) {
-
- const properties = {}; // Shared header DOM properties
-
- const header = this.#createElement('h' + level, properties, additionalProperties); // Creates the header element
- callback(this, header); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a `hr` to the overlay.
- * This `hr` element will have properties shared between all `hr` elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `hr` that are NOT shared between all overlay `hr` elements. These should be camelCase.
- * @param {function(Overlay, HTMLHRElement):void} [callback=()=>{}] - Additional JS modification to the `hr`.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.7
- * @example
- * // Assume all <hr> elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addhr({'id': 'foo'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <hr id="foo" class="bar">
- * </body>
- */
- addHr(additionalProperties = {}, callback = () => {}) {
-
- const properties = {}; // Shared <hr> DOM properties
-
- const hr = this.#createElement('hr', properties, additionalProperties); // Creates the <hr> element
- callback(this, hr); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a `br` to the overlay.
- * This `br` element will have properties shared between all `br` elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `br` that are NOT shared between all overlay `br` elements. These should be camelCase.
- * @param {function(Overlay, HTMLBRElement):void} [callback=()=>{}] - Additional JS modification to the `br`.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.11
- * @example
- * // Assume all <br> elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addbr({'id': 'foo'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <br id="foo" class="bar">
- * </body>
- */
- addBr(additionalProperties = {}, callback = () => {}) {
-
- const properties = {}; // Shared <br> DOM properties
-
- const br = this.#createElement('br', properties, additionalProperties); // Creates the <br> element
- callback(this, br); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a checkbox to the overlay.
- * This checkbox element will have properties shared between all checkbox elements in the overlay.
- * You can override the shared properties by using a callback. Note: the checkbox element is inside a label element.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the checkbox that are NOT shared between all overlay checkbox elements. These should be camelCase.
- * @param {function(Overlay, HTMLLabelElement, HTMLInputElement):void} [callback=()=>{}] - Additional JS modification to the checkbox.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.10
- * @example
- * // Assume all checkbox elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addCheckbox({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <label>
- * <input type="checkbox" id="foo" class="bar">
- * "Foobar."
- * </label>
- * </body>
- */
- addCheckbox(additionalProperties = {}, callback = () => {}) {
-
- const properties = {'type': 'checkbox'}; // Shared checkbox DOM properties
-
- const label = this.#createElement('label', {'textContent': additionalProperties['textContent'] ?? ''}); // Creates the label element
- delete additionalProperties['textContent']; // Deletes 'textContent' DOM property before adding the properties to the checkbox
- const checkbox = this.#createElement('input', properties, additionalProperties); // Creates the checkbox element
- label.insertBefore(checkbox, label.firstChild); // Makes the checkbox the first child of the label (before the text content)
- this.buildElement(); // Signifies that we are done adding children to the checkbox
- callback(this, label, checkbox); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a `button` to the overlay.
- * This `button` element will have properties shared between all `button` elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `button` that are NOT shared between all overlay `button` elements. These should be camelCase.
- * @param {function(Overlay, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the `button`.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.12
- * @example
- * // Assume all <button> elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addButton({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <button id="foo" class="bar">Foobar.</button>
- * </body>
- */
- addButton(additionalProperties = {}, callback = () => {}) {
-
- const properties = {}; // Shared <button> DOM properties
-
- const button = this.#createElement('button', properties, additionalProperties); // Creates the <button> element
- callback(this, button); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a help button to the overlay. It will have a "?" icon unless overridden in callback.
- * On click, the button will attempt to output the title to the output element (ID defined in Overlay constructor).
- * This `button` element will have properties shared between all `button` elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `button` that are NOT shared between all overlay `button` elements. These should be camelCase.
- * @param {function(Overlay, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the `button`.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.12
- * @example
- * // Assume all help button elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addButtonHelp({'id': 'foo', 'title': 'Foobar.'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <button id="foo" class="bar" title="Help: Foobar.">?</button>
- * </body>
- * @example
- * // Assume all help button elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addButtonHelp({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <button id="foo" class="bar" title="Help: Foobar.">?</button>
- * </body>
- */
- addButtonHelp(additionalProperties = {}, callback = () => {}) {
-
- const tooltip = additionalProperties['title'] ?? additionalProperties['textContent'] ?? 'Help: No info'; // Retrieves the tooltip
-
- // Makes sure the tooltip is stored in the title property
- delete additionalProperties['textContent'];
- additionalProperties['title'] = `Help: ${tooltip}`;
-
- // Shared help button DOM properties
- const properties = {
- 'textContent': '?',
- 'className': 'bm-help',
- 'onclick': () => {
- this.updateInnerHTML(this.outputStatusId, tooltip);
- }
- };
-
- const help = this.#createElement('button', properties, additionalProperties); // Creates the <button> element
- callback(this, help); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a `input` to the overlay.
- * This `input` element will have properties shared between all `input` elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `input` that are NOT shared between all overlay `input` elements. These should be camelCase.
- * @param {function(Overlay, HTMLInputElement):void} [callback=()=>{}] - Additional JS modification to the `input`.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.13
- * @example
- * // Assume all <input> elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addInput({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <input id="foo" class="bar">Foobar.</input>
- * </body>
- */
- addInput(additionalProperties = {}, callback = () => {}) {
-
- const properties = {}; // Shared <input> DOM properties
-
- const input = this.#createElement('input', properties, additionalProperties); // Creates the <input> element
- callback(this, input); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a file input to the overlay with enhanced visibility controls.
- * This input element will have properties shared between all file input elements in the overlay.
- * Uses multiple hiding methods to prevent browser native text from appearing during minimize/maximize.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the file input that are NOT shared between all overlay file input elements. These should be camelCase.
- * @param {function(Overlay, HTMLDivElement, HTMLInputElement, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the file input.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.17
- * @example
- * // Assume all file input elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addInputFile({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <div>
- * <input type="file" id="foo" class="bar" style="display: none"></input>
- * <button>Foobar.</button>
- * </div>
- * </body>
- */
- addInputFile(additionalProperties = {}, callback = () => {}) {
-
- const properties = {
- 'type': 'file',
- 'style': 'display: none !important; visibility: hidden !important; position: absolute !important; left: -9999px !important; width: 0 !important; height: 0 !important; opacity: 0 !important;'
- }; // Complete file input hiding to prevent native browser text interference
- const text = additionalProperties['textContent'] ?? ''; // Retrieves the text content
-
- delete additionalProperties['textContent']; // Deletes the text content before applying the additional properties to the file input
-
- const container = this.#createElement('div'); // Container for file input
- const input = this.#createElement('input', properties, additionalProperties); // Creates the file input
- this.buildElement(); // Signifies that we are done adding children to the file input
- const button = this.#createElement('button', {'textContent': text});
- this.buildElement(); // Signifies that we are done adding children to the button
- this.buildElement(); // Signifies that we are done adding children to the container
-
- // Prevent file input from being accessible or visible by screen-readers and tabbing
- input.setAttribute('tabindex', '-1');
- input.setAttribute('aria-hidden', 'true');
-
- button.addEventListener('click', () => {
- input.click(); // Clicks the file input
- });
-
- // Update button text when file is selected
- input.addEventListener('change', () => {
- button.style.maxWidth = `${button.offsetWidth}px`;
- if (input.files.length > 0) {
- button.textContent = input.files[0].name;
- } else {
- button.textContent = text;
- }
- });
-
- callback(this, container, input, button); // Runs any script passed in through the callback
- return this;
- }
-
- /** Adds a `textarea` to the overlay.
- * This `textarea` element will have properties shared between all `textarea` elements in the overlay.
- * You can override the shared properties by using a callback.
- * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `textarea` that are NOT shared between all overlay `textarea` elements. These should be camelCase.
- * @param {function(Overlay, HTMLTextAreaElement):void} [callback=()=>{}] - Additional JS modification to the `textarea`.
- * @returns {Overlay} Overlay class instance (this)
- * @since 0.43.13
- * @example
- * // Assume all <textarea> elements have a shared class (e.g. {'className': 'bar'})
- * overlay.addTextarea({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
- * // Output:
- * // (Assume <body> already exists in the webpage)
- * <body>
- * <textarea id="foo" class="bar">Foobar.</textarea>
- * </body>
- */
- addTextarea(additionalProperties = {}, callback = () => {}) {
-
- const properties = {}; // Shared <textarea> DOM properties
-
- const textarea = this.#createElement('textarea', properties, additionalProperties); // Creates the <textarea> element
- callback(this, textarea); // Runs any script passed in through the callback
- return this;
- }
-
- /** Updates the inner HTML of the element.
- * The element is discovered by it's id.
- * If the element is an `input`, it will modify the value attribute instead.
- * @param {string} id - The ID of the element to change
- * @param {string} html - The HTML/text to update with
- * @param {boolean} [doSafe] - (Optional) Should `textContent` be used instead of `innerHTML` to avoid XSS? False by default
- * @since 0.24.2
- */
- updateInnerHTML(id, html, doSafe=false) {
-
- const element = document.getElementById(id.replace(/^#/, '')); // Retrieve the element from the 'id' (removed the '#')
-
- if (!element) {return;} // Kills itself if the element does not exist
-
- // Input elements don't have innerHTML, so we modify the value attribute instead
- if (element instanceof HTMLInputElement) {
- element.value = html;
- return;
- }
-
- if (doSafe) {
- element.textContent = html; // Populate element with plain-text HTML/text
- } else {
- element.innerHTML = html; // Populate element with HTML/text
- }
- }
-
- /** Handles dragging of the overlay.
- * Uses requestAnimationFrame for smooth animations and GPU-accelerated transforms.
- * @param {string} moveMe - The ID of the element to be moved
- * @param {string} iMoveThings - The ID of the drag handle element
- * @since 0.8.2
- */
- handleDrag(moveMe, iMoveThings) {
- let isDragging = false;
- let offsetX, offsetY = 0;
- let animationFrame = null;
- let currentX = 0;
- let currentY = 0;
- let targetX = 0;
- let targetY = 0;
-
- // Retrieves the elements (allows either '#id' or 'id' to be passed in)
- moveMe = document.querySelector(moveMe?.[0] == '#' ? moveMe : '#' + moveMe);
- iMoveThings = document.querySelector(iMoveThings?.[0] == '#' ? iMoveThings : '#' + iMoveThings);
-
- // What to do when one of the two elements are not found
- if (!moveMe || !iMoveThings) {
- this.handleDisplayError(`Can not drag! ${!moveMe ? 'moveMe' : ''} ${!moveMe && !iMoveThings ? 'and ' : ''}${!iMoveThings ? 'iMoveThings ' : ''}was not found!`);
- return; // Kills itself
- }
-
- // Smooth animation loop using requestAnimationFrame for optimal performance
- const updatePosition = () => {
- if (isDragging) {
- // Only update DOM if position changed significantly (reduce repaints)
- const deltaX = Math.abs(currentX - targetX);
- const deltaY = Math.abs(currentY - targetY);
-
- if (deltaX > 0.5 || deltaY > 0.5) {
- currentX = targetX;
- currentY = targetY;
-
- // Use CSS transform for GPU acceleration instead of left/top
- moveMe.style.transform = `translate(${currentX}px, ${currentY}px)`;
- moveMe.style.left = '0px';
- moveMe.style.top = '0px';
- moveMe.style.right = '';
- }
-
- animationFrame = requestAnimationFrame(updatePosition);
- }
- };
-
- // Cache initial position to avoid expensive getBoundingClientRect calls during drag
- let initialRect = null;
-
- const startDrag = (clientX, clientY) => {
- isDragging = true;
- initialRect = moveMe.getBoundingClientRect();
- offsetX = clientX - initialRect.left;
- offsetY = clientY - initialRect.top;
-
- // Get current position from transform or use element position
- const computedStyle = window.getComputedStyle(moveMe);
- const transform = computedStyle.transform;
-
- if (transform && transform !== 'none') {
- const matrix = new DOMMatrix(transform);
- currentX = matrix.m41;
- currentY = matrix.m42;
- } else {
- currentX = initialRect.left;
- currentY = initialRect.top;
- }
-
- targetX = currentX;
- targetY = currentY;
-
- document.body.style.userSelect = 'none';
- iMoveThings.classList.add('dragging');
-
- // Start animation loop
- if (animationFrame) {
- cancelAnimationFrame(animationFrame);
- }
- updatePosition();
- };
-
- const endDrag = () => {
- isDragging = false;
- if (animationFrame) {
- cancelAnimationFrame(animationFrame);
- animationFrame = null;
- }
- document.body.style.userSelect = '';
- iMoveThings.classList.remove('dragging');
- };
-
- // Mouse down - start dragging
- iMoveThings.addEventListener('mousedown', function(event) {
- event.preventDefault();
- startDrag(event.clientX, event.clientY);
- });
-
- // Touch start - start dragging
- iMoveThings.addEventListener('touchstart', function(event) {
- const touch = event?.touches?.[0];
- if (!touch) {return;}
- startDrag(touch.clientX, touch.clientY);
- event.preventDefault();
- }, { passive: false });
-
- // Mouse move - update target position
- document.addEventListener('mousemove', function(event) {
- if (isDragging && initialRect) {
- targetX = event.clientX - offsetX;
- targetY = event.clientY - offsetY;
- }
- }, { passive: true });
-
- // Touch move - update target position
- document.addEventListener('touchmove', function(event) {
- if (isDragging && initialRect) {
- const touch = event?.touches?.[0];
- if (!touch) {return;}
- targetX = touch.clientX - offsetX;
- targetY = touch.clientY - offsetY;
- event.preventDefault();
- }
- }, { passive: false });
-
- // End drag events
- document.addEventListener('mouseup', endDrag);
- document.addEventListener('touchend', endDrag);
- document.addEventListener('touchcancel', endDrag);
- }
-
- /** Handles status display.
- * This will output plain text into the output Status box.
- * Additionally, this will output an info message to the console.
- * @param {string} text - The status text to display.
- * @since 0.58.4
- */
- handleDisplayStatus(text) {
- const consoleInfo = console.info; // Creates a copy of the console.info function
- consoleInfo(`${this.name}: ${text}`); // Outputs something like "ScriptName: text" as an info message to the console
- this.updateInnerHTML(this.outputStatusId, 'Status: ' + text, true); // Update output Status box
- }
-
- /** Handles error display.
- * This will output plain text into the output Status box.
- * Additionally, this will output an error to the console.
- * @param {string} text - The error text to display.
- * @since 0.41.6
- */
- handleDisplayError(text) {
- const consoleError = console.error; // Creates a copy of the console.error function
- consoleError(`${this.name}: ${text}`); // Outputs something like "ScriptName: text" as an error message to the console
- this.updateInnerHTML(this.outputStatusId, 'Error: ' + text, true); // Update output Status box
- }
-}
+ Press the arrows to reveal the option you want.
+
+
+ I want to download Blue Marble.(Click to Expand)
+
+ Click here to view the installation instructions.
+
+
+
+ I want to ask questions about Blue Marble.(Click to Expand)
+
+ Click here for the Discord server invite to the Blue Marble support server.
+
+ Click here for the GitHub help & question page for Blue Marble.
+
+
+
+ I want to report a bug.(Click to Expand)
+
+ Click here to report a bug, then choose the "Bug Report" option.
+
+
+
+ I want to suggest a feature.(Click to Expand)
+
+ Click here to suggest a feature, then choose the Feature Request" option.
+
+
+
+ I want to contribute.(Click to Expand)
+
+ Click here to read the contributing guidelines.
+
+
+
Overview
- Welcome to Blue Marble! Blue Marble is a userscript for the website wplace.live. If you like this userscript, please ⭐ the repository! If you wish to contribute to Blue Marble, check out the CONTRIBUTING.md file in docs/.
+ Welcome to Blue Marble! Blue Marble is a userscript for the website wplace.live. The purpose of Blue Marble is to allow you to take an image, and layer it onto the canvas! That way, you can easily trace the image of your art, without having to look back and forth between multiple tabs/monitors. In addition, Blue Marble supports some neat extra features such as:
+
+
Displaying the number of pixels you need to level up
+
Displaying a simple coordinate system (tile coordinats & pixel coordinates)
+
Allowing you to move the color palette to the top of the screen when placing pixels
+
Allowing you to use the eyedropper on the template image, provided the colors are correct
+
...and more!
+
+ If you like this userscript, please ⭐ the repository! If you wish to contribute to Blue Marble, check out the CONTRIBUTING.md file in docs/.
+
+
Installation Instructions
@@ -65,7 +118,7 @@
Installation instructions for Blue Marble are below. Click the arrows to expand the instructions you want to see. Blue text is a link.
- Install Chrome (Computer)(Click to Expand)
+ Install Chrome(Click to Expand)
@@ -80,25 +133,15 @@
Enable "Allow user scripts."
-
Download the BlueMarble.user.js file in the "assets" of the latest release.
-
Open the TamperMonkey Dashboard.
+
One-click install: Click this link to Install Blue Marble directly: Install Blue Marble
-
-
Drag the BlueMarble.user.js file inside the dashboard of TamperMonkey.
-
-
-
Click the "Install" button to install Blue Marble.
-
-
-
Enable Blue Marble inside the TamperMonkey dashboard.
-
-
+ TamperMonkey will automatically detect the userscript and prompt you to Install it.
import { uint8ToBase64 } from "./utils";
-
-/** An instance of a template.
- * Handles all mathematics, manipulation, and analysis regarding a single template.
- * @since 0.65.2
- */
-export default class Template {
-
- /** The constructor for the {@link Template} class with enhanced pixel tracking.
- * @param {Object} [params={}] - Object containing all optional parameters
- * @param {string} [params.displayName='My template'] - The display name of the template
- * @param {number} [params.sortID=0] - The sort number of the template for rendering priority
- * @param {string} [params.authorID=''] - The user ID of the person who exported the template (prevents sort ID collisions)
- * @param {string} [params.url=''] - The URL to the source image
- * @param {File} [params.file=null] - The template file (pre-processed File or processed bitmap)
- * @param {Array<number>} [params.coords=null] - The coordinates of the top left corner as (tileX, tileY, pixelX, pixelY)
- * @param {Object} [params.chunked=null] - The affected chunks of the template, and their template for each chunk
- * @param {number} [params.tileSize=1000] - The size of a tile in pixels (assumes square tiles)
- * @param {number} [params.pixelCount=0] - Total number of pixels in the template (calculated automatically during processing)
- * @since 0.65.2
- */
- constructor({
- displayName = 'My template',
- sortID = 0,
- authorID = '',
- url = '',
- file = null,
- coords = null,
- chunked = null,
- tileSize = 1000,
- } = {}) {
- this.displayName = displayName;
- this.sortID = sortID;
- this.authorID = authorID;
- this.url = url;
- this.file = file;
- this.coords = coords;
- this.chunked = chunked;
- this.tileSize = tileSize;
- this.pixelCount = 0; // Total pixel count in template
- }
-
- /** Creates chunks of the template for each tile.
- *
- * @returns {Object} Collection of template bitmaps & buffers organized by tile coordinates
- * @since 0.65.4
- */
- async createTemplateTiles() {
- console.log('Template coordinates:', this.coords);
-
- const shreadSize = 3; // Scale image factor for pixel art enhancement (must be odd)
- const bitmap = await createImageBitmap(this.file); // Create efficient bitmap from uploaded file
- const imageWidth = bitmap.width;
- const imageHeight = bitmap.height;
-
- // Calculate total pixel count using standard width × height formula
- // TODO: Use non-transparent pixels instead of basic width times height
- const totalPixels = imageWidth * imageHeight;
- console.log(`Template pixel analysis - Dimensions: ${imageWidth}×${imageHeight} = ${totalPixels.toLocaleString()} pixels`);
-
- // Store pixel count in instance property for access by template manager and UI components
- this.pixelCount = totalPixels;
-
- const templateTiles = {}; // Holds the template tiles
- const templateTilesBuffers = {}; // Holds the buffers of the template tiles
-
- const canvas = new OffscreenCanvas(this.tileSize, this.tileSize);
- const context = canvas.getContext('2d', { willReadFrequently: true });
-
- // For every tile...
- for (let pixelY = this.coords[3]; pixelY < imageHeight + this.coords[3]; ) {
-
- // Draws the partial tile first, if any
- // This calculates the size based on which is smaller:
- // A. The top left corner of the current tile to the bottom right corner of the current tile
- // B. The top left corner of the current tile to the bottom right corner of the image
- const drawSizeY = Math.min(this.tileSize - (pixelY % this.tileSize), imageHeight - (pixelY - this.coords[3]));
-
- console.log(`Math.min(${this.tileSize} - (${pixelY} % ${this.tileSize}), ${imageHeight} - (${pixelY - this.coords[3]}))`);
-
- for (let pixelX = this.coords[2]; pixelX < imageWidth + this.coords[2];) {
-
- console.log(`Pixel X: ${pixelX}\nPixel Y: ${pixelY}`);
-
- // Draws the partial tile first, if any
- // This calculates the size based on which is smaller:
- // A. The top left corner of the current tile to the bottom right corner of the current tile
- // B. The top left corner of the current tile to the bottom right corner of the image
- const drawSizeX = Math.min(this.tileSize - (pixelX % this.tileSize), imageWidth - (pixelX - this.coords[2]));
-
- console.log(`Math.min(${this.tileSize} - (${pixelX} % ${this.tileSize}), ${imageWidth} - (${pixelX - this.coords[2]}))`);
-
- console.log(`Draw Size X: ${drawSizeX}\nDraw Size Y: ${drawSizeY}`);
-
- // Change the canvas size and wipe the canvas
- const canvasWidth = drawSizeX * shreadSize;// + (pixelX % this.tileSize) * shreadSize;
- const canvasHeight = drawSizeY * shreadSize;// + (pixelY % this.tileSize) * shreadSize;
- canvas.width = canvasWidth;
- canvas.height = canvasHeight;
-
- console.log(`Draw X: ${drawSizeX}\nDraw Y: ${drawSizeY}\nCanvas Width: ${canvasWidth}\nCanvas Height: ${canvasHeight}`);
-
- context.imageSmoothingEnabled = false; // Nearest neighbor
-
- console.log(`Getting X ${pixelX}-${pixelX + drawSizeX}\nGetting Y ${pixelY}-${pixelY + drawSizeY}`);
-
- // Draws the template segment on this tile segment
- context.clearRect(0, 0, canvasWidth, canvasHeight); // Clear any previous drawing (only runs when canvas size does not change)
- context.drawImage(
- bitmap, // Bitmap image to draw
- pixelX - this.coords[2], // Coordinate X to draw from
- pixelY - this.coords[3], // Coordinate Y to draw from
- drawSizeX, // X width to draw from
- drawSizeY, // Y height to draw from
- 0, // Coordinate X to draw at
- 0, // Coordinate Y to draw at
- drawSizeX * shreadSize, // X width to draw at
- drawSizeY * shreadSize // Y height to draw at
- ); // Coordinates and size of draw area of source image, then canvas
-
- // const final = await canvas.convertToBlob({ type: 'image/png' });
- // const url = URL.createObjectURL(final); // Creates a blob URL
- // window.open(url, '_blank'); // Opens a new tab with blob
- // setTimeout(() => URL.revokeObjectURL(url), 60000); // Destroys the blob 1 minute later
-
- const imageData = context.getImageData(0, 0, canvasWidth, canvasHeight); // Data of the image on the canvas
-
- for (let y = 0; y < canvasHeight; y++) {
- for (let x = 0; x < canvasWidth; x++) {
- // For every pixel...
-
- // ... Make it transparent unless it is the "center"
- if (x % shreadSize !== 1 || y % shreadSize !== 1) {
- const pixelIndex = (y * canvasWidth + x) * 4; // Find the pixel index in an array where every 4 indexes are 1 pixel
- imageData.data[pixelIndex + 3] = 0; // Make the pixel transparent on the alpha channel
-
- // if (!!imageData.data[pixelIndex + 3]) {
- // imageData.data[pixelIndex + 3] = 50; // Alpha
- // imageData.data[pixelIndex] = 30; // Red
- // imageData.data[pixelIndex + 1] = 30; // Green
- // imageData.data[pixelIndex + 2] = 30; // Blue
- // }
- }
- }
- }
-
- console.log(`Shreaded pixels for ${pixelX}, ${pixelY}`, imageData);
-
- context.putImageData(imageData, 0, 0);
-
- // Creates the "0000,0000,000,000" key name
- const templateTileName = `${(this.coords[0] + Math.floor(pixelX / 1000))
- .toString()
- .padStart(4, '0')},${(this.coords[1] + Math.floor(pixelY / 1000))
- .toString()
- .padStart(4, '0')},${(pixelX % 1000)
- .toString()
- .padStart(3, '0')},${(pixelY % 1000).toString().padStart(3, '0')}`;
-
- templateTiles[templateTileName] = await createImageBitmap(canvas); // Creates the bitmap
-
- const canvasBlob = await canvas.convertToBlob();
- const canvasBuffer = await canvasBlob.arrayBuffer();
- const canvasBufferBytes = Array.from(new Uint8Array(canvasBuffer));
- templateTilesBuffers[templateTileName] = uint8ToBase64(canvasBufferBytes); // Stores the buffer
-
- console.log(templateTiles);
-
- pixelX += drawSizeX;
- }
-
- pixelY += drawSizeY;
- }
-
- console.log('Template Tiles: ', templateTiles);
- console.log('Template Tiles Buffers: ', templateTilesBuffers);
- return { templateTiles, templateTilesBuffers };
- }
-}
-
/** ApiManager class for handling API requests, responses, and interactions.
- * Note: Fetch spying is done in main.js, not here.
- * @since 0.11.1
- */
-
-import TemplateManager from "./templateManager.js";
-import { escapeHTML, numberToEncoded, serverTPtoDisplayTP } from "./utils.js";
-
-export default class ApiManager {
-
- /** Constructor for ApiManager class
- * @param {TemplateManager} templateManager
- * @since 0.11.34
- */
- constructor(templateManager) {
- this.templateManager = templateManager;
- this.disableAll = false; // Should the entire userscript be disabled?
- this.coordsTilePixel = []; // Contains the last detected tile/pixel coordinate pair requested
- this.templateCoordsTilePixel = []; // Contains the last "enabled" template coords
- }
-
- /** Determines if the spontaneously recieved response is something we want.
- * Otherwise, we can ignore it.
- * Note: Due to aggressive compression, make your calls like `data['jsonData']['name']` instead of `data.jsonData.name`
- *
- * @param {Overlay} overlay - The Overlay class instance
- * @since 0.11.1
- */
- spontaneousResponseListener(overlay) {
-
- // Triggers whenever a message is sent
- window.addEventListener('message', async (event) => {
-
- const data = event.data; // The data of the message
- const dataJSON = data['jsonData']; // The JSON response, if any
-
- // Kills itself if the message was not intended for Blue Marble
- if (!(data && data['source'] === 'blue-marble')) {return;}
-
- // Kills itself if the message has no endpoint (intended for Blue Marble, but not this function)
- if (!data['endpoint']) {return;}
-
- // Trims endpoint to the second to last non-number, non-null directoy.
- // E.g. "wplace.live/api/pixel/0/0?payload" -> "pixel"
- // E.g. "wplace.live/api/files/s0/tiles/0/0/0.png" -> "tiles"
- const endpointText = data['endpoint']?.split('?')[0].split('/').filter(s => s && isNaN(Number(s))).filter(s => s && !s.includes('.')).pop();
-
- console.log(`%cBlue Marble%c: Recieved message about "%s"`, 'color: cornflowerblue;', '', endpointText);
-
- // Each case is something that Blue Marble can use from the fetch.
- // For instance, if the fetch was for "me", we can update the overlay stats
- switch (endpointText) {
-
- case 'me': // Request to retrieve user data
-
- // If the game can not retrieve the userdata...
- if (dataJSON['status'] && dataJSON['status']?.toString()[0] != '2') {
- // The server is probably down (NOT a 2xx status)
-
- overlay.handleDisplayError(`You are not logged in!\nCould not fetch userdata.`);
- return; // Kills itself before attempting to display null userdata
- }
-
- const nextLevelPixels = Math.ceil(Math.pow(Math.floor(dataJSON['level']) * Math.pow(30, 0.65), (1/0.65)) - dataJSON['pixelsPainted']); // Calculates pixels to the next level
-
- console.log(dataJSON['id']);
- if (!!dataJSON['id'] || dataJSON['id'] === 0) {
- console.log(numberToEncoded(
- dataJSON['id'],
- '!#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'
- ));
- }
- this.templateManager.userID = dataJSON['id'];
-
- overlay.updateInnerHTML('bm-user-name', `Username: <b>${escapeHTML(dataJSON['name'])}</b>`); // Updates the text content of the username field
- overlay.updateInnerHTML('bm-user-droplets', `Droplets: <b>${new Intl.NumberFormat().format(dataJSON['droplets'])}</b>`); // Updates the text content of the droplets field
- overlay.updateInnerHTML('bm-user-nextlevel', `Next level in <b>${new Intl.NumberFormat().format(nextLevelPixels)}</b> pixel${nextLevelPixels == 1 ? '' : 's'}`); // Updates the text content of the next level field
- break;
-
- case 'pixel': // Request to retrieve pixel data
- const coordsTile = data['endpoint'].split('?')[0].split('/').filter(s => s && !isNaN(Number(s))); // Retrieves the tile coords as [x, y]
- const payloadExtractor = new URLSearchParams(data['endpoint'].split('?')[1]); // Declares a new payload deconstructor and passes in the fetch request payload
- const coordsPixel = [payloadExtractor.get('x'), payloadExtractor.get('y')]; // Retrieves the deconstructed pixel coords from the payload
-
- // Don't save the coords if there are previous coords that could be used
- if (this.coordsTilePixel.length && (!coordsTile.length || !coordsPixel.length)) {
- overlay.handleDisplayError(`Coordinates are malformed!\nDid you try clicking the canvas first?`);
- return; // Kills itself
- }
-
- this.coordsTilePixel = [...coordsTile, ...coordsPixel]; // Combines the two arrays such that [x, y, x, y]
- const displayTP = serverTPtoDisplayTP(coordsTile, coordsPixel);
-
- const spanElements = document.querySelectorAll('span'); // Retrieves all span elements
-
- // For every span element, find the one we want (pixel numbers when canvas clicked)
- for (const element of spanElements) {
- if (element.textContent.trim().includes(`${displayTP[0]}, ${displayTP[1]}`)) {
-
- let displayCoords = document.querySelector('#bm-display-coords'); // Find the additional pixel coords span
-
- const text = `(Tl X: ${coordsTile[0]}, Tl Y: ${coordsTile[1]}, Px X: ${coordsPixel[0]}, Px Y: ${coordsPixel[1]})`;
-
- // If we could not find the addition coord span, we make it then update the textContent with the new coords
- if (!displayCoords) {
- displayCoords = document.createElement('span');
- displayCoords.id = 'bm-display-coords';
- displayCoords.textContent = text;
- displayCoords.style = 'margin-left: calc(var(--spacing)*3); font-size: small;';
- element.parentNode.parentNode.parentNode.insertAdjacentElement('afterend', displayCoords);
- } else {
- displayCoords.textContent = text;
- }
- }
- }
- break;
-
- case 'tiles':
-
- // Runs only if the tile has the template
- let tileCoordsTile = data['endpoint'].split('/');
- tileCoordsTile = [parseInt(tileCoordsTile[tileCoordsTile.length - 2]), parseInt(tileCoordsTile[tileCoordsTile.length - 1].replace('.png', ''))];
-
- const blobUUID = data['blobID'];
- const blobData = data['blobData'];
-
- const templateBlob = await this.templateManager.drawTemplateOnTile(blobData, tileCoordsTile);
-
- window.postMessage({
- source: 'blue-marble',
- blobID: blobUUID,
- blobData: templateBlob,
- blink: data['blink']
- });
- break;
-
- case 'robots': // Request to retrieve what script types are allowed
- this.disableAll = dataJSON['userscript']?.toString().toLowerCase() == 'false'; // Disables Blue Marble if site owner wants userscripts disabled
- break;
- }
- });
- }
-}