mirror of
https://github.com/SwingTheVine/Wplace-BlueMarble.git
synced 2026-03-11 21:26:55 +00:00
Merge bb5b54b061 into 1045f1a7f6
This commit is contained in:
commit
76ee827e9a
24 changed files with 2459 additions and 1109 deletions
325
dist/BlueMarble-For-GreasyFork.user.css
vendored
325
dist/BlueMarble-For-GreasyFork.user.css
vendored
|
|
@ -1,131 +1,3 @@
|
|||
/* src/WindowFilter.css */
|
||||
#bm-window-filter p svg {
|
||||
display: inline;
|
||||
height: 1em;
|
||||
fill: white;
|
||||
}
|
||||
#bm-filter-flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 1em 3ch;
|
||||
}
|
||||
.bm-filter-color {
|
||||
width: fit-content;
|
||||
max-width: 35ch;
|
||||
background-color: rgba(21, 48, 99, 0.9);
|
||||
border-radius: 1em;
|
||||
padding: 0.5em;
|
||||
gap: 1ch;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.bm-filter-color:hover,
|
||||
.bm-filter-color:focus-within {
|
||||
background-color: rgba(17, 40, 85, 0.9);
|
||||
}
|
||||
.bm-filter-container-rgb {
|
||||
display: block;
|
||||
border: thick double darkslategray;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
padding: 1ch;
|
||||
}
|
||||
.bm-filter-color[data-id="-2"] .bm-filter-container-rgb {
|
||||
background:
|
||||
conic-gradient(
|
||||
#aa0000 0%,
|
||||
#aaaa00 16.6%,
|
||||
#00aa00 33.3%,
|
||||
#00aaaa 50%,
|
||||
#0000aa 66.6%,
|
||||
#aa00aa 83.3%,
|
||||
#aa0000 100%);
|
||||
}
|
||||
.bm-filter-color[data-id="-1"] .bm-filter-container-rgb {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 8 8" width="1em" height="1em"><path d="M0,0V8H16V16H8V0" fill="rgba(0,0,0,0.5)"/></svg>') repeat;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.bm-filter-color[data-id="-1"] .bm-filter-container-rgb svg {
|
||||
fill: white !important;
|
||||
}
|
||||
.bm-filter-color[data-id="0"] .bm-filter-container-rgb {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
#bm-window-filter .bm-filter-container-rgb button {
|
||||
padding: 0.75em 0.5ch;
|
||||
}
|
||||
.bm-filter-container-rgb svg {
|
||||
width: 4ch;
|
||||
}
|
||||
.bm-filter-color > .bm-flex-between {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
}
|
||||
.bm-filter-color small {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
#bm-window-filter .bm-filter-color.bm-color-hide {
|
||||
display: none;
|
||||
}
|
||||
.bm-windowed #bm-filter-flex {
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
}
|
||||
.bm-windowed .bm-filter-color {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.bm-windowed .bm-filter-container-rgb {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 0.5ch;
|
||||
align-items: center;
|
||||
padding: 0.1em 0.5ch;
|
||||
border: none;
|
||||
border-radius: 1em;
|
||||
}
|
||||
#bm-window-filter.bm-windowed .bm-filter-container-rgb button {
|
||||
padding: 0.5em 0.25ch;
|
||||
}
|
||||
.bm-windowed .bm-filter-container-rgb svg {
|
||||
width: 3ch;
|
||||
}
|
||||
.bm-windowed .bm-filter-color h2 {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
/* src/WindowWizard.css */
|
||||
#bm-wizard-tlist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
#bm-wizard-tlist > .bm-container {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
background-color: rgba(21, 48, 99, 0.9);
|
||||
border-radius: 1em;
|
||||
padding: 0.5em;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
#bm-wizard-tlist > .bm-container:hover,
|
||||
#bm-wizard-tlist > .bm-container:focus-within {
|
||||
background-color: rgba(17, 40, 85, 0.9);
|
||||
}
|
||||
#bm-wizard-tlist .bm-wizard-template-container-image {
|
||||
height: 100%;
|
||||
font-size: xxx-large;
|
||||
}
|
||||
#bm-wizard-tlist .bm-wizard-template-container-flavor {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* src/confettiManager.css */
|
||||
div:has(> confetti-piece) {
|
||||
position: absolute;
|
||||
|
|
@ -222,7 +94,8 @@ confetti-piece {
|
|||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.bm-dragbar h1 {
|
||||
.bm-dragbar h1,
|
||||
.bm-dragbar-text {
|
||||
font-size: 1.2em;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
|
|
@ -406,11 +279,201 @@ input[type=file] {
|
|||
font-size: 1.6em;
|
||||
font-family: monospace;
|
||||
}
|
||||
.bm-container:not(#bm-window-main .bm-container) {
|
||||
margin: 0.25em 0;
|
||||
.bm-windowed .bm-container:not(#bm-window-main .bm-container) {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
.bm-windowed h1:not(#bm-window-main h1) {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* src/WindowFilter.css */
|
||||
#bm-window-filter p svg {
|
||||
display: inline;
|
||||
height: 1em;
|
||||
fill: white;
|
||||
}
|
||||
#bm-filter-flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 1em 3ch;
|
||||
}
|
||||
#bm-window-filter .bm-filter-color {
|
||||
width: fit-content;
|
||||
max-width: 35ch;
|
||||
background-color: rgba(21, 48, 99, 0.9);
|
||||
border-radius: 1em;
|
||||
padding: 0.5em;
|
||||
gap: 1ch;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
#bm-window-filter .bm-filter-color:hover,
|
||||
#bm-window-filter.bm-filter-color:focus-within {
|
||||
background-color: rgba(17, 40, 85, 0.9);
|
||||
}
|
||||
#bm-window-filter .bm-filter-container-rgb {
|
||||
display: block;
|
||||
border: thick double darkslategray;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
padding: 1ch;
|
||||
}
|
||||
#bm-window-filter .bm-filter-color[data-id="-2"] .bm-filter-container-rgb {
|
||||
background:
|
||||
conic-gradient(
|
||||
#aa0000 0%,
|
||||
#aaaa00 16.6%,
|
||||
#00aa00 33.3%,
|
||||
#00aaaa 50%,
|
||||
#0000aa 66.6%,
|
||||
#aa00aa 83.3%,
|
||||
#aa0000 100%);
|
||||
}
|
||||
#bm-window-filter .bm-filter-color[data-id="-1"] .bm-filter-container-rgb {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 8 8" width="1em" height="1em"><path d="M0,0V8H16V16H8V0" fill="rgba(0,0,0,0.5)"/></svg>') repeat;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
#bm-window-filter .bm-filter-color[data-id="-1"] .bm-filter-container-rgb svg {
|
||||
fill: white !important;
|
||||
}
|
||||
#bm-window-filter .bm-filter-color[data-id="0"] .bm-filter-container-rgb {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
#bm-window-filter .bm-filter-container-rgb button {
|
||||
padding: 0.75em 0.5ch;
|
||||
}
|
||||
#bm-window-filter .bm-filter-container-rgb svg {
|
||||
width: 4ch;
|
||||
}
|
||||
#bm-window-filter .bm-filter-color > .bm-flex-between {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
}
|
||||
#bm-window-filter .bm-filter-color small {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
#bm-window-filter .bm-filter-color.bm-color-hide {
|
||||
display: none;
|
||||
}
|
||||
#bm-window-filter.bm-windowed #bm-filter-flex {
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
}
|
||||
#bm-window-filter.bm-windowed .bm-filter-color {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#bm-window-filter.bm-windowed .bm-filter-container-rgb {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 0.5ch;
|
||||
align-items: center;
|
||||
padding: 0.1em 0.5ch;
|
||||
border: none;
|
||||
border-radius: 1em;
|
||||
}
|
||||
#bm-window-filter.bm-windowed .bm-filter-container-rgb button {
|
||||
padding: 0.5em 0.25ch;
|
||||
}
|
||||
#bm-window-filter.bm-windowed .bm-filter-container-rgb svg {
|
||||
width: 3ch;
|
||||
}
|
||||
#bm-window-filter.bm-windowed .bm-filter-color h2 {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
#bm-window-filter #bm-filter-windowed-color-totals {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* src/WindowSettings.css */
|
||||
#bm-window-settings div:has(> .bm-highlight-preset-container) {
|
||||
width: fit-content;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
#bm-window-settings .bm-highlight-preset-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 13%;
|
||||
}
|
||||
#bm-window-settings .bm-highlight-preset-container span {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
#bm-window-settings .bm-highlight-preset-container button {
|
||||
width: fit-content;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
#bm-window-settings .bm-highlight-preset-container svg {
|
||||
stroke: #333;
|
||||
stroke-width: 0.02px;
|
||||
width: 100%;
|
||||
min-width: 1.5ch;
|
||||
max-width: 14.5ch;
|
||||
}
|
||||
#bm-window-settings .bm-highlight-preset-container button:hover svg,
|
||||
#bm-window-settings .bm-highlight-preset-container button:focus svg {
|
||||
opacity: 0.9;
|
||||
}
|
||||
#bm-window-settings .bm-highlight-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
width: 25%;
|
||||
min-width: 3ch;
|
||||
max-width: 15ch;
|
||||
}
|
||||
#bm-window-settings .bm-highlight-grid > button {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-color: white;
|
||||
border: #333 1px solid;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#bm-window-settings .bm-highlight-grid > button[data-status=Incorrect] {
|
||||
background-color: brown;
|
||||
}
|
||||
#bm-window-settings .bm-highlight-grid > button[data-status=Template] {
|
||||
background-color: darkslategray;
|
||||
}
|
||||
#bm-window-settings .bm-highlight-grid > button:hover,
|
||||
#bm-window-settings .bm-highlight-grid > button:focus {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* src/WindowWizard.css */
|
||||
#bm-wizard-tlist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
#bm-wizard-tlist > .bm-container {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
background-color: rgba(21, 48, 99, 0.9);
|
||||
border-radius: 1em;
|
||||
padding: 0.5em;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
#bm-wizard-tlist > .bm-container:hover,
|
||||
#bm-wizard-tlist > .bm-container:focus-within {
|
||||
background-color: rgba(17, 40, 85, 0.9);
|
||||
}
|
||||
#bm-wizard-tlist .bm-wizard-template-container-image {
|
||||
height: 100%;
|
||||
font-size: xxx-large;
|
||||
}
|
||||
#bm-wizard-tlist .bm-wizard-template-container-flavor {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* src/main.css */
|
||||
|
|
|
|||
2277
dist/BlueMarble-For-GreasyFork.user.js
vendored
2277
dist/BlueMarble-For-GreasyFork.user.js
vendored
File diff suppressed because it is too large
Load diff
4
dist/BlueMarble-Standalone.user.js
vendored
4
dist/BlueMarble-Standalone.user.js
vendored
File diff suppressed because one or more lines are too long
2
dist/BlueMarble.user.css
vendored
2
dist/BlueMarble.user.css
vendored
File diff suppressed because one or more lines are too long
4
dist/BlueMarble.user.js
vendored
4
dist/BlueMarble.user.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -39,6 +39,7 @@ Special Thanks:
|
|||
[Donators](https://ko-fi.com/swingthevine):
|
||||
* Espresso
|
||||
* BEST FAN
|
||||
* FuchsDresden
|
||||
* Jack
|
||||
* raiken_au
|
||||
* Jacob
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
<a href="https://discord.gg/tpeBPy46hf" target="_blank" rel="noopener noreferrer"><img alt="Contact Me" src="https://img.shields.io/badge/Contact_Me-gray?style=flat&logo=Discord&logoColor=white&logoSize=auto&labelColor=cornflowerblue"></a>
|
||||
<a href="https://bluemarble.lol/" target="_blank" rel="noopener noreferrer"><img alt="Blue Marble Website" src="https://img.shields.io/badge/Blue_Marble_Website-crqch-blue?style=flat&logo=globe&logoColor=white"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="WakaTime" src="https://img.shields.io/badge/Coding_Time-212hrs_17mins-blue?style=flat&logo=wakatime&logoColor=black&logoSize=auto&labelColor=white"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-1115-black?style=flat"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-1217-black?style=flat"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Lines of Code" src="https://img.shields.io/badge/Lines_Of_Code-6620-blue?style=flat"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Comments" src="https://img.shields.io/badge/Lines_Of_Comments-5414-blue?style=flat"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Compression" src="https://img.shields.io/badge/Compression-71.85%25-blue"></a>
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "wplace-bluemarble",
|
||||
"version": "0.90.76",
|
||||
"version": "0.91.102",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wplace-bluemarble",
|
||||
"version": "0.90.76",
|
||||
"version": "0.91.102",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"jsdoc": "^4.0.5",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "wplace-bluemarble",
|
||||
"version": "0.91.0",
|
||||
"version": "0.91.102",
|
||||
"type": "module",
|
||||
"homepage": "https://bluemarble.lol/",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// @name Blue Marble
|
||||
// @name:en Blue Marble
|
||||
// @namespace https://github.com/SwingTheVine/
|
||||
// @version 0.91.0
|
||||
// @version 0.91.102
|
||||
// @description A userscript to enhance the user experience on Wplace.live. This includes, but is not limited to: uploading images to display locally on a canvas, adding a button to move the Wplace color palette menu, and other QoL features.
|
||||
// @description:en A userscript to enhance the user experience on Wplace.live. This includes, but is not limited to: uploading images to display locally on a canvas, adding a button to move the Wplace color palette menu, and other QoL features.
|
||||
// @author SwingTheVine
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ export default class Overlay {
|
|||
|
||||
/** The API manager instance. Later populated when setApiManager is called @type {ApiManager} */
|
||||
this.apiManager = null;
|
||||
|
||||
/** The Settings Manager instance. Later populated when setSettingsManager is called @type {SettingsManager} */
|
||||
this.settingsManager = null;
|
||||
|
||||
this.outputStatusId = 'bm-output-status'; // ID for status element
|
||||
|
||||
|
|
@ -51,6 +54,12 @@ export default class Overlay {
|
|||
*/
|
||||
setApiManager(apiManager) {this.apiManager = apiManager;}
|
||||
|
||||
/** Populates the settingsManager variable with the settingsManager class.
|
||||
* @param {SettingsManager} settingsManager - The settingsManager class instance
|
||||
* @since 0.91.11
|
||||
*/
|
||||
setSettingsManager(settingsManager) {this.settingsManager = settingsManager;}
|
||||
|
||||
/** Creates an element.
|
||||
* For **internal use** of the {@link Overlay} class.
|
||||
* @param {string} tag - The tag name as a string.
|
||||
|
|
@ -109,10 +118,7 @@ export default class Overlay {
|
|||
).join('')
|
||||
] = value;
|
||||
} else if (property.startsWith('aria')) {
|
||||
const camelCase = property.slice(5).split('-').map(
|
||||
(part, i) => (i == 0) ? part : part[0].toUpperCase() + part.slice(1)
|
||||
).join('');
|
||||
element['aria' + camelCase[0].toUpperCase() + camelCase.slice(1)] = value;
|
||||
element.setAttribute(property, value); // We can't do the solution for 'data', as 'aria-labelledby' would fail to apply
|
||||
} else {
|
||||
element[property] = value;
|
||||
}
|
||||
|
|
@ -511,8 +517,24 @@ export default class Overlay {
|
|||
|
||||
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
|
||||
// Stores the label content from the additional property
|
||||
const labelContent = {};
|
||||
|
||||
// If the label content was passed in as 'textContent'...
|
||||
if (!!additionalProperties['textContent']) {
|
||||
|
||||
// Store the information, then delete it from additionalProperties
|
||||
labelContent['textContent'] = additionalProperties['textContent'];
|
||||
delete additionalProperties['textContent']; // Deletes 'textContent' DOM property before adding the properties to the checkbox
|
||||
} else if (!!additionalProperties['innerHTML']) {
|
||||
// Else if the label content was passed in as 'innerHTML'...
|
||||
|
||||
// Store the information, then delete it from additionalProperties
|
||||
labelContent['innerHTML'] = additionalProperties['innerHTML'];
|
||||
delete additionalProperties['textContent'];
|
||||
}
|
||||
|
||||
const label = this.#createElement('label', labelContent); // Creates the label element
|
||||
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
|
||||
|
|
|
|||
149
src/Template.js
149
src/Template.js
|
|
@ -1,4 +1,4 @@
|
|||
import { uint8ToBase64 } from "./utils";
|
||||
import { sleep, uint8ToBase64, viewCanvasInNewTab } from "./utils";
|
||||
|
||||
/** An instance of a template.
|
||||
* Handles all mathematics, manipulation, and analysis regarding a single template.
|
||||
|
|
@ -43,17 +43,26 @@ export default class Template {
|
|||
this.tileSize = tileSize;
|
||||
/** Total pixel count in template @type {{total: number, colors: Map<number, number>, correct?: { [key: string]: Map<number, number> }}} */
|
||||
this.pixelCount = { total: 0, colors: new Map() };
|
||||
|
||||
this.shouldSkipTransTiles = true; // Should transparent template tiles be skipped during template creation?
|
||||
this.shouldAggSkipTransTiles = false; // Should transparent template tiles be aggressively skipped during tempalte creation?
|
||||
}
|
||||
|
||||
/** Creates chunks of the template for each tile.
|
||||
* @param {Number} tileSize - Size of the tile as determined by templateManager
|
||||
* @param {Object} paletteBM - An collection of Uint32Arrays containing the palette BM uses
|
||||
* @param {boolean} shouldSkipTransTiles - Should transparent tiles be skipped over when creating the template?
|
||||
* @param {boolean} shouldAggSkipTransTiles - Should transparent tiles be aggressively skipped over when creating the template?
|
||||
* @returns {Object} Collection of template bitmaps & buffers organized by tile coordinates
|
||||
* @since 0.65.4
|
||||
*/
|
||||
async createTemplateTiles(tileSize, paletteBM) {
|
||||
async createTemplateTiles(tileSize, paletteBM, shouldSkipTransTiles, shouldAggSkipTransTiles) {
|
||||
console.log('Template coordinates:', this.coords);
|
||||
|
||||
// Updates the class instance variable with the new information
|
||||
this.shouldSkipTransTiles = shouldSkipTransTiles;
|
||||
this.shouldAggSkipTransTiles = shouldAggSkipTransTiles;
|
||||
|
||||
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;
|
||||
|
|
@ -64,8 +73,16 @@ export default class Template {
|
|||
const templateTiles = {}; // Holds the template tiles
|
||||
const templateTilesBuffers = {}; // Holds the buffers of the template tiles
|
||||
|
||||
// The main canvas used during template creation
|
||||
const canvas = new OffscreenCanvas(this.tileSize, this.tileSize);
|
||||
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
// The canvas used to check if a specific template tile is transparent or not
|
||||
const transCanvas = new OffscreenCanvas(this.tileSize, this.tileSize);
|
||||
const transContext = transCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
// Makes it so that `.drawImage()` calls on the canvas used to calculate transparency always draw below what is already on the canvas
|
||||
transContext.globalCompositeOperation = "destination-over";
|
||||
|
||||
// Prep the canvas for drawing the entire template (so we can find total pixels)
|
||||
canvas.width = imageWidth;
|
||||
|
|
@ -101,7 +118,7 @@ export default class Template {
|
|||
contextMask.fillRect(1, 1, 1, 1);
|
||||
|
||||
// For every tile...
|
||||
for (let pixelY = this.coords[3]; pixelY < imageHeight + this.coords[3]; ) {
|
||||
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:
|
||||
|
|
@ -121,6 +138,26 @@ export default class Template {
|
|||
// 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]));
|
||||
|
||||
// If the user wants to skip any tiles where the template is transparent...
|
||||
if (shouldSkipTransTiles) {
|
||||
|
||||
// Detects if the canvas is fully transparent
|
||||
const isTemplateTileTransparent = !this.calculateCanvasTransparency({
|
||||
bitmap: bitmap,
|
||||
bitmapParams: [pixelX - this.coords[2], pixelY - this.coords[3], drawSizeX, drawSizeY], // Top left X, Top left Y, Width, Height
|
||||
transCanvas: transCanvas,
|
||||
transContext: transContext
|
||||
});
|
||||
|
||||
console.log(`Tile contains template: ${!isTemplateTileTransparent}`);
|
||||
|
||||
// If the template in this tile is transparent...
|
||||
if (isTemplateTileTransparent) {
|
||||
pixelX += drawSizeX; // If you remove this, it will get stuck forever processing the template
|
||||
continue; // ...the user does not want to save this tile, so we skip to the next tile
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Math.min(${this.tileSize} - (${pixelX} % ${this.tileSize}), ${imageWidth} - (${pixelX - this.coords[2]}))`);
|
||||
|
||||
console.log(`Draw Size X: ${drawSizeX}\nDraw Size Y: ${drawSizeY}`);
|
||||
|
|
@ -155,6 +192,8 @@ export default class Template {
|
|||
context.globalCompositeOperation = "destination-in"; // The existing canvas content is kept where both the new shape and existing canvas content overlap. Everything else is made transparent.
|
||||
// For our purposes, this means any non-transparent pixels on the mask will be kept
|
||||
|
||||
console.log(`Should Skip: ${shouldSkipTransTiles}; Should Agg Skip: ${shouldAggSkipTransTiles}`);
|
||||
|
||||
// Fills the canvas with the mask
|
||||
context.fillStyle = context.createPattern(canvasMask, "repeat");
|
||||
context.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
|
@ -197,6 +236,110 @@ export default class Template {
|
|||
return { templateTiles, templateTilesBuffers };
|
||||
}
|
||||
|
||||
/** Detects if the canvas is transparent.
|
||||
* @param {Object} param - Object that contains the parameters for the function
|
||||
* @param {ImageBitmap} param.bitmap - The bitmap template image
|
||||
* @param {Array<number, number, number, number>} param.bitmapParams - The parameters to obtain the template tile image from the bitmap
|
||||
* @param {OffscreenCanvas | HTMLCanvasElement} param.transCanvas - The canvas to draw to in order to calculate this
|
||||
* @param {OffscreenCanvasRenderingContext2D} param.transContext - The context for the transparent canvas to draw to
|
||||
* @return {boolean} Is the canvas transparent? If transparent, then `true` is returned. Otherwise, `false`.
|
||||
* @since 0.91.75
|
||||
*/
|
||||
calculateCanvasTransparency({
|
||||
bitmap: bitmap,
|
||||
bitmapParams: bitmapParams,
|
||||
transCanvas: transCanvas,
|
||||
transContext: transContext
|
||||
}) {
|
||||
|
||||
console.log(`Calculating template tile transparency...`);
|
||||
|
||||
console.log(`Should Skip: ${this.shouldSkipTransTiles}; Should Agg: ${this.shouldAggSkipTransTiles}`);
|
||||
|
||||
const timer = Date.now(); // Starts the timer
|
||||
|
||||
// Contains the directions to move the canvas when duplicating, in the unit of pixels
|
||||
const duplicationCoordinateArray = [
|
||||
[ 0, 1], // E.g. move 0 on the x axis, and 1 down on the y axis
|
||||
[ 1, 0],
|
||||
[ 0, -2], // E.g. move 0 on the x axis, and 2 up on the y axis
|
||||
[ -2, 0],
|
||||
[ 0, 4],
|
||||
[ 4, 0],
|
||||
[ 0, -8],
|
||||
[ -8, 0],
|
||||
[ 0, 16],
|
||||
[ 16, 0],
|
||||
[ 0, -32],
|
||||
[-32, 0]
|
||||
];
|
||||
|
||||
// Changes the size of the canvas so that it equals the template tile
|
||||
const transCanvasWidth = bitmapParams[2];
|
||||
const transCanvasHeight = bitmapParams[3];
|
||||
transCanvas.width = transCanvasWidth;
|
||||
transCanvas.height = transCanvasHeight;
|
||||
|
||||
transContext.clearRect(0, 0, transCanvasWidth, transCanvasHeight); // Clear any previous drawing (only runs when canvas size does not change)
|
||||
|
||||
// If the user does want to aggressively skip transparent template tiles...
|
||||
if (this.shouldAggSkipTransTiles) {
|
||||
// (This code will only run if `this.shouldSkipTransTiles` is `true`)
|
||||
|
||||
// Draw the template tile onto the canvas scaled down to 10x10
|
||||
transContext.drawImage(
|
||||
bitmap, // The bitmap image
|
||||
...bitmapParams, // Bitmap image parameters (x, y, width, height)
|
||||
0, 0, // The coordinate draw the output *at*
|
||||
10, 10 // The width and height of the output
|
||||
);
|
||||
} else {
|
||||
// Else, the user wants to skip transparent template tiles normally...
|
||||
|
||||
// Draw the template tile onto the canvas
|
||||
transContext.drawImage(
|
||||
bitmap, // The bitmap image
|
||||
...bitmapParams, // Bitmap image parameters (x, y, width, height)
|
||||
0, 0, // The coordinate draw the output *at*
|
||||
transCanvasWidth, transCanvasHeight // Stretch to canvas (the canvas should already be the same size as the template image)
|
||||
)
|
||||
|
||||
// For each canvas duplication...
|
||||
for (const [relativeX, relativeY] of duplicationCoordinateArray) {
|
||||
|
||||
// Duplicate the canvas onto itself, but shifted slightly
|
||||
transContext.drawImage(
|
||||
transCanvas, // The canvas we are drawing to *is* the source image
|
||||
0, 0, transCanvasWidth, transCanvasHeight, // The entire canvas (as a source image)
|
||||
relativeX, relativeY, transCanvasWidth, transCanvasHeight // The output coordinates and size on the same canvas
|
||||
)
|
||||
}
|
||||
|
||||
// Scale down the image to 10x10, and store it between (0, 0) and (9, 9) on the canvas
|
||||
transContext.drawImage(
|
||||
transCanvas, // The canvas we are drawing to *is* the source image
|
||||
0, 0, transCanvasWidth, transCanvasHeight, // The entire canvas (as a source image)
|
||||
0, 0, 10, 10 // The output coordinates and size on the same canvas
|
||||
);
|
||||
}
|
||||
|
||||
const shunkCanvas = transContext.getImageData(0, 0, 10, 10);
|
||||
const shunkCanvas32 = new Uint32Array(shunkCanvas.data.buffer);
|
||||
|
||||
console.log(`Calculated canvas transparency in ${(Date.now() - timer) / 1000} seconds.`);
|
||||
|
||||
// For every pixel in the `shrunkCanvas32` array...
|
||||
for (const pixel of shunkCanvas32) {
|
||||
|
||||
// If the pixel is NOT 100% transparent
|
||||
if (!!pixel) {
|
||||
return true; // Return `true` early since we confirmed a template exists in the tile
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Since we could not confirm any template exists, we assume no template eixsts in this tile
|
||||
}
|
||||
|
||||
/** Calculates top left coordinate of template.
|
||||
* It uses `Template.chunked` to update `Template.coords`
|
||||
* @since 0.88.504
|
||||
|
|
|
|||
|
|
@ -101,11 +101,12 @@ export default class WindowCredts extends Overlay {
|
|||
.addUl()
|
||||
.addLi({'textContent': 'Espresso'}).buildElement()
|
||||
.addLi({'textContent': 'BEST FAN'}).buildElement()
|
||||
.addLi({'textContent': 'FuchsDresden'}).buildElement()
|
||||
.addLi({'textContent': 'Jack'}).buildElement()
|
||||
.addLi({'textContent': 'raiken_au'}).buildElement()
|
||||
.addLi({'textContent': 'Jacob'}).buildElement()
|
||||
.addLi({'textContent': 'StupidOne'}).buildElement()
|
||||
.addLi({'textContent': '1 Anonymous Supporter'}).buildElement()
|
||||
.addLi({'textContent': '2 Anonymous Supporters'}).buildElement()
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
|
||||
/* Filter color */
|
||||
.bm-filter-color {
|
||||
#bm-window-filter .bm-filter-color {
|
||||
width: fit-content;
|
||||
max-width: 35ch;
|
||||
background-color: rgba(21, 48, 99, 0.9);
|
||||
|
|
@ -28,13 +28,13 @@
|
|||
}
|
||||
|
||||
/* Filter color on hover */
|
||||
.bm-filter-color:hover,
|
||||
.bm-filter-color:focus-within {
|
||||
#bm-window-filter .bm-filter-color:hover,
|
||||
#bm-window-filter.bm-filter-color:focus-within {
|
||||
background-color: rgba(17, 40, 85, 0.9);
|
||||
}
|
||||
|
||||
/* Filter window container for RGB color display */
|
||||
.bm-filter-container-rgb {
|
||||
#bm-window-filter .bm-filter-container-rgb {
|
||||
display: block;
|
||||
border: thick double darkslategray;
|
||||
width: fit-content;
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
}
|
||||
|
||||
/* Filter window container for RGB color display for Other color */
|
||||
.bm-filter-color[data-id="-2"] .bm-filter-container-rgb {
|
||||
#bm-window-filter .bm-filter-color[data-id="-2"] .bm-filter-container-rgb {
|
||||
background: conic-gradient(
|
||||
#aa0000 0%,
|
||||
#aaaa00 16.6%,
|
||||
|
|
@ -56,16 +56,16 @@
|
|||
}
|
||||
|
||||
/* Filter window container for RGB color display for Erased color */
|
||||
.bm-filter-color[data-id="-1"] .bm-filter-container-rgb {
|
||||
#bm-window-filter .bm-filter-color[data-id="-1"] .bm-filter-container-rgb {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 8 8" width="1em" height="1em"><path d="M0,0V8H16V16H8V0" fill="rgba(0,0,0,0.5)"/></svg>') repeat;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.bm-filter-color[data-id="-1"] .bm-filter-container-rgb svg {
|
||||
#bm-window-filter .bm-filter-color[data-id="-1"] .bm-filter-container-rgb svg {
|
||||
fill: white !important;
|
||||
}
|
||||
|
||||
/* Filter window container for RGB color display for Transparent color */
|
||||
.bm-filter-color[data-id="0"] .bm-filter-container-rgb {
|
||||
#bm-window-filter .bm-filter-color[data-id="0"] .bm-filter-container-rgb {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
|
@ -75,19 +75,19 @@
|
|||
}
|
||||
|
||||
/* Filter window hide color button SVG */
|
||||
.bm-filter-container-rgb svg {
|
||||
#bm-window-filter .bm-filter-container-rgb svg {
|
||||
width: 4ch;
|
||||
}
|
||||
|
||||
/* Filter window container for color information */
|
||||
.bm-filter-color > .bm-flex-between {
|
||||
#bm-window-filter .bm-filter-color > .bm-flex-between {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Filter window color flavor text */
|
||||
.bm-filter-color small {
|
||||
#bm-window-filter .bm-filter-color small {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
|
|
@ -99,20 +99,20 @@
|
|||
/* WINDOWED MODE */
|
||||
|
||||
/* Filter flex in windowed mode */
|
||||
.bm-windowed #bm-filter-flex {
|
||||
#bm-window-filter.bm-windowed #bm-filter-flex {
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
/* Filter color in windowed mode */
|
||||
.bm-windowed .bm-filter-color {
|
||||
#bm-window-filter.bm-windowed .bm-filter-color {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Filter window container for RGB color display in windowed mode */
|
||||
.bm-windowed .bm-filter-container-rgb {
|
||||
#bm-window-filter.bm-windowed .bm-filter-container-rgb {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 0.5ch;
|
||||
|
|
@ -128,11 +128,16 @@
|
|||
}
|
||||
|
||||
/* Filter window hide color button SVG in windowed mode */
|
||||
.bm-windowed .bm-filter-container-rgb svg {
|
||||
#bm-window-filter.bm-windowed .bm-filter-container-rgb svg {
|
||||
width: 3ch;
|
||||
}
|
||||
|
||||
/* Filter window header 2 in windowed mode */
|
||||
.bm-windowed .bm-filter-color h2 {
|
||||
#bm-window-filter.bm-windowed .bm-filter-color h2 {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
/* Filter window dragbar text area in windowed mode */
|
||||
#bm-window-filter #bm-filter-windowed-color-totals {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ export default class WindowFilter extends Overlay {
|
|||
.addSpan({'id': 'bm-filter-tot-completed', 'innerHTML': '??? ???'}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-container'})
|
||||
.addP({'innerHTML': `Colors with the icon ${this.eyeOpen.replace('<svg', '<svg aria-label="Eye Open"')} will be shown on the canvas. Colors with the icon ${this.eyeClosed.replace('<svg', '<svg aria-label="Eye Closed"')} will not be shown on the canvas. The "Hide All Colors" and "Show All Colors" buttons only apply to colors that display in the list below. The amount of correct pixels is dependent on how many tiles of the template you have loaded since you last opened Wplace.live. If all tiles have been loaded, then the "correct pixel" count is accurate.`}).buildElement()
|
||||
.addP({'innerHTML': `Press the 🗗 button to make this window smaller. Colors with the icon ${this.eyeOpen.replace('<svg', '<svg aria-label="Eye Open"')} will be shown on the canvas. Colors with the icon ${this.eyeClosed.replace('<svg', '<svg aria-label="Eye Closed"')} will not be shown on the canvas. The "Hide All Colors" and "Show All Colors" buttons only apply to colors that display in the list below. The amount of correct pixels is dependent on how many tiles of the template you have loaded since you last opened Wplace.live. If all tiles have been loaded, then the "correct pixel" count is accurate.`}).buildElement()
|
||||
.buildElement()
|
||||
.addHr().buildElement()
|
||||
.addForm({'class': 'bm-container'})
|
||||
|
|
@ -210,10 +210,19 @@ export default class WindowFilter extends Overlay {
|
|||
this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window bm-windowed'})
|
||||
.addDragbar()
|
||||
.addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Color Filter"', 'data-button-status': 'expanded'}, (instance, button) => {
|
||||
button.onclick = () => instance.handleMinimization(button);
|
||||
button.onclick = () => {
|
||||
const windowedColorTotals = document.querySelector('#bm-filter-windowed-color-totals');
|
||||
if (windowedColorTotals) {
|
||||
windowedColorTotals.style.display = (button.dataset['buttonStatus'] == 'expanded') ? 'none' : '';
|
||||
}
|
||||
instance.handleMinimization(button);
|
||||
};
|
||||
button.ontouchend = () => {button.click()}; // Needed only to negate weird interaction with dragbar
|
||||
}).buildElement()
|
||||
.addDiv().buildElement() // Contains the minimized h1 element
|
||||
.addDiv()
|
||||
.addSpan({'id': 'bm-filter-windowed-color-totals', 'class': 'bm-dragbar-text', 'style': 'font-weight: 700;'}).buildElement() // Contains correct / total pixel values
|
||||
// Minimized h1 element will appear here
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-flex-center'})
|
||||
.addButton({'class': 'bm-button-circle', 'textContent': '🗖', 'aria-label': 'Switch to fullscreen mode for "Color Filter"'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
|
|
@ -423,7 +432,7 @@ export default class WindowFilter extends Overlay {
|
|||
.addSmall({'textContent': `#${color.id.toString().padStart(2, 0)}`}).buildElement()
|
||||
.addSmall({'class': 'bm-filter-color-pxl-cnt', 'textContent': `${colorCorrectLocalized} / ${colorTotalLocalized}`}).buildElement()
|
||||
.buildElement()
|
||||
.addP({'class': 'bm-filter-color-pxl-desc', 'textContent': `${((typeof colorIncorrect == 'number') && !isNaN(colorIncorrect)) ? colorIncorrect : '???'} incorrect pixels. Completed: ${colorPercent}`}).buildElement()
|
||||
.addP({'class': 'bm-filter-color-pxl-desc', 'textContent': `${((typeof colorIncorrect == 'number') && !isNaN(colorIncorrect)) ? colorIncorrect : '???'} incorrect pixel${(colorIncorrect == 1) ? '' : 's'}. Completed: ${colorPercent}`}).buildElement()
|
||||
.buildElement()
|
||||
.buildElement();
|
||||
}
|
||||
|
|
@ -513,18 +522,21 @@ export default class WindowFilter extends Overlay {
|
|||
}
|
||||
}
|
||||
|
||||
/** The information about a specific color on the palette.
|
||||
* @typedef {Object} ColorData
|
||||
* @property {number | string} colorTotal
|
||||
* @property {string} colorTotalLocalized
|
||||
* @property {number | string} colorCorrect
|
||||
* @property {string} colorCorrectLocalized
|
||||
* @property {string} colorPercent
|
||||
* @property {number} colorIncorrect
|
||||
*/
|
||||
|
||||
/** Updates the information inside the colors in the color list.
|
||||
* If the color list does not exist yet, it returns the color information instead.
|
||||
* This assumes the information inside each element is the same between fullscreen and windowed mode.
|
||||
* @since 0.90.60
|
||||
* @returns {Object<number, {
|
||||
* colorTotal: number | string,
|
||||
* colorTotalLocalized: string,
|
||||
* colorCorrect: number | string,
|
||||
* colorCorrectLocalized: string,
|
||||
* colorPercent: string,
|
||||
* colorIncorrect: number
|
||||
* }}
|
||||
* @returns {Object.<number, ColorData>}
|
||||
*/
|
||||
updateColorList() {
|
||||
|
||||
|
|
@ -577,6 +589,22 @@ export default class WindowFilter extends Overlay {
|
|||
}
|
||||
}
|
||||
|
||||
// Obtains the correct / total pixels display element, or `undefined` if in fullscreen mode
|
||||
const windowedColorTotals = document.querySelector('#bm-filter-windowed-color-totals');
|
||||
|
||||
// If the element exists...
|
||||
if (windowedColorTotals) {
|
||||
|
||||
// Returns the number, unlocalized (no space to localize)
|
||||
// OR returns the three characters on either end of the string, with the middle replaced with an ellipse.
|
||||
// E.g. '1234567' or '123…678'
|
||||
const allCorrect = (this.allPixelsCorrectTotal.toString().length > 7) ? this.allPixelsCorrectTotal.toString().slice(0, 2) + '…' + this.allPixelsCorrectTotal.toString().slice(-3) : this.allPixelsCorrectTotal.toString();
|
||||
const allTotal = (this.allPixelsTotal.toString().length > 7) ? this.allPixelsTotal.toString().slice(0, 2) + '…' + this.allPixelsTotal.toString().slice(-3) : this.allPixelsTotal.toString();
|
||||
|
||||
// Updates the display with XSS protection enabled (because why not)
|
||||
this.updateInnerHTML('#bm-filter-windowed-color-totals', `${allCorrect}/${allTotal}`, true);
|
||||
}
|
||||
|
||||
// Return early if the color list does not exist.
|
||||
// We can't update DOM elements that don't exist, so we exit now.
|
||||
if (!colorList) {return colorStatistics;}
|
||||
|
|
@ -610,7 +638,7 @@ export default class WindowFilter extends Overlay {
|
|||
|
||||
// Updates the pixel description if it exists
|
||||
const pixelDesc = document.querySelector(`#${this.windowID} .bm-filter-color[data-id="${colorID}"] .bm-filter-color-pxl-desc`);
|
||||
if (pixelDesc) {pixelDesc.textContent = `${((typeof colorIncorrect == 'number') && !isNaN(colorIncorrect)) ? colorIncorrect : '???'} incorrect pixels. Completed: ${colorPercent}`;}
|
||||
if (pixelDesc) {pixelDesc.textContent = `${((typeof colorIncorrect == 'number') && !isNaN(colorIncorrect)) ? colorIncorrect : '???'} incorrect pixel${(colorIncorrect == 1) ? '' : 's'}. Completed: ${colorPercent}`;}
|
||||
}
|
||||
|
||||
// Since the dataset has changed, we need to sort again
|
||||
|
|
|
|||
|
|
@ -163,6 +163,11 @@ export default class WindowMain extends Overlay {
|
|||
.addDiv({'class': 'bm-container bm-flex-between', 'style': 'margin-bottom: 0; flex-direction: column;'})
|
||||
.addDiv({'class': 'bm-flex-between'})
|
||||
// .addButton({'class': 'bm-button-circle', 'innerHTML': '🖌'}).buildElement()
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': '⚙️', 'title': 'Settings'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
instance.settingsManager.buildWindow();
|
||||
}
|
||||
}).buildElement()
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': '🧙', 'title': 'Template Wizard'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
const templateManager = instance.apiManager?.templateManager;
|
||||
|
|
|
|||
79
src/WindowSettings.css
Normal file
79
src/WindowSettings.css
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/* @since 0.91.22 */
|
||||
|
||||
/* Highlight preset group container */
|
||||
#bm-window-settings div:has(> .bm-highlight-preset-container) {
|
||||
width: fit-content;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Highlight preset container */
|
||||
#bm-window-settings .bm-highlight-preset-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
/* Highlight preset title */
|
||||
#bm-window-settings .bm-highlight-preset-container span {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
/* Highlight preset button */
|
||||
#bm-window-settings .bm-highlight-preset-container button {
|
||||
width: fit-content;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Highlight preset SVG */
|
||||
#bm-window-settings .bm-highlight-preset-container svg {
|
||||
stroke: #333;
|
||||
stroke-width: 0.02px;
|
||||
width: 100%;
|
||||
min-width: 1.5ch;
|
||||
max-width: 14.5ch;
|
||||
}
|
||||
|
||||
/* Highlight preset SVG on hover/focus */
|
||||
#bm-window-settings .bm-highlight-preset-container button:hover svg,
|
||||
#bm-window-settings .bm-highlight-preset-container button:focus svg {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Highlight pattern container */
|
||||
#bm-window-settings .bm-highlight-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
width: 25%;
|
||||
min-width: 3ch;
|
||||
max-width: 15ch;
|
||||
}
|
||||
|
||||
/* Highlight pattern button */
|
||||
#bm-window-settings .bm-highlight-grid > button {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-color: white;
|
||||
border: #333 1px solid;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Highlight pattern button in 'Incorrect' mode */
|
||||
#bm-window-settings .bm-highlight-grid > button[data-status="Incorrect"] {
|
||||
background-color: brown;
|
||||
}
|
||||
|
||||
/* Highlight pattern button in 'Template' mode */
|
||||
#bm-window-settings .bm-highlight-grid > button[data-status="Template"] {
|
||||
background-color: darkslategray;
|
||||
}
|
||||
|
||||
/* Highlight pattern button when hovered/focused */
|
||||
#bm-window-settings .bm-highlight-grid > button:hover,
|
||||
#bm-window-settings .bm-highlight-grid > button:focus {
|
||||
opacity: 0.8;
|
||||
}
|
||||
97
src/WindowSettings.js
Normal file
97
src/WindowSettings.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import Overlay from "./Overlay";
|
||||
|
||||
/** The overlay builder for the settings window in Blue Marble.
|
||||
* The logic for this window is managed in {@link SettingsManager}
|
||||
* @description This class handles the overlay UI for the settings window of the Blue Marble userscript.
|
||||
* @class WindowSettings
|
||||
* @since 0.91.11
|
||||
* @see {@link Overlay} for examples
|
||||
*/
|
||||
export default class WindowSettings extends Overlay {
|
||||
|
||||
/** Constructor for the Settings window
|
||||
* @param {string} name - The name of the userscript
|
||||
* @param {string} version - The version of the userscript
|
||||
* @since 0.91.11
|
||||
* @see {@link Overlay#constructor} for examples
|
||||
*/
|
||||
constructor(name, version) {
|
||||
super(name, version); // Executes the code in the Overlay constructor
|
||||
this.window = null; // Contains the *window* DOM tree
|
||||
this.windowID = 'bm-window-settings'; // The ID attribute for this window
|
||||
this.windowParent = document.body; // The parent of the window DOM tree
|
||||
}
|
||||
|
||||
/** Spawns a Settings window.
|
||||
* If another settings window already exists, we DON'T spawn another!
|
||||
* Parent/child relationships in the DOM structure below are indicated by indentation.
|
||||
* @since 0.91.11
|
||||
*/
|
||||
buildWindow() {
|
||||
|
||||
// If a settings window already exists, close it
|
||||
if (document.querySelector(`#${this.windowID}`)) {
|
||||
document.querySelector(`#${this.windowID}`).remove();
|
||||
return;
|
||||
}
|
||||
|
||||
this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window'})
|
||||
.addDragbar()
|
||||
.addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Color Filter"', 'data-button-status': 'expanded'}, (instance, button) => {
|
||||
button.onclick = () => instance.handleMinimization(button);
|
||||
button.ontouchend = () => {button.click()}; // Needed only to negate weird interaction with dragbar
|
||||
}).buildElement()
|
||||
.addDiv().buildElement() // Contains the minimized h1 element
|
||||
.addDiv({'class': 'bm-flex-center'})
|
||||
.addButton({'class': 'bm-button-circle', 'textContent': '✖', 'aria-label': 'Close window "Color Filter"'}, (instance, button) => {
|
||||
button.onclick = () => {document.querySelector(`#${this.windowID}`)?.remove();};
|
||||
button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-window-content'})
|
||||
.addDiv({'class': 'bm-container bm-center-vertically'})
|
||||
.addHeader(1, {'textContent': 'Settings'}).buildElement()
|
||||
.buildElement()
|
||||
.addHr().buildElement()
|
||||
.addP({'textContent': 'Settings take 5 seconds to save.'}).buildElement()
|
||||
.addDiv({'class': 'bm-container bm-scrollable'}, (instance, div) => {
|
||||
// Each category in the settings window
|
||||
this.buildHighlight();
|
||||
this.buildTemplate();
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.buildElement().buildOverlay(this.windowParent);
|
||||
|
||||
// Creates dragging capability on the drag bar for dragging the window
|
||||
this.handleDrag(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-dragbar`);
|
||||
}
|
||||
|
||||
/** Displays an error when a settings category fails to load.
|
||||
* @param {string} name - The name of the category
|
||||
* @since 0.91.11
|
||||
*/
|
||||
#errorOverrideFailure(name) {
|
||||
this.window = this.addDiv({'class': 'bm-container'})
|
||||
.addHeader(2, {'textContent': name}).buildElement()
|
||||
.addHr().buildElement()
|
||||
.addP({'innerHTML': `An error occured loading the ${name} category. <code>SettingsManager</code> failed to override the ${name} function inside <code>WindowSettings</code>.`}).buildElement()
|
||||
.buildElement();
|
||||
}
|
||||
|
||||
/** Builds the highlight section of the window.
|
||||
* This should be overriden by {@link SettingsManager}
|
||||
* @since 0.91.11
|
||||
*/
|
||||
buildHighlight() {
|
||||
this.#errorOverrideFailure('Pixel Highlight');
|
||||
}
|
||||
|
||||
/** Builds the template section of the window.
|
||||
* This should be overriden by {@link SettingsManager}
|
||||
* @since 0.91.68
|
||||
*/
|
||||
buildTemplate() {
|
||||
this.#errorOverrideFailure('Template');
|
||||
}
|
||||
}
|
||||
|
|
@ -6,4 +6,5 @@
|
|||
@import './confettiManager.css';
|
||||
@import './overlay.css';
|
||||
@import './WindowFilter.css';
|
||||
@import './WindowSettings.css';
|
||||
@import './WindowWizard.css';
|
||||
|
|
|
|||
11
src/main.js
11
src/main.js
|
|
@ -8,6 +8,7 @@ import TemplateManager from './templateManager.js';
|
|||
import { consoleLog, consoleWarn } from './utils.js';
|
||||
import WindowMain from './WindowMain.js';
|
||||
import WindowTelemetry from './WindowTelemetry.js';
|
||||
import SettingsManager from './settingsManager.js';
|
||||
|
||||
const name = GM_info.script.name.toString(); // Name of userscript
|
||||
const version = GM_info.script.version.toString(); // Version of userscript
|
||||
|
|
@ -186,19 +187,25 @@ if (!!(robotoMonoInjectionPoint.indexOf('@font-face') + 1)) {
|
|||
document.head?.appendChild(stylesheetLink);
|
||||
}
|
||||
|
||||
const userSettings = JSON.parse(GM_getValue('bmUserSettings', '{}')); // Loads the user settings
|
||||
|
||||
// CONSTRUCTORS
|
||||
const observers = new Observers(); // Constructs a new Observers object
|
||||
const windowMain = new WindowMain(name, version); // Constructs a new Overlay object for the main overlay
|
||||
const templateManager = new TemplateManager(name, version, windowMain); // Constructs a new TemplateManager object
|
||||
const templateManager = new TemplateManager(name, version); // Constructs a new TemplateManager object
|
||||
const apiManager = new ApiManager(templateManager); // Constructs a new ApiManager object
|
||||
const settingsManager = new SettingsManager(name, version, userSettings); // Constructs a new SettingsManager
|
||||
|
||||
windowMain.setSettingsManager(settingsManager); // Sets the settings manager
|
||||
windowMain.setApiManager(apiManager); // Sets the API manager
|
||||
|
||||
templateManager.setWindowMain(windowMain);
|
||||
templateManager.setSettingsManager(settingsManager); // Sets the settings manager
|
||||
|
||||
const storageTemplates = JSON.parse(GM_getValue('bmTemplates', '{}'));
|
||||
console.log(storageTemplates);
|
||||
templateManager.importJSON(storageTemplates); // Loads the templates
|
||||
|
||||
const userSettings = JSON.parse(GM_getValue('bmUserSettings', '{}')); // Loads the user settings
|
||||
|
||||
console.log(userSettings);
|
||||
console.log(Object.keys(userSettings).length);
|
||||
|
|
|
|||
|
|
@ -92,7 +92,9 @@
|
|||
}
|
||||
|
||||
/* Header 1 when inside dragbar */
|
||||
.bm-dragbar h1 {
|
||||
/* Or, when the custom class is used */
|
||||
.bm-dragbar h1,
|
||||
.bm-dragbar-text {
|
||||
font-size: 1.2em;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
|
|
@ -345,8 +347,10 @@ input[type="file"] {
|
|||
|
||||
/* Containers for "sections" of elements in windowed mode */
|
||||
/* Does not apply to the main window */
|
||||
.bm-container:not(#bm-window-main .bm-container) {
|
||||
margin: 0.25em 0;
|
||||
.bm-windowed .bm-container:not(#bm-window-main .bm-container) {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
/* Do not use 'margin' shorthand, as it will override left/right margin */
|
||||
}
|
||||
|
||||
/* Header 1 in windowed mode */
|
||||
|
|
|
|||
331
src/settingsManager.js
Normal file
331
src/settingsManager.js
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
import { sleep } from "./utils";
|
||||
import WindowSettings from "./WindowSettings";
|
||||
|
||||
/** SettingsManager class for handling user settings and making them persist between sessions.
|
||||
* Logic for {@link WindowSettings} is managed here.
|
||||
* "Flags" should follow the same styling as `.classList()` and should not contain spaces.
|
||||
* A flag should always be false by default.
|
||||
* When a flag is false, it will not exist in the "flags" Array.
|
||||
* (Therefore, "flags" should be `[]` by default)
|
||||
* If it exists in the "flags" Array, then the flag is `true`.
|
||||
* @class SettingsManager
|
||||
* @since 0.91.11
|
||||
* @example
|
||||
* {
|
||||
* "uuid": "497dcba3-ecbf-4587-a2dd-5eb0665e6880",
|
||||
* "telemetry": 1,
|
||||
* "flags": ["hl-noTrans", "ftr-oWin", "te-noSkip"],
|
||||
* "highlight": [[1,0,-1],[1,-1,0],[2,1,0],[1,0,1]],
|
||||
* "filter": [-2,0,4,5,6,29,63]
|
||||
* }
|
||||
*/
|
||||
export default class SettingsManager extends WindowSettings {
|
||||
|
||||
/** Constructor for the SettingsManager class
|
||||
* @param {string} name - The name of the userscript
|
||||
* @param {string} version - The version of the userscript
|
||||
* @param {Object} userSettings - The user settings as an object
|
||||
* @since 0.91.11
|
||||
*/
|
||||
constructor(name, version, userSettings) {
|
||||
super(name, version); // Executes WindowSettings constructor
|
||||
|
||||
this.userSettings = userSettings; // User settings as an Object
|
||||
this.userSettings.flags ??= []; // Makes sure the key "flags" always exists
|
||||
this.userSettingsOld = structuredClone(this.userSettings); // Creates a duplicate of the user settings to store the old version of user settings from 5+ seconds ago
|
||||
this.userSettingsSaveLocation = 'bmUserSettings'; // Storage save location
|
||||
|
||||
this.updateFrequency = 5000; // Cooldown between saving to storage (throttle)
|
||||
this.lastUpdateTime = 0; // When this unix timestamp is within the last 5 seconds, we should save this.userSettings to storage
|
||||
|
||||
setInterval(this.updateUserStorage.bind(this), this.updateFrequency); // Runs every X seconds (see updateFrequency)
|
||||
}
|
||||
|
||||
/** Updates the user settings in userscript storage
|
||||
* @since 0.91.39
|
||||
*/
|
||||
async updateUserStorage() {
|
||||
|
||||
// Turns the objects into a string
|
||||
const userSettingsCurrent = JSON.stringify(this.userSettings);
|
||||
const userSettingsOld = JSON.stringify(this.userSettingsOld);
|
||||
|
||||
// If the user settings have changed, AND the last update to user storage was over 5 seconds ago (5sec throttle)...
|
||||
if ((userSettingsCurrent != userSettingsOld) && ((Date.now() - this.lastUpdateTime) > this.updateFrequency)) {
|
||||
await GM.setValue(this.userSettingsSaveLocation, userSettingsCurrent); // Updates user storage
|
||||
this.userSettingsOld = structuredClone(this.userSettings); // Updates the old user settings with a duplicate of the current user settings
|
||||
this.lastUpdateTime = Date.now(); // Updates the variable that contains the last time updated
|
||||
console.log(userSettingsCurrent);
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggles a boolean flag to the state that was passed in.
|
||||
* If no state was passed in, the flag will flip to the opposite state.
|
||||
* The existence of the flag determines its state. If it exists, it is `true`.
|
||||
* @param {string} flagName - The name of the flag to toggle
|
||||
* @param {boolean} [state=undefined] - (Optional) The state to change the flag to
|
||||
* @since 0.91.60
|
||||
*/
|
||||
toggleFlag(flagName, state = undefined) {
|
||||
|
||||
const flagIndex = this.userSettings?.flags?.indexOf(flagName) ?? -1; // Is the flag `true`?
|
||||
|
||||
// If the flag is enabled, AND the user does not want to force the flag to be true...
|
||||
if ((flagIndex != -1) && (state !== true)) {
|
||||
|
||||
this.userSettings?.flags?.splice(flagIndex, 1); // Remove the flag (makes it false)
|
||||
} else if ((flagIndex == -1) && (state !== false)) {
|
||||
// Else if the flag is disabled, AND the user does not want to force the flag to be false...
|
||||
this.userSettings?.flags?.push(flagName); // Add the flag (makes it true)
|
||||
}
|
||||
}
|
||||
|
||||
// This is one of the most insane OOP setups I have ever laid my eyes on
|
||||
|
||||
/** Builds the "highlight" category of the settings window
|
||||
* @since 0.91.18
|
||||
* @see WindowSettings#buildHighlight
|
||||
*/
|
||||
buildHighlight() {
|
||||
|
||||
const highlightPresetOff = '<svg viewBox="0 0 3 3"><path d="M0,0H3V3H0ZM0,1H3M0,2H3M1,0V3M2,0V3" fill="#fff"/><path d="M1,1H2V2H1Z" fill="#2f4f4f"/></svg>';
|
||||
const highlightPresetCross = '<svg viewBox="0 0 3 3"><path d="M0,0H3V3H0Z" fill="#fff"/><path d="M1,0H2V1H3V2H2V3H1V2H0V1H1Z" fill="brown"/><path d="M1,1H2V2H1Z" fill="#2f4f4f"/></svg>';
|
||||
|
||||
// Obtains user settings for highlight from storage, or the default array if nothing was found
|
||||
const storedHighlight = this.userSettings?.highlight ?? [[1, 0, 1], [2, 0, 0], [1, -1, 0], [1, 1, 0], [1, 0, -1]];
|
||||
|
||||
// Constructs the category and adds it to the window
|
||||
this.window = this.addDiv({'class': 'bm-container'})
|
||||
.addHeader(2, {'textContent': 'Pixel Highlight'}).buildElement()
|
||||
.addHr().buildElement()
|
||||
.addDiv({'class': 'bm-container', 'style': 'margin-left: 1.5ch;'})
|
||||
.addCheckbox({'textContent': 'Highlight transparent pixels'}, (instance, label, checkbox) => {
|
||||
checkbox.checked = !this.userSettings?.flags?.includes('hl-noTrans'); // Makes the checkbox match the last stored user setting
|
||||
checkbox.onchange = (event) => this.toggleFlag('hl-noTrans', !event.target.checked); // Forces the flag to be the opposite state as the checkbox. E.g. "Checked" means 'hl-noTrans' is false (does not exist).
|
||||
}).buildElement()
|
||||
.addP({'id': 'bm-highlight-preset-label', 'textContent': 'Choose a preset:', 'style': 'font-weight: 700;'}).buildElement()
|
||||
.addDiv({'class': 'bm-flex-center', 'role': 'group', 'aria-labelledby': 'bm-highlight-preset-label'})
|
||||
.addDiv({'class': 'bm-highlight-preset-container'})
|
||||
.addSpan({'textContent': 'None'}).buildElement()
|
||||
.addButton({'innerHTML': highlightPresetOff, 'aria-label': 'Preset "None"'}, (instance, button) => {button.onclick = () => this.#updateHighlightToPreset('None')}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-highlight-preset-container'})
|
||||
.addSpan({'textContent': 'Cross'}).buildElement()
|
||||
.addButton({'innerHTML': highlightPresetCross, 'aria-label': 'Preset "Cross Shape"'}, (instance, button) => {button.onclick = () => this.#updateHighlightToPreset('Cross')}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-highlight-preset-container'})
|
||||
.addSpan({'textContent': 'X'}).buildElement()
|
||||
.addButton({'innerHTML': highlightPresetCross.replace('d="M1,0H2V1H3V2H2V3H1V2H0V1H1Z"', 'd="M0,0V1H3V0H2V3H3V2H0V3H1V0Z"'), 'aria-label': 'Preset "X Shape"'}, (instance, button) => {button.onclick = () => this.#updateHighlightToPreset('X')}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-highlight-preset-container'})
|
||||
.addSpan({'textContent': 'Full'}).buildElement()
|
||||
.addButton({'innerHTML': highlightPresetOff.replace('#fff', '#2f4f4f'), 'aria-label': 'Preset "Full Template"'}, (instance, button) => {button.onclick = () => this.#updateHighlightToPreset('Full')}).buildElement()
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
.addP({'id': 'bm-highlight-grid-label', 'textContent': 'Create a custom pattern:', 'style': 'font-weight: 700;'}).buildElement()
|
||||
.addDiv({'class': 'bm-highlight-grid', 'role': 'group', 'aria-labelledby': 'bm-highlight-grid-label'});
|
||||
// We leave this open so we can add buttons
|
||||
|
||||
// For each of the 9 buttons...
|
||||
for (let buttonY = -1; buttonY <= 1; buttonY++) {
|
||||
for (let buttonX = -1; buttonX <= 1; buttonX++) {
|
||||
const buttonState = storedHighlight[storedHighlight.findIndex(([, x, y]) => ((x == buttonX) && (y == buttonY)))]?.[0] ?? 0;
|
||||
let buttonStateName = 'Disabled';
|
||||
if (buttonState == 1) {
|
||||
buttonStateName = 'Incorrect';
|
||||
} else if (buttonState == 2) {
|
||||
buttonStateName = 'Template';
|
||||
}
|
||||
this.window = this.addButton({
|
||||
'data-status': buttonStateName,
|
||||
'aria-label': `Sub-pixel ${buttonStateName.toLowerCase()}`
|
||||
}, (instance, button) => {
|
||||
button.onclick = () => this.#updateHighlightSettings(button, [buttonX, buttonY])
|
||||
}).buildElement();
|
||||
}
|
||||
}
|
||||
|
||||
// Resumes from where we left off before we added buttons
|
||||
this.window = this.buildElement()
|
||||
.buildElement()
|
||||
.buildElement();
|
||||
}
|
||||
|
||||
/** Updates the display of the highlight buttons in the settings window.
|
||||
* Additionally, it will update user settings with the new selection.
|
||||
* @param {HTMLButtonElement} button - The button that was pressed
|
||||
* @param {Array<number, number>} coords - The relative coordinates of the button
|
||||
* @since 0.91.46
|
||||
*/
|
||||
#updateHighlightSettings(button, coords) {
|
||||
|
||||
button.disabled = true; // Disabled the button until we are done
|
||||
|
||||
const status = button.dataset['status']; // Obtains the current status of the button
|
||||
|
||||
/** Obtains the old highlight storage, or sets it to default. @type {Array<number[]>} */
|
||||
const userStorageOld = this.userSettings?.highlight ?? [[1, 0, 1], [2, 0, 0], [1, -1, 0], [1, 1, 0], [1, 0, -1]];
|
||||
|
||||
let userStorageChange = [2, 0, 0]; // The new change to the user storage
|
||||
|
||||
const userStorageNew = userStorageOld; // The old storage with the new change
|
||||
|
||||
// For each different type of status...
|
||||
switch (status) {
|
||||
|
||||
// If the button was in the "Disabled" state
|
||||
case 'Disabled':
|
||||
|
||||
// Change to "Incorrect"
|
||||
button.dataset['status'] = 'Incorrect';
|
||||
button.ariaLabel = 'Sub-pixel incorrect';
|
||||
userStorageChange = [1, ...coords];
|
||||
break;
|
||||
|
||||
// If the button was in the "Incorrect" state
|
||||
case 'Incorrect':
|
||||
|
||||
// Change to "Template"
|
||||
button.dataset['status'] = 'Template';
|
||||
button.ariaLabel = 'Sub-pixel template';
|
||||
userStorageChange = [2, ...coords];
|
||||
break;
|
||||
|
||||
// If the button was in the "Template" state
|
||||
case 'Template':
|
||||
|
||||
// Change to "Disabled"
|
||||
button.dataset['status'] = 'Disabled';
|
||||
button.ariaLabel = 'Sub-pixel disabled';
|
||||
userStorageChange = [0, ...coords];
|
||||
break;
|
||||
}
|
||||
|
||||
// Finds the index of the pixel to change
|
||||
const indexOfChange = userStorageOld.findIndex(([, x, y]) => ((x == userStorageChange[1]) && (y == userStorageChange[2])));
|
||||
|
||||
// If the new sub-pixel state is NOT disabled
|
||||
if (userStorageChange[0] != 0) {
|
||||
|
||||
// If a sub-pixel was found...
|
||||
if (indexOfChange != -1) {
|
||||
userStorageNew[indexOfChange] = userStorageChange;
|
||||
} else {
|
||||
userStorageNew.push(userStorageChange);
|
||||
}
|
||||
} else if (indexOfChange != -1) {
|
||||
// Else, it is disabled. We want to remove it if it exists.
|
||||
userStorageNew.splice(indexOfChange, 1); // Removes 1 index from the array at the index of the pixel change
|
||||
}
|
||||
|
||||
this.userSettings['highlight'] = userStorageNew;
|
||||
// TODO: Add timer update here
|
||||
|
||||
button.disabled = false; // Reenables the button since we are done
|
||||
}
|
||||
|
||||
/** Changes the highlight buttons to the clicked preset.
|
||||
* @param {string} preset - The name of the preset
|
||||
* @since 0.91.49
|
||||
*/
|
||||
async #updateHighlightToPreset(preset) {
|
||||
|
||||
// Obtains all preset buttons as a NodeList
|
||||
const presetButtons = document.querySelectorAll('.bm-highlight-preset-container button');
|
||||
|
||||
// For each preset...
|
||||
for (const button of presetButtons) {
|
||||
button.disabled = true; // Disables the button
|
||||
}
|
||||
|
||||
let presetArray = [0,0,0,0,2,0,0,0,0]; // The preset "None"
|
||||
|
||||
// Selects the preset passed in
|
||||
switch (preset) {
|
||||
case 'Cross':
|
||||
presetArray = [0,1,0,1,2,1,0,1,0]; // The preset "Cross"
|
||||
break;
|
||||
case 'X':
|
||||
presetArray = [1,0,1,0,2,0,1,0,1]; // The preset "X"
|
||||
break;
|
||||
case 'Full':
|
||||
presetArray = [2,2,2,2,2,2,2,2,2]; // The preset "Full"
|
||||
break;
|
||||
}
|
||||
|
||||
// Obtains the buttons to click as a NodeList
|
||||
const buttons = document.querySelector('.bm-highlight-grid')?.childNodes ?? [];
|
||||
|
||||
// For each button...
|
||||
for (let buttonIndex = 0; buttonIndex < buttons.length; buttonIndex++) {
|
||||
|
||||
const button = buttons[buttonIndex]; // Gets the current button to check
|
||||
|
||||
// Gets the state of the button as a number
|
||||
let buttonState = button.dataset['status'];
|
||||
buttonState = (buttonState != 'Disabled') ? ((buttonState != 'Incorrect') ? 2 : 1) : 0;
|
||||
|
||||
// Finds the difference between the preset and the button
|
||||
let buttonStateDelta = presetArray[buttonIndex] - buttonState;
|
||||
|
||||
// Since there is no difference, the button matches, so we skip it
|
||||
if (buttonStateDelta == 0) {continue;}
|
||||
|
||||
// Makes the difference positive
|
||||
buttonStateDelta += (buttonStateDelta < 0) ? 3 : 0;
|
||||
|
||||
/** At this point, these are the possible options:
|
||||
* 1. The preset is zero and the button is two (-2) so we need to click once
|
||||
* 2. The preset is one and the button is two (-1) so we need to click twice
|
||||
* 3. The preset is one ahead of the button (1) so we need to click once
|
||||
* 4. The preset is two ahead of the button (2) so we need to click twice
|
||||
* Due to the addition of three in the line above, options 1 & 3 combine, and options 2 & 4 combine.
|
||||
* Now the only options we have are:
|
||||
* 1. If (1) then click once
|
||||
* 2. If (2) then click twice
|
||||
* Also due to the addition of three in the line above, our two options are POSITIVE numbers
|
||||
*/
|
||||
|
||||
button.click(); // Clicks once
|
||||
|
||||
// Clicks a second time if needed
|
||||
if (buttonStateDelta == 2) {
|
||||
|
||||
// For 0.2 seconds, or when the button is NOT disabled, wait for 10 milliseconds before attempting to continue
|
||||
for (let timeWaited = 0; timeWaited < 200; timeWaited += 10) {
|
||||
if (!button.disabled) {break;} // Breaks early once the button is enabled
|
||||
await sleep(10);
|
||||
}
|
||||
|
||||
button.click(); // Clicks again
|
||||
}
|
||||
}
|
||||
|
||||
// For each preset...
|
||||
for (const button of presetButtons) {
|
||||
button.disabled = false; // Re-enables the button
|
||||
}
|
||||
}
|
||||
|
||||
/** Build the "template" category of settings window
|
||||
* @since 0.91.68
|
||||
* @see WindowSettings#buildTemplate
|
||||
*/
|
||||
buildTemplate() {
|
||||
|
||||
this.window = this.addDiv({'class': 'bm-container'})
|
||||
.addHeader(2, {'textContent': 'Pixel Highlight'}).buildElement()
|
||||
.addHr().buildElement()
|
||||
.addDiv({'class': 'bm-container', 'style': 'margin-left: 1.5ch;'})
|
||||
.addCheckbox({'textContent': 'Template creation should skip transparent tiles'}, (instance, label, checkbox) => {
|
||||
checkbox.checked = !this.userSettings?.flags?.includes('hl-noSkip'); // Makes the checkbox match the last stored user setting
|
||||
checkbox.onchange = (event) => this.toggleFlag('hl-noSkip', !event.target.checked); // If the user wants to skip, then the checkbox is NOT checked
|
||||
}).buildElement()
|
||||
.addCheckbox({'innerHTML': 'Experimental: Template creation should <em>aggressively</em> skip transparent tiles'}, (instance, label, checkbox) => {
|
||||
checkbox.checked = this.userSettings?.flags?.includes('hl-agSkip'); // Makes the checkbox match the last stored user setting
|
||||
checkbox.onchange = (event) => this.toggleFlag('hl-agSkip', event.target.checked); // If the user wants to aggressively skip, then the checkbox is checked
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import SettingsManager from "./settingsManager";
|
||||
import Template from "./Template";
|
||||
import { base64ToUint8, colorpaletteForBlueMarble, consoleError, consoleLog, consoleWarn, localizeNumber, numberToEncoded, sleep, viewCanvasInNewTab } from "./utils";
|
||||
import WindowMain from "./WindowMain";
|
||||
import WindowWizard from "./WindowWizard";
|
||||
|
||||
/** Manages the template system.
|
||||
|
|
@ -83,14 +85,17 @@ import WindowWizard from "./WindowWizard";
|
|||
export default class TemplateManager {
|
||||
|
||||
/** The constructor for the {@link TemplateManager} class.
|
||||
* @param {string} name - The name of the userscript
|
||||
* @param {string} version - The version of the userscript (SemVer as string)
|
||||
* @since 0.55.8
|
||||
*/
|
||||
constructor(name, version, overlay) {
|
||||
constructor(name, version) {
|
||||
|
||||
// Meta
|
||||
this.name = name; // Name of userscript
|
||||
this.version = version; // Version of userscript
|
||||
this.overlay = overlay; // The main instance of the Overlay class
|
||||
this.windowMain = null; // The main instance of the Overlay class
|
||||
this.settingsManager = null; // The main instance of the SettingsManager class
|
||||
this.schemaVersion = '2.0.0'; // Version of JSON schema
|
||||
this.userID = null; // The ID of the current user
|
||||
this.encodingBase = '!#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'; // Characters to use for encoding/decoding
|
||||
|
|
@ -111,6 +116,22 @@ export default class TemplateManager {
|
|||
this.shouldFilterColor = new Map();
|
||||
}
|
||||
|
||||
/** Updates the stored instance of the main window.
|
||||
* @param {WindowMain} windowMain - The main window instance
|
||||
* @since 0.91.54
|
||||
*/
|
||||
setWindowMain(windowMain) {
|
||||
this.windowMain = windowMain;
|
||||
}
|
||||
|
||||
/** Updates the stored instance of the SettingsManager.
|
||||
* @param {SettingsManager} settingsManager - The settings manager instance
|
||||
* @since 0.91.54
|
||||
*/
|
||||
setSettingsManager(settingsManager) {
|
||||
this.settingsManager = settingsManager;
|
||||
}
|
||||
|
||||
/** Creates the JSON object to store templates in
|
||||
* @returns {{ whoami: string, scriptVersion: string, schemaVersion: string, templates: Object }} The JSON object
|
||||
* @since 0.65.4
|
||||
|
|
@ -135,7 +156,7 @@ export default class TemplateManager {
|
|||
// Creates the JSON object if it does not already exist
|
||||
if (!this.templatesJSON) {this.templatesJSON = await this.createJSON(); console.log(`Creating JSON...`);}
|
||||
|
||||
this.overlay.handleDisplayStatus(`Creating template at ${coords.join(', ')}...`);
|
||||
this.windowMain.handleDisplayStatus(`Creating template at ${coords.join(', ')}...`);
|
||||
|
||||
// Creates a new template instance
|
||||
const template = new Template({
|
||||
|
|
@ -145,8 +166,16 @@ export default class TemplateManager {
|
|||
file: blob,
|
||||
coords: coords
|
||||
});
|
||||
|
||||
// Does the user want to skip transparent tiles while creating templates?
|
||||
const shouldSkipTransTiles = !this.settingsManager?.userSettings?.flags?.includes('hl-noSkip');
|
||||
|
||||
// Does the user want to aggressively skip transparent tiles while creating templates?
|
||||
const shouldAggSkipTransTiles = this.settingsManager?.userSettings?.flags?.includes('hl-agSkip');
|
||||
|
||||
console.log(`Should Skip: ${shouldSkipTransTiles}; Should Agg Skip: ${shouldAggSkipTransTiles}`);
|
||||
|
||||
const { templateTiles, templateTilesBuffers } = await template.createTemplateTiles(this.tileSize, this.paletteBM); // Chunks the tiles
|
||||
const { templateTiles, templateTilesBuffers } = await template.createTemplateTiles(this.tileSize, this.paletteBM, shouldSkipTransTiles, shouldAggSkipTransTiles); // Chunks the tiles
|
||||
|
||||
template.chunked = templateTiles; // Stores the chunked tile bitmaps
|
||||
|
||||
|
|
@ -166,7 +195,7 @@ export default class TemplateManager {
|
|||
this.templatesArray = []; // Remove this to enable multiple templates (2/2)
|
||||
this.templatesArray.push(template); // Pushes the Template object instance to the Template Array
|
||||
|
||||
this.overlay.handleDisplayStatus(`Template created at ${coords.join(', ')}!`);
|
||||
this.windowMain.handleDisplayStatus(`Template created at ${coords.join(', ')}!`);
|
||||
|
||||
console.log(Object.keys(this.templatesJSON.templates).length);
|
||||
console.log(this.templatesJSON);
|
||||
|
|
@ -497,12 +526,12 @@ export default class TemplateManager {
|
|||
const pixelCountFormatted = localizeNumber(totalPixels);
|
||||
|
||||
// Display status information about the templates being rendered
|
||||
this.overlay.handleDisplayStatus(
|
||||
this.windowMain.handleDisplayStatus(
|
||||
`Displaying ${templateCount} template${templateCount == 1 ? '' : 's'}.\nTotal pixels: ${pixelCountFormatted}`
|
||||
);
|
||||
} else {
|
||||
//this.overlay.handleDisplayStatus(`Displaying ${templateCount} templates.`);
|
||||
this.overlay.handleDisplayStatus(`Sleeping\nVersion: ${this.version}`);
|
||||
this.windowMain.handleDisplayStatus(`Sleeping\nVersion: ${this.version}`);
|
||||
return tileBlob; // No templates are on this tile. Return the original tile early
|
||||
}
|
||||
|
||||
|
|
@ -523,6 +552,26 @@ export default class TemplateManager {
|
|||
|
||||
const tileBeforeTemplates = context.getImageData(0, 0, drawSize, drawSize);
|
||||
const tileBeforeTemplates32 = new Uint32Array(tileBeforeTemplates.data.buffer);
|
||||
|
||||
// Obtains the highlight pattern
|
||||
const highlightPattern = this.settingsManager?.userSettings?.highlight || [[2, 0, 0]];
|
||||
// The code demands that a highlight pattern always exists.
|
||||
// Therefore, to disable highlighting, the highlight pattern is `[[2, 0, 0]]`.
|
||||
// `[[2, 0, 0]]` is special, and will skip the highlighting code altogether.
|
||||
// As a side-effect, the template will always display while enabled.
|
||||
// You can't disable all sub-pixels in order to hide the template.
|
||||
|
||||
// Contains the first index of the highlight pattern.
|
||||
const highlightPatternIndexZero = highlightPattern?.[0];
|
||||
// This is so we can later determine if the pattern is the preset "None"
|
||||
|
||||
// Should highlighting be disabled?
|
||||
const highlightDisabled = (
|
||||
(highlightPattern?.length == 1)
|
||||
&& (highlightPatternIndexZero?.[0] == 2)
|
||||
&& (highlightPatternIndexZero?.[1] == 0)
|
||||
&& (highlightPatternIndexZero?.[2] == 0)
|
||||
)
|
||||
|
||||
// For each template in this tile, draw them.
|
||||
for (const template of templatesToDraw) {
|
||||
|
|
@ -557,7 +606,9 @@ export default class TemplateManager {
|
|||
} = this.#calculateCorrectPixelsOnTile_And_FilterTile({
|
||||
tile: tileBeforeTemplates32,
|
||||
template: templateBeforeFilter32,
|
||||
templateInfo: [coordXtoDrawAt, coordYtoDrawAt, template.bitmap.width, template.bitmap.height]
|
||||
templateInfo: [coordXtoDrawAt, coordYtoDrawAt, template.bitmap.width, template.bitmap.height],
|
||||
highlightPattern: highlightPattern,
|
||||
highlightDisabled: highlightDisabled
|
||||
});
|
||||
|
||||
let pixelsCorrectTotal = 0;
|
||||
|
|
@ -573,7 +624,8 @@ export default class TemplateManager {
|
|||
|
||||
// If there are colors to filter, then we draw the filtered template on the canvas
|
||||
// Or, if there are Erased (#deface) pixels, then we draw the modified template on the canvas
|
||||
if ((this.shouldFilterColor.size != 0) || templateHasErased) {
|
||||
// Or, if the user has enabled highlighting, then we draw the modified template on the canvas
|
||||
if ((this.shouldFilterColor.size != 0) || templateHasErased || !highlightDisabled) {
|
||||
console.log('Colors to filter: ', this.shouldFilterColor);
|
||||
//context.putImageData(new ImageData(new Uint8ClampedArray(templateAfterFilter.buffer), template.bitmap.width, template.bitmap.height), coordXtoDrawAt, coordYtoDrawAt);
|
||||
context.drawImage(await createImageBitmap(new ImageData(new Uint8ClampedArray(templateAfterFilter.buffer), template.bitmap.width, template.bitmap.height)), coordXtoDrawAt, coordYtoDrawAt);
|
||||
|
|
@ -654,7 +706,7 @@ export default class TemplateManager {
|
|||
} else {
|
||||
// We don't know what the schema is. Unsupported?
|
||||
|
||||
this.overlay.handleDisplayError(`Template version ${schemaVersion} is unsupported.\nUse Blue Marble version ${scriptVersion} or load a new template.`);
|
||||
this.windowMain.handleDisplayError(`Template version ${schemaVersion} is unsupported.\nUse Blue Marble version ${scriptVersion} or load a new template.`);
|
||||
}
|
||||
|
||||
/** Loads schema of Blue Marble template storage
|
||||
|
|
@ -757,17 +809,22 @@ export default class TemplateManager {
|
|||
/** Calculates the correct pixels on this tile.
|
||||
* In addition, this function filters colors based on user input.
|
||||
* In addition, this function modifies colors to properly display (#deface).
|
||||
* In addition, this function modifies incorrect pixels to display highlighting.
|
||||
* This function has multiple purposes only to reduce iterations of scans over every pixel on the template.
|
||||
* @param {Object} params - Object containing all parameters
|
||||
* @param {Uint32Array} params.tile - The tile without templates as a Uint32Array
|
||||
* @param {Uint32Array} params.template - The template without filtering as a Uint32Array
|
||||
* @param {Array<Number, Number, Number, Number>} params.templateInfo - Information about template location and size
|
||||
* @param {Array<number[]>} params.highlightPattern - The highlight pattern selected by the user
|
||||
* @param {boolean} params.highlightDisabled - Should highlighting be disabled?
|
||||
* @returns {{correctPixels: Map<number, number>, filteredTemplate: Uint32Array}} A Map containing the color IDs (keys) and how many correct pixels there are for that color (values)
|
||||
*/
|
||||
#calculateCorrectPixelsOnTile_And_FilterTile({
|
||||
tile: tile32,
|
||||
template: template32,
|
||||
templateInfo: templateInformation
|
||||
templateInfo: templateInformation,
|
||||
highlightPattern: highlightPattern,
|
||||
highlightDisabled: highlightDisabled
|
||||
}) {
|
||||
|
||||
// Size of a pixel in actuality
|
||||
|
|
@ -788,6 +845,10 @@ export default class TemplateManager {
|
|||
|
||||
//console.log(`TemplateX: ${templateCoordX}\nTemplateY: ${templateCoordY}\nStarting Row:${templateCoordY+tilePixelOffsetY}\nStarting Column:${templateCoordX+tilePixelOffsetX}`);
|
||||
|
||||
// Obtains if the user wants to highlight tile pixels that are transparent, but the template pixel is not
|
||||
const shouldTransparentTilePixelsBeHighlighted = !this.settingsManager?.userSettings?.flags?.includes('hl-noTrans');
|
||||
// The actual logic of this boolean is "should all pixels be highlighted"
|
||||
|
||||
const { palette: _, LUT: lookupTable } = this.paletteBM; // Obtains the palette and LUT
|
||||
|
||||
// Makes a copy of the color palette Blue Marble uses, turns it into a Map, and adds data to count the amount of each color
|
||||
|
|
@ -796,6 +857,10 @@ export default class TemplateManager {
|
|||
// For each center pixel...
|
||||
for (let templateRow = 1; templateRow < templateHeight; templateRow += pixelSize) {
|
||||
for (let templateColumn = 1; templateColumn < templateWidth; templateColumn += pixelSize) {
|
||||
// ROWS ARE VERTICAL. "ROWS" AS IN, LIKE ON A SPREADSHEET
|
||||
// COLUMNS ARE HORIZONTAL. "COLUMNS" AS IN, LIKE ON A SPREADSHEET
|
||||
// THE FIFTH ROW IS FIVE DOWN FROM THE ZEROTH ROW
|
||||
// THE THIRD COLUMN IS TO THE RIGHT OF THE FIRST COLUMN
|
||||
|
||||
// The pixel on the tile to target (1 pixel above the template)
|
||||
const tileRow = (templateCoordY + templateRow) + tilePixelOffsetY; // (Template offset + current row) - 1
|
||||
|
|
@ -812,6 +877,9 @@ export default class TemplateManager {
|
|||
// Finds the best matching color ID for the template pixel. If none is found, default to "-2"
|
||||
const bestTemplateColorID = lookupTable.get(templatePixel) ?? -2;
|
||||
|
||||
// Finds the best matching color ID for the tile pixel. If none is found, default to "-2"
|
||||
const bestTileColorID = lookupTable.get(tilePixelAbove) ?? -2;
|
||||
|
||||
// ----- COLOR FILTER -----
|
||||
// If this pixel on the template is a color the user wants to hide on the canvas...
|
||||
if (this.shouldFilterColor.get(bestTemplateColorID)) {
|
||||
|
|
@ -857,6 +925,39 @@ export default class TemplateManager {
|
|||
}
|
||||
// ----- END OF ERASED -----
|
||||
|
||||
// ----- HIGHLIGHTING -----
|
||||
|
||||
// If highlighting is enabled, AND the template pixel is NOT transparent AND the template pixel does NOT match the tile pixel
|
||||
if (!highlightDisabled && (templatePixelAlpha > tolerance) && (bestTileColorID != bestTemplateColorID)) {
|
||||
|
||||
// If the tile pixel is NOT transparent, OR the user wants to highlight transparent pixels
|
||||
if (shouldTransparentTilePixelsBeHighlighted || (tilePixelAlpha > tolerance)) {
|
||||
|
||||
// Obtains the template color of this pixel
|
||||
const templatePixelColor = template32[(templateRow * templateWidth) + templateColumn];
|
||||
// This will retrieve the tile background instead if the color is filtered!
|
||||
|
||||
// For each of the 9 subpixels inside the pixel...
|
||||
for (const subpixelPattern of highlightPattern) {
|
||||
|
||||
// Deconstructs the sub pixel
|
||||
const [subpixelState, subpixelColumnDelta, subpixelRowDelta] = subpixelPattern;
|
||||
// "Delta" because the coordinate of the sub-pixel is relative to the center of the pixel
|
||||
|
||||
// Obtains the subpixel color to use
|
||||
const subpixelColor = (subpixelState != 0) ? ((subpixelState != 1) ? templatePixelColor : 0xFF0000FF) : 0x00000000;
|
||||
// 0 = Transparent (black)
|
||||
// 1 = Red (#FF0000)
|
||||
// 2 = Template (matches template or hides if filtered)
|
||||
|
||||
// Sets the subpixel to match the color on the highlight pattern
|
||||
template32[((templateRow + subpixelRowDelta) * templateWidth) + (templateColumn + subpixelColumnDelta)] = subpixelColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- END OF HIGHLIGHTING -----
|
||||
|
||||
// If the template pixel is Erased, and the tile pixel is transparent...
|
||||
if ((bestTemplateColorID == -1) && (tilePixelAbove <= tolerance)) {
|
||||
|
||||
|
|
@ -874,11 +975,10 @@ export default class TemplateManager {
|
|||
}
|
||||
// If the code passes this point, both pixels are opaque & not Erased.
|
||||
|
||||
// Finds the best matching color ID for the tile pixel. If none is found, default to "-2"
|
||||
const bestTileColorID = lookupTable.get(tilePixelAbove) ?? -2;
|
||||
|
||||
// If the template pixel does not match the tile pixel, then the pixel is skipped.
|
||||
if (bestTileColorID != bestTemplateColorID) {continue;}
|
||||
// If the template pixel does not match the tile pixel, then the pixel is skipped after highlighting.
|
||||
if (bestTileColorID != bestTemplateColorID) {
|
||||
continue;
|
||||
}
|
||||
// If the code passes this point, the template pixel matches the tile pixel.
|
||||
|
||||
// Increments the count by 1 for the best matching color ID (which can be negative).
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function getWplaceVersion() {
|
|||
|
||||
/** Halts execution of this specific userscript, for the specified time.
|
||||
* This will not block the thread.
|
||||
* @param {number} - Time to wait in milliseconds
|
||||
* @param {number} time - Time to wait in milliseconds
|
||||
* @since 0.88.483
|
||||
* @returns {Promise} Promise of a setTimeout()
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue