diff --git a/docs/ApiManager.html b/docs/ApiManager.html new file mode 100644 index 0000000..a0da020 --- /dev/null +++ b/docs/ApiManager.html @@ -0,0 +1,178 @@ + + + + + + ApiManager - Documentation + + + + + + + + + + + + + + + + + +
+ +

ApiManager

+ + + + + + + +
+ +
+ +

+ ApiManager +

+ + +
+ +
+
+ + +
+ + + +

new ApiManager()

+ + + + + +
+

ApiManager class for handling API requests, responses, and interactions. +Note: Fetch spying is done in main.js, not here.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.11.1
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/ConfettiManager.html b/docs/ConfettiManager.html new file mode 100644 index 0000000..c3837d9 --- /dev/null +++ b/docs/ConfettiManager.html @@ -0,0 +1,177 @@ + + + + + + ConfettiManager - Documentation + + + + + + + + + + + + + + + + + +
+ +

ConfettiManager

+ + + + + + + +
+ +
+ +

+ ConfettiManager +

+ + +
+ +
+
+ + +
+ + + +

new ConfettiManager()

+ + + + + +
+

Manages any confetti animation used by Blue Marble.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.356
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/Observers.html b/docs/Observers.html new file mode 100644 index 0000000..2e01443 --- /dev/null +++ b/docs/Observers.html @@ -0,0 +1,178 @@ + + + + + + Observers - Documentation + + + + + + + + + + + + + + + + + +
+ +

Observers

+ + + + + + + +
+ +
+ +

+ Observers +

+ + +
+ +
+
+ + +
+ + + +

new Observers()

+ + + + + +
+

This class contains all MutationObservers used (which is 1 probably). +This is not an object, but rather a "collection" of functions (in a class).

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/Overlay.html b/docs/Overlay.html new file mode 100644 index 0000000..810d3db --- /dev/null +++ b/docs/Overlay.html @@ -0,0 +1,202 @@ + + + + + + Overlay - Documentation + + + + + + + + + + + + + + + + + +
+ +

Overlay

+ + + + + + + +
+ +
+ +

+ Overlay +

+ + +
+ +
+
+ + +
+ + + +

new Overlay()

+ + + + + +
+

This class handles the overlay UI for the Blue Marble script.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.0.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
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>
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/Overlay.js.html b/docs/Overlay.js.html new file mode 100644 index 0000000..9eb49f4 --- /dev/null +++ b/docs/Overlay.js.html @@ -0,0 +1,1496 @@ + + + + + + Overlay.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

Overlay.js

+ + + + + + + +
+
+
/** The overlay builder for the Blue Marble script.
+ * @description This class handles the overlay UI for the Blue Marble script.
+ * @class Overlay
+ * @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
+
+    /** 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
+
+    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;}
+
+  /** 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.
+   * @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)) {
+      this.#applyAttribute(element, property, value);
+    }
+
+    // For every passed in additional property, apply the it to the element
+    for (const [property, value] of Object.entries(additionalProperties)) {
+      this.#applyAttribute(element, property, value);
+    }
+    
+    return element;
+  }
+
+  /** Applies an attribute to an element
+   * @param {HTMLElement} element - The element to apply the attribute to
+   * @param {String} property - The name of the attribute to apply
+   * @param {String} value - The value of the attribute
+   * @since 0.88.136
+   */
+  #applyAttribute(element, property, value) {
+    if (property == 'class') {
+      element.classList.add(...value.split(/\s+/)); // converts `'foo bar'` to `'foo', 'bar'` which is accepted
+    } else if (property == 'for') {
+      element.htmlFor = value;
+    } else if (property == 'tabindex') {
+      element.tabIndex = Number(value);
+    } else if (property == 'readonly') {
+      element.readOnly = ((value == 'true') || (value == '1'));
+    } else if (property == 'maxlength') {
+      element.maxLength = Number(value);
+    } else if (property.startsWith('data')) {
+      element.dataset[
+        property.slice(5).split('-').map(
+          (part, i) => (i == 0) ? part : part[0].toUpperCase() + part.slice(1)
+        ).join('')
+      ] = value;
+    } else if (property.startsWith('aria')) {
+      element.setAttribute(property, value); // We can't do the solution for 'data', as 'aria-labelledby' would fail to apply
+    } else {
+      element[property] = value;
+    }
+  }
+
+  /** 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, HTMLElement):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 `span` to the overlay.
+   * This `span` element will have properties shared between all `span` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `span` that are NOT shared between all overlay `span` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLSpanElement):void} [callback=()=>{}] - Additional JS modification to the `span`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.55.8
+   * @example
+   * // Assume all <span> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addSpan({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <span id="foo" class="bar">Foobar.</span>
+   * </body>
+   */
+  addSpan(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <span> DOM properties
+
+    const span = this.#createElement('span', properties, additionalProperties); // Creates the <span> element
+    callback(this, span); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a `details` to the overlay.
+   * This `details` element will have properties shared between all `details` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `details` that are NOT shared between all overlay `details` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLDetailsElement):void} [callback=()=>{}] - Additional JS modification to the `details`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.96
+   * @example
+   * // Assume all <details> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addDetails({'id': 'foo'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <details id="foo" class="bar"></details>
+   * </body>
+   */
+  addDetails(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <details> DOM properties
+
+    const details = this.#createElement('details', properties, additionalProperties); // Creates the <details> element
+    callback(this, details); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a `summary` to the overlay.
+   * This `summary` element will have properties shared between all `summary` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `summary` that are NOT shared between all overlay `summary` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLElement):void} [callback=()=>{}] - Additional JS modification to the `summary`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.96
+   * @example
+   * // Assume all <summary> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addSummary({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <summary id="foo" class="bar">Foobar.</summary>
+   * </body>
+   */
+  addSummary(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <summary> DOM properties
+
+    const summary = this.#createElement('summary', properties, additionalProperties); // Creates the <summary> element
+    callback(this, summary); // 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 `form` to the overlay.
+   * This `form` element will have properties shared between all `form` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `form` that are NOT shared between all overlay `form` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLFormElement):void} [callback=()=>{}] - Additional JS modification to the `form`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.246
+   * @example
+   * // Assume all <form> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addForm({'id': 'foo'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <form id="foo" class="bar"></form>
+   * </body>
+   */
+  addForm(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <form> DOM properties
+
+    const form = this.#createElement('form', properties, additionalProperties); // Creates the <form> element
+    callback(this, form); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a `fieldset` to the overlay.
+   * This `fieldset` element will have properties shared between all `fieldset` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `fieldset` that are NOT shared between all overlay `fieldset` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLFieldSetElement):void} [callback=()=>{}] - Additional JS modification to the `fieldset`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.246
+   * @example
+   * // Assume all <fieldset> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addFieldset({'id': 'foo'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <fieldset id="foo" class="bar"></fieldset>
+   * </body>
+   */
+  addFieldset(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <fieldset> DOM properties
+
+    const fieldset = this.#createElement('fieldset', properties, additionalProperties); // Creates the <fieldset> element
+    callback(this, fieldset); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a `legend` to the overlay.
+   * This `legend` element will have properties shared between all `legend` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `legend` that are NOT shared between all overlay `legend` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLLegendElement):void} [callback=()=>{}] - Additional JS modification to the `legend`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.246
+   * @example
+   * // Assume all <legend> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addLegend({'id': 'foo', textContent: 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <legend id="foo" class="bar">
+   *     "Foobar."
+   *   </legend>
+   * </body>
+   */
+  addLegend(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <legend> DOM properties
+
+    const legend = this.#createElement('legend', properties, additionalProperties); // Creates the <legend> element
+    callback(this, legend); // 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
+
+    // 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
+    callback(this, label, checkbox); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a label & select element to the overlay.
+   * This select element will have properties shared between all select elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the checkbox that are NOT shared between all overlay select elements. These should be camelCase.
+   * @param {function(Overlay, HTMLLabelElement, HTMLSelectElement):void} [callback=()=>{}] - Additional JS modification to the label/select elements.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.243
+   * @example
+   * // Assume all select elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addSelect({'id': 'foo', 'textContent': 'Foobar: '}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <label for="foo">
+   *     "Foobar: "
+   *   </label>
+   *   <select id="foo" class="bar"></select>
+   * </body>
+   */
+  addSelect(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared select DOM properties
+
+    const label = this.#createElement('label', {'textContent': additionalProperties['textContent'] ?? '', 'for': additionalProperties['id'] ?? ''}); // Creates the label element
+    delete additionalProperties['textContent']; // Deletes 'textContent' DOM property before adding the properties to the select
+    this.buildElement(); // Signifies that we are done adding children to the label
+    
+    const select = this.#createElement('select', properties, additionalProperties); // Creates the select element
+    callback(this, label, select); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds an option to the overlay.
+   * This `option` element will have properties shared between all `option` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `option` that are NOT shared between all overlay `option` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLOptionElement):void} [callback=()=>{}] - Additional JS modification to the `option`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.244
+   * @example
+   * // Assume all <option> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addOption({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <option id="foo" class="bar">Foobar.</option>
+   * </body>
+   */
+  addOption(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <option> DOM properties
+
+    const option = this.#createElement('option', properties, additionalProperties); // Creates the <option> element
+    callback(this, option); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds an ordered list to the overlay.
+   * This `ol` element will have properties shared between all `ol` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `ol` that are NOT shared between all overlay `ol` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLOListElement):void} [callback=()=>{}] - Additional JS modification to the `ol`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <ol> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addOl({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <ol id="foo" class="bar">Foobar.</ol>
+   * </body>
+   */
+  addOl(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <ol> DOM properties
+
+    const ol = this.#createElement('ol', properties, additionalProperties); // Creates the <ol> element
+    callback(this, ol); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds an unordered list to the overlay.
+   * This `ul` element will have properties shared between all `ul` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `ul` that are NOT shared between all overlay `ul` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLUListElement):void} [callback=()=>{}] - Additional JS modification to the `ul`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <ul> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addUl({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <ul id="foo" class="bar">Foobar.</ul>
+   * </body>
+   */
+  addUl(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <ul> DOM properties
+
+    const ul = this.#createElement('ul', properties, additionalProperties); // Creates the <ul> element
+    callback(this, ul); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a `menu` to the overlay.
+   * This `menu` element will have properties shared between all `menu` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `menu` that are NOT shared between all overlay `menu` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLMenuElement):void} [callback=()=>{}] - Additional JS modification to the `menu`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <menu> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addMenu({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <menu id="foo" class="bar">Foobar.</menu>
+   * </body>
+   */
+  addMenu(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <menu> DOM properties
+
+    const menu = this.#createElement('menu', properties, additionalProperties); // Creates the <menu> element
+    callback(this, menu); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a list item to the overlay.
+   * This `li` element will have properties shared between all `li` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `li` that are NOT shared between all overlay `li` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLLIElement):void} [callback=()=>{}] - Additional JS modification to the `li`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <li> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addLi({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <li id="foo" class="bar">Foobar.</li>
+   * </body>
+   */
+  addLi(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <li> DOM properties
+
+    const li = this.#createElement('li', properties, additionalProperties); // Creates the <li> element
+    callback(this, li); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a table to the overlay.
+   * This `table` element will have properties shared between all `table` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `table` that are NOT shared between all overlay `table` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLTableElement):void} [callback=()=>{}] - Additional JS modification to the `table`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <table> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addTable({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <table id="foo" class="bar">Foobar.</table>
+   * </body>
+   */
+  addTable(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <table> DOM properties
+
+    const table = this.#createElement('table', properties, additionalProperties); // Creates the <table> element
+    callback(this, table); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a table caption to the overlay.
+   * This `caption` element will have properties shared between all `caption` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `caption` that are NOT shared between all overlay `caption` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLTableCaptionElement):void} [callback=()=>{}] - Additional JS modification to the `caption`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <caption> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addCaption({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <caption id="foo" class="bar">Foobar.</caption>
+   * </body>
+   */
+  addCaption(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <caption> DOM properties
+
+    const caption = this.#createElement('caption', properties, additionalProperties); // Creates the <caption> element
+    callback(this, caption); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a table header to the overlay.
+   * This `thead` element will have properties shared between all `thead` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `thead` that are NOT shared between all overlay `thead` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLTableSectionElement):void} [callback=()=>{}] - Additional JS modification to the `thead`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <thead> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addThead({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <thead id="foo" class="bar">Foobar.</thead>
+   * </body>
+   */
+  addThead(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <thead> DOM properties
+
+    const thead = this.#createElement('thead', properties, additionalProperties); // Creates the <thead> element
+    callback(this, thead); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a table body to the overlay.
+   * This `tbody` element will have properties shared between all `tbody` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `tbody` that are NOT shared between all overlay `tbody` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLTableSectionElement):void} [callback=()=>{}] - Additional JS modification to the `tbody`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <tbody> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addTbody({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <tbody id="foo" class="bar">Foobar.</tbody>
+   * </body>
+   */
+  addTbody(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <tbody> DOM properties
+
+    const tbody = this.#createElement('tbody', properties, additionalProperties); // Creates the <tbody> element
+    callback(this, tbody); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a table footer to the overlay.
+   * This `tfoot` element will have properties shared between all `tfoot` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `tfoot` that are NOT shared between all overlay `tfoot` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLTableSectionElement):void} [callback=()=>{}] - Additional JS modification to the `tfoot`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <tfoot> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addTfoot({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <tfoot id="foo" class="bar">Foobar.</tfoot>
+   * </body>
+   */
+  addTfoot(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <tfoot> DOM properties
+
+    const tfoot = this.#createElement('tfoot', properties, additionalProperties); // Creates the <tfoot> element
+    callback(this, tfoot); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a table row to the overlay.
+   * This `tr` element will have properties shared between all `tr` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `tr` that are NOT shared between all overlay `tr` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLTableRowElement):void} [callback=()=>{}] - Additional JS modification to the `tr`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <tr> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addTr({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <tr id="foo" class="bar">Foobar.</tr>
+   * </body>
+   */
+  addTr(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <tfoot> DOM properties
+
+    const tr = this.#createElement('tr', properties, additionalProperties); // Creates the <tr> element
+    callback(this, tr); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a table header (label) cell to the overlay.
+   * This `th` element will have properties shared between all `th` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `th` that are NOT shared between all overlay `th` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLTableCellElement):void} [callback=()=>{}] - Additional JS modification to the `th`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <th> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addTh({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <th id="foo" class="bar">Foobar.</th>
+   * </body>
+   */
+  addTh(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <th> DOM properties
+
+    const th = this.#createElement('th', properties, additionalProperties); // Creates the <th> element
+    callback(this, th); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a table data cell to the overlay.
+   * This `td` element will have properties shared between all `td` elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the `td` that are NOT shared between all overlay `td` elements. These should be camelCase.
+   * @param {function(Overlay, HTMLTableCellElement):void} [callback=()=>{}] - Additional JS modification to the `td`.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.180
+   * @example
+   * // Assume all <td> elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addTd({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <td id="foo" class="bar">Foobar.</td>
+   * </body>
+   */
+  addTd(additionalProperties = {}, callback = () => {}) {
+
+    const properties = {}; // Shared <td> DOM properties
+
+    const td = this.#createElement('td', properties, additionalProperties); // Creates the <td> element
+    callback(this, td); // 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', 
+      'tabindex': '-1',
+      'aria-hidden': 'true'
+    }; // 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
+    
+    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;
+  }
+
+  /** Adds a dragbar `div` element to the overlay.
+   * This dragbar element will have properties shared between all dragbar elements in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the dragbar that are NOT shared between all overlay dragbars. These should be camelCase.
+   * @param {function(Overlay, HTMLDivElement):void} [callback=()=>{}] - Additional JS modification to the dragbar.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.145
+   * @example
+   * // Assume all dragbar elements have a shared class (e.g. {'className': 'bar'})
+   * overlay.addDragbar({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <div id="foo" class="bar">Foobar.</div>
+   * </body>
+   */
+  addDragbar(additionalProperties = {}, callback = () => {}) {
+
+    // Shared dragbar DOM properties
+    const properties = {
+      'class': 'bm-dragbar'
+    };
+
+    const dragbar = this.#createElement('div', properties, additionalProperties); // Creates the dragbar element
+    callback(this, dragbar); // Runs any script passed in through the callback
+    return this;
+  }
+
+  /** Adds a timer `time` element to the overlay.
+   * This timer will countdown until it reaches the end date that was passed in.
+   * Additionally, you can update the end date by changing the endDate dataset attribute on the element.
+   * Timer elements are not localized. Output is HH:MM:SS with no units.
+   * This timer will have properties shared between all timers in the overlay.
+   * You can override the shared properties by using a callback.
+   * @param {Date} [endDate=Date.now()] - The time to count down to.
+   * @param {number} [updateInterval=500] - The time in milliseconds to update the display of the timer. Default is 500 milliseconds.
+   * @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the timer that are NOT shared between all overlay timers. These should be camelCase.
+   * @param {function(Overlay, HTMLTimeElement):void} [callback=()=>{}] - Additional JS modification to the timer.
+   * @returns {Overlay} Overlay class instance (this)
+   * @since 0.88.313
+   * @example
+   * // Assume all timers have a shared class (e.g. {'className': 'bar'})
+   * overlay.addTimer(Date.now() + 2211632704000, 500, {'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+   * // Output:
+   * // (Assume <body> already exists in the webpage)
+   * <body>
+   *   <time id="bm-timer-dh8fhw80" class="bar" datetime="PT27H34M56S" data-end-date="1771749296000">27:34:56</div>
+   * </body>
+   */
+  addTimer(endDate = Date.now(), updateInterval = 500, additionalProperties = {}, callback = () => {}) {
+
+    const timerClass = 'bm-timer';
+
+    // Use the provided ID attribute. Otherwise, generate a random one.
+    // "Random" ID is 8 pseudo-random hexdecimal characters from a UUIDv4 string
+    // The ID has a prefix that is equal to the bm-timer class, then a dash, then the random hexdecimal digits
+    const timerID = additionalProperties?.['id'] || (timerClass + '-' + crypto.randomUUID().slice(0, 8));
+
+    // Shared timer DOM properties
+    const properties = {
+      'class': timerClass
+    }
+
+    const timer = this.#createElement('time', properties, additionalProperties); // Creates the timer element
+    timer.id = timerID; // Adds the ID to the timer
+    timer.dataset['endDate'] = endDate; // Adds the end date to the timer
+
+    // Creates the logic that keeps updating the timer
+    setInterval(() => {
+
+      // Kills the timer logic if the timer element does not exist in the main DOM tree
+      if (!timer.isConnected) {/*clearInterval(timer);*/ return;}
+
+      // Returns time remaining in seconds, or 0 seconds if timer has reached end time.
+      // "Total" indicates it is the total time for that unit. E.g. 62 minutes is "62" minutes.
+      const timeRemainingTotalMs = Math.max(timer.dataset['endDate'] - Date.now(), 0);
+      const timeRemainingTotalSec = Math.floor(timeRemainingTotalMs / 1000);
+      const timeRemainingTotalHr = Math.floor(timeRemainingTotalSec / 3600);
+      
+      // Remaining time in certain units.
+      // "Only" indicates it is formatted in that unit. E.g. 62 minutes is "2" minutes.
+      const timeRemainingOnlySec = Math.floor(timeRemainingTotalSec % 60);
+      const timeRemainingOnlyMin = Math.floor((timeRemainingTotalSec % 3600) / 60);
+
+      // Date-time string that is compliant with the HTML Standard for durations
+      timer.setAttribute('datetime', `PT${timeRemainingTotalHr}H${timeRemainingOnlyMin}M${timeRemainingOnlySec}S`);
+      // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-duration-string
+
+      // Formats the timer as HH:MM:SS and displays it to the user
+      timer.textContent = 
+        String(timeRemainingTotalHr).padStart(2, '0') + ':' +
+        String(timeRemainingOnlyMin).padStart(2, '0') + ':' +
+        String(timeRemainingOnlySec).padStart(2, '0')
+      ;
+    }, updateInterval);
+
+    callback(this, timer); // 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 the minimization logic for windows spawned by Blue Marble
+   * @param {HTMLButtonElement} button - The UI button that triggered this minimization event
+   * @since 0.88.142
+  */
+  handleMinimization(button) {
+
+    if (button.disabled) {return;} // Don't minimize if the window is currently minimizing
+
+    button.disabled = true; // Disables the button until the transition ends
+    button.style.textDecoration = 'none'; // Disables the disabled button text decoration strikethrough line
+
+    const window = button.closest('.bm-window'); // Get the window
+    const dragbar = button.closest('.bm-dragbar'); // Get the dragbar
+    const header = window.querySelector('h1'); // Get the header
+    const windowContent = window.querySelector('.bm-window-content'); // Get the window content container
+
+    window.parentElement.append(window); // Moves the window to the top
+
+    // If window content is open...
+    if (button.dataset['buttonStatus'] == 'expanded') {
+      // ...we want to close it
+      
+      // Logic for the transition animation to collapse the window
+      windowContent.style.height = windowContent.scrollHeight + 'px';
+      window.style.width = window.scrollWidth + 'px'; // So the width of the window does not change due to the lack of content
+      windowContent.style.height = '0'; // Set the height to 0px
+      windowContent.addEventListener('transitionend', function handler() { // Add an event listener to cleanup once the minimize transition is complete
+        windowContent.style.display = 'none'; // Changes "display" to "none" for screen readers
+        button.disabled = false; // Enables the button
+        button.style.textDecoration = ''; // Resets the text decoration to default
+        windowContent.removeEventListener('transitionend', handler); // Removes the event listener
+      });
+      
+      // Makes a clone of the h1 element inside the window, and adds it to the dragbar
+      const dragbarHeader1 = header.cloneNode(true);
+      const dragbarHeader1Text = dragbarHeader1.textContent;
+      button.nextElementSibling.appendChild(dragbarHeader1);
+      
+      button.textContent = '▶'; // Swap button icon
+      button.dataset['buttonStatus'] = 'collapsed'; // Swap button status tracker
+      button.ariaLabel = `Unminimize window "${dragbarHeader1Text}"`; // Screen reader label
+    } else {
+      // Else, the window is closed, and we want to open it
+
+      // Deletes the h1 element inside the dragbar
+      const dragbarHeader1 = dragbar.querySelector('h1');
+      const dragbarHeader1Text = dragbarHeader1.textContent;
+      dragbarHeader1.remove();
+
+      // Logic for the transition animation to expand the window
+      windowContent.style.display = ''; // Resets display to default
+      windowContent.style.height = '0'; // Sets the height to 0
+      window.style.width = ''; // Resets the window width to default
+      windowContent.style.height = windowContent.scrollHeight + 'px'; // Change the height back to normal
+      windowContent.addEventListener('transitionend', function handler() { // Add an event listener to cleanup once the minimize transition is complete
+        windowContent.style.height = ''; // Changes the height back to default
+        button.disabled = false; // Enables the button
+        button.style.textDecoration = ''; // Resets the text decoration to default
+        windowContent.removeEventListener('transitionend', handler); // Removes the event listener
+      });
+
+      button.textContent = '▼'; // Swap button icon
+      button.dataset['buttonStatus'] = 'expanded'; // Swap button status tracker
+      button.ariaLabel = `Minimize window "${dragbarHeader1Text}"`; // Screen reader label
+    }
+  }
+
+  /** Handles dragging of the overlay.
+   * Uses requestAnimationFrame for smooth animations and GPU-accelerated transforms.
+   * Make sure to use the appropriate CSS selectors.
+   * @param {string} moveMeSelector - The element to be moved
+   * @param {string} iMoveThingsSelector - The drag handle element
+   * @since 0.8.2
+  */
+  handleDrag(moveMeSelector, iMoveThingsSelector) {
+
+    // Retrieves the elements
+    const moveMe = document.querySelector(moveMeSelector);
+    const iMoveThings = document.querySelector(iMoveThingsSelector);
+    
+    // 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
+    }
+
+    let isDragging = false;
+    let offsetX, offsetY = 0;
+    let animationFrame = null;
+    let currentX = 0;
+    let currentY = 0;
+    let targetX = 0;
+    let targetY = 0;
+    let initialRect = null; // Cache initial position to avoid expensive getBoundingClientRect calls during drag
+
+    // 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);
+      }
+    };
+    
+    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('bm-dragging');
+
+      // Add move listeners when dragging starts
+      document.addEventListener('mousemove', onMouseMove);
+      document.addEventListener('touchmove', onTouchMove, { passive: false });
+      document.addEventListener('mouseup', endDrag);
+      document.addEventListener('touchend', endDrag);
+      document.addEventListener('touchcancel', endDrag);
+      
+      // 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('bm-dragging');
+
+      // Remove move listeners when drag ends
+      document.removeEventListener('mousemove', onMouseMove);
+      document.removeEventListener('touchmove', onTouchMove);
+      document.removeEventListener('mouseup', endDrag);
+      document.removeEventListener('touchend', endDrag);
+      document.removeEventListener('touchcancel', endDrag);
+    };
+
+    // Mouse move
+    const onMouseMove = event => {
+      if (isDragging && initialRect) {
+        targetX = event.clientX - offsetX;
+        targetY = event.clientY - offsetY;
+      }
+    }
+
+    // Touch move
+    const onTouchMove = event => {
+      if (isDragging && initialRect) {
+        const touch = event.touches[0];
+        if (!touch) return;
+        targetX = touch.clientX - offsetX;
+        targetY = touch.clientY - offsetY;
+        event.preventDefault();
+      }
+    };
+
+    // 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 });
+  }
+
+  /** 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
+  }
+}
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/SettingsManager.html b/docs/SettingsManager.html new file mode 100644 index 0000000..56cdc11 --- /dev/null +++ b/docs/SettingsManager.html @@ -0,0 +1,196 @@ + + + + + + SettingsManager - Documentation + + + + + + + + + + + + + + + + + +
+ +

SettingsManager

+ + + + + + + +
+ +
+ +

+ SettingsManager +

+ + +
+ +
+
+ + +
+ + + +

new SettingsManager()

+ + + + + +
+

SettingsManager class for handling user settings and making them persist between sessions. +Logic for 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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.11
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
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]
+}
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/Template.html b/docs/Template.html new file mode 100644 index 0000000..86c3cbf --- /dev/null +++ b/docs/Template.html @@ -0,0 +1,178 @@ + + + + + + Template - Documentation + + + + + + + + + + + + + + + + + +
+ +

Template

+ + + + + + + +
+ +
+ +

+ Template +

+ + +
+ +
+
+ + +
+ + + +

new Template()

+ + + + + +
+

An instance of a template. +Handles all mathematics, manipulation, and analysis regarding a single template.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.65.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/Template.js.html b/docs/Template.js.html new file mode 100644 index 0000000..d5def7f --- /dev/null +++ b/docs/Template.js.html @@ -0,0 +1,458 @@ + + + + + + Template.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

Template.js

+ + + + + + + +
+
+
import { sleep, uint8ToBase64, viewCanvasInNewTab } from "./utils";
+
+/** An instance of a template.
+ * Handles all mathematics, manipulation, and analysis regarding a single template.
+ * @class 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, number, number, 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 as a bitmap
+   * @param {Object} [params.chunked32={}] - The affected chunks of the template, and their template for each chunk as a Uint32Array
+   * @param {number} [params.tileSize=1000] - The size of a tile in pixels (assumes square tiles)
+   * @param {Object} [params.pixelCount={total:0, colors:Map}] - 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,
+    chunked32 = {},
+    tileSize = 1000,
+  } = {}) {
+    this.displayName = displayName;
+    this.sortID = sortID;
+    this.authorID = authorID;
+    this.url = url;
+    this.file = file;
+    this.coords = coords;
+    this.chunked = chunked;
+    this.chunked32 = chunked32;
+    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, 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;
+    const imageHeight = bitmap.height;
+
+    this.tileSize = tileSize; // Tile size predetermined by the templateManager
+
+    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;
+    canvas.height = imageHeight;
+    context.imageSmoothingEnabled = false; // Nearest neighbor
+
+    context.drawImage(bitmap, 0, 0); // Draws the template to the canvas
+
+    let timer = Date.now();
+    const totalPixelMap = this.#calculateTotalPixelsFromImageData(context.getImageData(0, 0, imageWidth, imageHeight), paletteBM); // Calculates total pixels from the template buffer retrieved from the canvas context image data
+    console.log(`Calculating total pixels took ${(Date.now() - timer) / 1000.0} seconds`);
+
+    let totalPixels = 0; // Will store the total amount of non-Transparent color pixels
+    const transparentColorID = 0; // Color ID for the Transparent color
+
+    // For each color in the total pixel Map...
+    for (const [color, total] of totalPixelMap) {
+
+      if (color == transparentColorID) {continue;} // Skip Transparent color
+
+      totalPixels += total; // Adds the total amount for the pixel color to the total amount for all colors
+    }
+
+    this.pixelCount = { total: totalPixels, colors: totalPixelMap }; // Stores the total pixel count in the Template instance
+
+    timer = Date.now();
+
+    // Creates a mask where the middle pixel is white, and everything else is transparent
+    const canvasMask = new OffscreenCanvas(3, 3);
+    const contextMask = canvasMask.getContext("2d");
+    contextMask.clearRect(0, 0, 3, 3);
+    contextMask.fillStyle = "white";
+    contextMask.fillRect(1, 1, 1, 1);
+
+    // 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]));
+
+        // 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}`);
+
+        // 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
+
+        context.save(); // Saves the current context of the canvas
+        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);
+
+        context.restore(); // Restores the context of the canvas to the previous save
+
+        const imageData = context.getImageData(0, 0, canvasWidth, canvasHeight); // Data of the image on the canvas
+
+        console.log(`Shreaded pixels for ${pixelX}, ${pixelY}`, imageData);
+
+        // 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')
+        }`;
+
+        this.chunked32[templateTileName] = new Uint32Array(imageData.data.buffer); // Creates the Uint32Array
+
+        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(`Parsing template took ${(Date.now() - timer) / 1000.0} seconds`);
+    console.log('Template Tiles: ', templateTiles);
+    console.log('Template Tiles Buffers: ', templateTilesBuffers);
+    console.log('Template Tiles Uint32Array: ', this.chunked32);
+    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
+   */
+  calculateCoordsFromChunked() {
+    let topLeftCoord = [Infinity, Infinity, Infinity, Infinity];
+    const tileKeys = Object.keys(this.chunked).sort(); // Sorts the tile keys
+    tileKeys.forEach((key, index) => { // For each tile key...
+      const [tileX, tileY, pixelX, pixelY] = key.split(',').map(Number); // Deconstruct the tile key
+      if ((tileY < topLeftCoord[1]) || (tileY == topLeftCoord[1] && tileX < topLeftCoord[0])) {
+        topLeftCoord = [tileX, tileY, pixelX, pixelY]; // Record the smallest tile key coordinates. Otherwise, use previous best
+      }
+    });
+    this.coords = topLeftCoord;
+  }
+
+  /** Calculates the total pixels for each color for the image.
+   * 
+   * @param {ImageData} imageData - The pre-shreaded image "casted" onto a canvas
+   * @param {Object} paletteBM - The palette Blue Marble uses for colors
+   * @param {Number} paletteTolerance - How close an RGB color has to be in order to be considered a palette color. A tolerance of "3" means the sum of the RGB can be up to 3 away from the actual value.
+   * @returns {Map<Number, Number>} A map where the key is the color ID, and the value is the total pixels for that color ID
+   * @since 0.88.6
+   */
+  #calculateTotalPixelsFromImageData(imageData, paletteBM) {
+
+    const buffer32Arr = new Uint32Array(imageData.data.buffer); // RGB values as a Uint32Array. Each index represents 1 pixel.
+    const { palette: _, LUT: lookupTable } = 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
+    const _colorpalette = new Map(); // Temp color palette
+
+    // For every pixel...
+    for (let pixelIndex = 0; pixelIndex < buffer32Arr.length; pixelIndex++) {
+      
+      const pixel = buffer32Arr[pixelIndex]; // Current pixel to check
+      let bestColorID = -2; // Will eventually store the best match for color ID
+
+      // If the pixel is transparent...
+      if ((pixel >>> 24) == 0) {
+        bestColorID = 0; // Set the color ID to 0
+      } else {
+        // Else, look up the color ID in the "cube" LUT. If none is found, fallback to -2 ("Other")
+        bestColorID = lookupTable.get(pixel) ?? -2;
+      }
+
+      // Increments the count by 1 for the best matching color ID (which can be negative).
+      // If the color ID has not been counted yet, default to 1
+      const colorIDcount = _colorpalette.get(bestColorID);
+      _colorpalette.set(bestColorID, colorIDcount ? colorIDcount + 1 : 1);
+    }
+
+    console.log(_colorpalette);
+    return _colorpalette;
+  }
+}
+
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/TemplateManager.html b/docs/TemplateManager.html new file mode 100644 index 0000000..911559f --- /dev/null +++ b/docs/TemplateManager.html @@ -0,0 +1,256 @@ + + + + + + TemplateManager - Documentation + + + + + + + + + + + + + + + + + +
+ +

TemplateManager

+ + + + + + + +
+ +
+ +

+ TemplateManager +

+ + +
+ +
+
+ + +
+ + + +

new TemplateManager()

+ + + + + +
+

Manages the template system. +This class handles all external requests for template modification, creation, and analysis. +It serves as the central coordinator between template instances and the user interface.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.55.8
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
Examples
+ +
// JSON structure for a template made in schema version 2.0.0.
+// Note: The pixel "colors" Object contains more than 2 keys.
+// Note: The template tiles are stored as base64 PNG images.
+{
+  "whoami": "BlueMarble",
+  "scriptVersion": "1.13.0",
+  "schemaVersion": "2.0.0",
+  "templates": {
+    "0 $Z": {
+      "name": "My Template",
+      "enabled": true,
+      "pixels": {
+        "total": 40399,
+        "colors": {
+          "-2": 40000,
+          "0": 399
+        }
+      }
+      "tiles": {
+        "1231,0047,183,593": "iVBORw0KGgoAAAANSUhEUgAA",
+        "1231,0048,183,000": "AAAFCAYAAACNbyblAAAAHElEQVQI12P4"
+      }
+    },
+    "1 $Z": {
+      "name": "My Template",
+      "URL": "https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/dist/assets/Favicon.png",
+      "URLType": "template",
+      "enabled": false,
+      "pixels": {
+        "total": 40399,
+        "colors": {
+          "-2": 40000,
+          "0": 399
+        }
+      }
+      "tiles": {
+        "375,1846,276,188": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA",
+        "376,1846,000,188": "data:image/png;AAAFCAYAAACNbyblAAAAHElEQVQI12P4"
+      }
+    }
+  }
+}
+ +
// JSON structure for a template made in schema version 1.0.0.
+// Note: The template tiles are stored as base64 PNG images.
+{
+  "whoami": "BlueMarble",
+  "scriptVersion": "1.13.0",
+  "schemaVersion": "1.0.0",
+  "templates": {
+    "0 $Z": {
+      "name": "My Template",
+      "enabled": true,
+      "coords": "2000, 230, 45, 201"
+      "palette": {
+        "0,0,0": {
+           "count": 123,
+           "enabled": true
+        },
+        "255,255,255": {
+           "count": 1315,
+           "enabled": false
+        }
+      }
+      "tiles": {
+        "1231,0047,183,593": "iVBORw0KGgoAAAANSUhEUgAA",
+        "1231,0048,183,000": "AAAFCAYAAACNbyblAAAAHElEQVQI12P4"
+      }
+    }
+  }
+}
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/WindowCredits.html b/docs/WindowCredits.html new file mode 100644 index 0000000..9139816 --- /dev/null +++ b/docs/WindowCredits.html @@ -0,0 +1,184 @@ + + + + + + WindowCredits - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowCredits

+ + + + + + + +
+ +
+ +

+ WindowCredits +

+ + +
+ +
+
+ + +
+ + + +

new WindowCredits()

+ + + + + +
+

Manages the credits window for Blue Marble.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.90.9
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/WindowCredits.js.html b/docs/WindowCredits.js.html new file mode 100644 index 0000000..c088b53 --- /dev/null +++ b/docs/WindowCredits.js.html @@ -0,0 +1,178 @@ + + + + + + WindowCredits.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowCredits.js

+ + + + + + + +
+
+
import Overlay from "./Overlay";
+import { localizeDate } from "./utils";
+
+/** Manages the credits window for Blue Marble.
+ * @class WindowCredits
+ * @since 0.90.9
+ * @see {@link Overlay} for examples
+ */
+export default class WindowCredts extends Overlay {
+
+  /** Constructor for the Credits window
+   * @param {string} name - The name of the userscript
+   * @param {string} version - The version of the userscript
+   * @since 0.90.9
+   * @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-credits'; // The ID attribute for this window
+    this.windowParent = document.body; // The parent of the window DOM tree
+  }
+
+  /** Spawns a Credits window.
+   * If another credits window already exists, we DON'T spawn another!
+   * Parent/child relationships in the DOM structure below are indicated by indentation.
+   * @since 0.90.9
+   */
+  buildWindow() {
+
+    // ASCII art of "Blue Marble"
+    const ascii = `
+██████╗ ██╗     ██╗   ██╗███████╗
+██╔══██╗██║     ██║   ██║██╔════╝
+██████╔╝██║     ██║   ██║█████╗  
+██╔══██╗██║     ██║   ██║██╔══╝  
+██████╔╝███████╗╚██████╔╝███████╗
+╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝
+
+███╗   ███╗ █████╗ ██████╗ ██████╗ ██╗     ███████╗
+████╗ ████║██╔══██╗██╔══██╗██╔══██╗██║     ██╔════╝
+██╔████╔██║███████║██████╔╝██████╔╝██║     █████╗  
+██║╚██╔╝██║██╔══██║██╔══██╗██╔══██╗██║     ██╔══╝  
+██║ ╚═╝ ██║██║  ██║██║  ██║██████╔╝███████╗███████╗
+╚═╝     ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝ ╚══════╝╚══════╝
+`;
+
+    // If a credits window already exists, close it
+    if (document.querySelector(`#${this.windowID}`)) {
+      document.querySelector(`#${this.windowID}`).remove();
+      return;
+    }
+
+    // Creates a new credits window
+    this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window'}, (instance, div) => {})
+      .addDragbar()
+        .addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Credits"', '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
+        .addButton({'class': 'bm-button-circle', 'textContent': '✖', 'aria-label': 'Close window "Credits"'}, (instance, button) => {
+          button.onclick = () => {document.querySelector(`#${this.windowID}`)?.remove();};
+          button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
+        }).buildElement()
+      .buildElement()
+      .addDiv({'class': 'bm-window-content'})
+        .addDiv({'class': 'bm-container bm-center-vertically'})
+          .addHeader(1, {'textContent': 'Credits'}).buildElement()
+        .buildElement()
+        .addHr().buildElement()
+        .addDiv({'class': 'bm-container bm-scrollable'})
+          .addSpan({'role': 'img', 'aria-label': this.name})
+            .addSpan({'innerHTML': ascii, 'class': 'bm-ascii', 'aria-hidden': 'true'}).buildElement()
+          .buildElement()
+          .addBr().buildElement()
+          .addHr().buildElement()
+          .addBr().buildElement()
+          .addSpan({'textContent': '"Blue Marble" userscript is made by SwingTheVine.'}).buildElement()
+          .addBr().buildElement()
+          .addSpan({'innerHTML': 'The <a href="https://bluemarble.lol/" target="_blank" rel="noopener noreferrer">Blue Marble Website</a> is made by <a href="https://github.com/crqch" target="_blank" rel="noopener noreferrer">crqch</a>.'}).buildElement()
+          .addBr().buildElement()
+          .addSpan({'textContent': `The Blue Marble Website used until ${localizeDate(new Date(1756069320 * 1000))} was made by Camille Daguin.`}).buildElement()
+          .addBr().buildElement()
+          .addSpan({'textContent': 'The favicon "Blue Marble" is owned by NASA. (The image of the Earth is owned by NASA)'}).buildElement()
+          .addBr().buildElement()
+          .addSpan({'textContent': 'Special Thanks:'}).buildElement()
+          .addUl()
+            .addLi({'textContent': 'Espresso, Meqa, and Robot for moderating SwingTheVine\'s community.'}).buildElement()
+            .addLi({'innerHTML': 'nof, <a href="https://github.com/TouchedByDarkness" target="_blank" rel="noopener noreferrer">darkness</a> for creating similar userscripts!'}).buildElement()
+            .addLi({'innerHTML': '<a href="https://wondapon.net/" target="_blank" rel="noopener noreferrer">Wonda</a> for the Blue Marble banner image!'}).buildElement()
+            .addLi({'innerHTML': '<a href="https://github.com/BullStein" target="_blank" rel="noopener noreferrer">BullStein</a>, <a href="https://github.com/allanf181" target="_blank" rel="noopener noreferrer">allanf181</a> for being early beta testers!'}).buildElement()
+            .addLi({'innerHTML': 'guidu_ and <a href="https://github.com/Nick-machado" target="_blank" rel="noopener noreferrer">Nick-machado</a> for the original "Minimize" Button code!'}).buildElement()
+            .addLi({'innerHTML': 'Nomad and <a href="https://www.youtube.com/@gustav_vv" target="_blank" rel="noopener noreferrer">Gustav</a> for the tutorials!'}).buildElement()
+            .addLi({'innerHTML': '<a href="https://github.com/cfpwastaken" target="_blank" rel="noopener noreferrer">cfp</a> for creating the template overlay that Blue Marble was based on!'}).buildElement()
+            .addLi({'innerHTML': '<a href="https://forcenetwork.cloud/" target="_blank" rel="noopener noreferrer">Force Network</a> for hosting the <a href="https://github.com/SwingTheVine/Wplace-TelemetryServer" target="_blank" rel="noopener noreferrer">telemetry server</a>!'}).buildElement()
+            .addLi({'innerHTML': '<a href="https://thebluecorner.net" target="_blank" rel="noopener noreferrer">TheBlueCorner</a> for getting me interested in online pixel canvases!'}).buildElement()
+          .buildElement()
+          .addBr().buildElement()
+          .addSpan({'innerHTML': '<a href="https://ko-fi.com/swingthevine" target="_blank" rel="noopener noreferrer">Donators</a>:'}).buildElement()
+          .addUl()
+            .addLi({'textContent': 'Soultree'}).buildElement()
+            .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': '2 Anonymous Supporters'}).buildElement()
+          .buildElement()
+        .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`);
+  }
+}
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/WindowFilter.html b/docs/WindowFilter.html new file mode 100644 index 0000000..d495d93 --- /dev/null +++ b/docs/WindowFilter.html @@ -0,0 +1,184 @@ + + + + + + WindowFilter - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowFilter

+ + + + + + + +
+ +
+ +

+ WindowFilter +

+ + +
+ +
+
+ + +
+ + + +

new WindowFilter()

+ + + + + +
+

This class handles the overlay UI for the color filter window of the Blue Marble userscript.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.329
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/WindowFilter.js.html b/docs/WindowFilter.js.html new file mode 100644 index 0000000..5c81432 --- /dev/null +++ b/docs/WindowFilter.js.html @@ -0,0 +1,767 @@ + + + + + + WindowFilter.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowFilter.js

+ + + + + + + +
+
+
import ConfettiManager from "./confetttiManager";
+import Overlay from "./Overlay";
+import { calculateRelativeLuminance, localizeDate, localizeNumber, localizePercent, rgbToHex } from "./utils";
+
+/** The overlay builder for the color filter Blue Marble window.
+ * @description This class handles the overlay UI for the color filter window of the Blue Marble userscript.
+ * @class WindowFilter
+ * @since 0.88.329
+ * @see {@link Overlay} for examples
+ */
+export default class WindowFilter extends Overlay {
+
+  /** Constructor for the color filter window
+   * @param {*} executor - The executing class
+   * @since 0.88.329
+   * @see {@link Overlay#constructor}
+   */
+  constructor(executor) {
+    super(executor.name, executor.version); // Executes the code in the Overlay constructor
+    this.window = null; // Contains the *window* DOM tree
+    this.windowID = 'bm-window-filter'; // The ID attribute for this window
+    this.colorListID = 'bm-filter-flex'; // The ID attribute for the color list
+    this.windowParent = document.body; // The parent of the window DOM tree
+
+    /** The templateManager instance currently being used. @type {TemplateManager} */
+    this.templateManager = executor.apiManager?.templateManager;
+
+    // Eye icons
+    this.eyeOpen = '<svg viewBox="0 .5 6 3"><path d="M0,2Q3-1 6,2Q3,5 0,2H2A1,1 0 1 0 3,1Q3,2 2,2"/></svg>';
+    this.eyeClosed = '<svg viewBox="0 1 12 6"><mask id="a"><path d="M0,0H12V8L0,2" fill="#fff"/></mask><path d="M0,4Q6-2 12,4Q6,10 0,4H4A2,2 0 1 0 6,2Q6,4 4,4ZM1,2L10,6.5L9.5,7L.5,2.5" mask="url(#a)"/></svg>';
+
+    // Obtains the color palette Blue Marble currently uses
+    const { palette: palette, LUT: _ } = this.templateManager.paletteBM;
+    this.palette = palette;
+
+    // Tile quantity information
+    this.tilesLoadedTotal = 0; // Number of tiles that have been loaded in this session
+    this.tilesTotal = 0; // Number of tiles total, across all templates
+
+    // Pixel statistics
+    this.allPixelsColor = new Map(); // The amount of pixels total per color as a Map
+    this.allPixelsCorrect = new Map(); // The amount of correct pixels per color as a Map
+    this.allPixelsCorrectTotal = 0; // Sums the pixels placed as "correct" per everything
+    this.allPixelsTotal = 0; // Sums the pixels placed as "total" per everything
+    this.timeRemaining = 0; // Calculates the date & time the user will complete the templates
+    this.timeRemainingLocalized = ''; // The date & time the user will complete the templates in the date-time format of the user's device, as a string
+
+    // Color list display settings
+    this.sortPrimary = 'id'; // The last used primary sort option
+    this.sortSecondary = 'ascending'; // The last used secondary sort option
+    this.showUnused = false; // Were unused colors shown the last time the user sorted the color list?
+  }
+
+  /** Spawns a Color Filter window.
+   * If another color filter window already exists, we DON'T spawn another!
+   * Parent/child relationships in the DOM structure below are indicated by indentation.
+   * @since 0.88.149
+   */
+  buildWindow() {
+
+    // If a color filter wizard window already exists, close it
+    if (document.querySelector(`#${this.windowID}`)) {
+      document.querySelector(`#${this.windowID}`).remove();
+      return;
+    }
+    
+    // Creates a new color filter window
+    this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window'}, (instance, div) => {
+      // div.onclick = (event) => {
+      //   if (event.target.closest('button, a, input, select')) {return;} // Exit-early if interactive child was clicked
+      //   div.parentElement.appendChild(div); // When the window is clicked on, bring to top
+      // }
+    }).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': 'Switch to windowed mode for "Color Filter"'}, (instance, button) => {
+            button.onclick = () => {
+              document.querySelector(`#${this.windowID}`)?.remove();
+              this.buildWindowed();
+            };
+            button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
+          }).buildElement()
+          .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': 'Color Filter'}).buildElement()
+        .buildElement()
+        .addHr().buildElement()
+        .addDiv({'class': 'bm-container bm-flex-between bm-center-vertically', 'style': 'gap: 1.5ch;'})
+          .addButton({'textContent': 'Hide All Colors'}, (instance, button) => {
+            button.onclick = () => this.#selectColorList(false);
+          }).buildElement()
+          .addButton({'textContent': 'Refresh Data'}, (instance, button) => {
+            button.onclick = () => {
+              button.disabled = true;
+              this.updateColorList();
+              button.disabled = false;
+            };
+          }).buildElement()
+          .addButton({'textContent': 'Show All Colors'}, (instance, button) => {
+            button.onclick = () => this.#selectColorList(true);
+          }).buildElement()
+        .buildElement()
+        .addDiv({'class': 'bm-container bm-scrollable'})
+          .addDiv({'class': 'bm-container', 'style': 'margin-left: 2.5ch; margin-right: 2.5ch;'})
+            .addDiv({'class': 'bm-container'})
+              .addSpan({'id': 'bm-filter-tile-load', 'innerHTML': '<b>Tiles Loaded:</b> 0 / ???'}).buildElement()
+              .addBr().buildElement()
+              .addSpan({'id': 'bm-filter-tot-correct', 'innerHTML': '<b>Correct Pixels:</b> ???'}).buildElement()
+              .addBr().buildElement()
+              .addSpan({'id': 'bm-filter-tot-total', 'innerHTML': '<b>Total Pixels:</b> ???'}).buildElement()
+              .addBr().buildElement()
+              .addSpan({'id': 'bm-filter-tot-remaining', 'innerHTML': '<b>Complete:</b> ??? (???)'}).buildElement()
+              .addBr().buildElement()
+              .addSpan({'id': 'bm-filter-tot-completed', 'innerHTML': '??? ???'}).buildElement()
+            .buildElement()
+            .addDiv({'class': 'bm-container'})
+              .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'})
+              .addFieldset()
+                .addLegend({'textContent': 'Sort Options:', 'style': 'font-weight: 700;'}).buildElement()
+                .addDiv({'class': 'bm-container'})
+                  .addSelect({'id': 'bm-filter-sort-primary', 'name': 'sortPrimary', 'textContent': 'I want to view '})
+                    .addOption({'value': 'id', 'textContent': 'color IDs'}).buildElement()
+                    .addOption({'value': 'name', 'textContent': 'color names'}).buildElement()
+                    .addOption({'value': 'premium', 'textContent': 'premium colors'}).buildElement()
+                    .addOption({'value': 'percent', 'textContent': 'percentage'}).buildElement()
+                    .addOption({'value': 'correct', 'textContent': 'correct pixels'}).buildElement()
+                    .addOption({'value': 'incorrect', 'textContent': 'incorrect pixels'}).buildElement()
+                    .addOption({'value': 'total', 'textContent': 'total pixels'}).buildElement()
+                  .buildElement()
+                  .addSelect({'id': 'bm-filter-sort-secondary', 'name': 'sortSecondary', 'textContent': ' in '})
+                    .addOption({'value': 'ascending', 'textContent': 'ascending'}).buildElement()
+                    .addOption({'value': 'descending', 'textContent': 'descending'}).buildElement()
+                  .buildElement()
+                  .addSpan({'textContent': ' order.'}).buildElement()
+                .buildElement()
+                .addDiv({'class': 'bm-container'})
+                  .addCheckbox({'id': 'bm-filter-show-unused', 'name': 'showUnused', 'textContent': 'Show unused colors'}).buildElement()
+                .buildElement()
+              .buildElement()
+              .addDiv({'class': 'bm-container'})
+                .addButton({'textContent': 'Sort Colors', 'type': 'submit'}, (instance, button) => {
+                  button.onclick = (event) => {
+                    event.preventDefault(); // Stop default form submission
+
+                    // Get the form data
+                    const formData = new FormData(document.querySelector(`#${this.windowID} form`));
+                    const formValues = {};
+                    for (const [input, value] of formData) {
+                      formValues[input] = value;
+                    }
+                    console.log(`Primary: ${formValues['sortPrimary']}; Secondary: ${formValues['sortSecondary']}; Unused: ${formValues['showUnused'] == 'on'}`);
+                    
+                    // Sort the color list
+                    this.#sortColorList(formValues['sortPrimary'], formValues['sortSecondary'], formValues['showUnused'] == 'on');
+                  }
+                }).buildElement()
+              .buildElement()
+            .buildElement()
+          .buildElement()
+          // Color list will appear here in the DOM tree
+        .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`);
+
+    // Obtains the scrollable container to put the color filter in
+    const scrollableContainer = document.querySelector(`#${this.windowID} .bm-container.bm-scrollable`);
+    
+    // These run when the user opens the Color Filter window
+    this.#buildColorList(scrollableContainer);
+    this.#sortColorList(this.sortPrimary, this.sortSecondary, this.showUnused);
+
+    // Displays some template statistics to the user
+    this.updateInnerHTML('#bm-filter-tile-load', `<b>Tiles Loaded:</b> ${localizeNumber(this.tilesLoadedTotal)} / ${localizeNumber(this.tilesTotal)}`);
+    this.updateInnerHTML('#bm-filter-tot-correct', `<b>Correct Pixels:</b> ${localizeNumber(this.allPixelsCorrectTotal)}`);
+    this.updateInnerHTML('#bm-filter-tot-total', `<b>Total Pixels:</b> ${localizeNumber(this.allPixelsTotal)}`);
+    this.updateInnerHTML('#bm-filter-tot-remaining', `<b>Remaining:</b> ${localizeNumber((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0))} (${localizePercent(((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0)) / (this.allPixelsTotal || 1))})`);
+    this.updateInnerHTML('#bm-filter-tot-completed', `<b>Completed at:</b> <time datetime="${this.timeRemaining.toISOString().replace(/\.\d{3}Z$/, 'Z')}">${this.timeRemainingLocalized}</time>`);
+  }
+
+  /** Spawns a windowed Color Filter window.
+   * If another color filter window already exists, we DON'T spawn another!
+   * Parent/child relationships in the DOM structure below are indicated by indentation.
+   * @since 0.90.35
+   */
+  buildWindowed() {
+
+    // If a color filter wizard window already exists, close it
+    if (document.querySelector(`#${this.windowID}`)) {
+      document.querySelector(`#${this.windowID}`).remove();
+      return;
+    }
+
+    // Creates a new windowed color filter window
+    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 = () => {
+            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()
+          .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 = () => {
+              document.querySelector(`#${this.windowID}`)?.remove();
+              this.buildWindow();
+            };
+            button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
+          }).buildElement()
+          .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': 'Color Filter'}).buildElement()
+        .buildElement()
+        .addHr().buildElement()
+        .addDiv({'class': 'bm-container bm-flex-between bm-center-vertically', 'style': 'gap: 1.5ch;'})
+          .addButton({'textContent': 'None'}, (instance, button) => {
+            button.onclick = () => this.#selectColorList(false);
+          }).buildElement()
+          .addButton({'textContent': 'Refresh'}, (instance, button) => {
+            button.onclick = () => {
+              button.disabled = true;
+              this.updateColorList();
+              button.disabled = false;
+            };
+          }).buildElement()
+          .addButton({'textContent': 'All'}, (instance, button) => {
+            button.onclick = () => this.#selectColorList(true);
+          }).buildElement()
+        .buildElement()
+        .addDiv({'class': 'bm-container bm-scrollable'})
+          // Color list will appear here
+        .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`);
+
+    // Obtains the scrollable container to put the color filter in
+    const scrollableContainer = document.querySelector(`#${this.windowID} .bm-container.bm-scrollable`);
+    
+    // These run when the user opens the Color Filter window
+    this.#buildColorList(scrollableContainer);
+    this.#sortColorList(this.sortPrimary, this.sortSecondary, this.showUnused);
+  }
+
+  /** Creates the color list container.
+   * @param {HTMLElement} parentElement - Parent element to add the color list to as a child
+   * @since 0.88.222
+   */
+  #buildColorList(parentElement) {
+
+    // Figures out if this window is fullscreen or windowed mode
+    const isWindowedMode = parentElement.closest(`#${this.windowID}`)?.classList.contains('bm-windowed');
+    // Note: `undefined` is expected to behave as if `false`
+    
+    console.log(`Is Windowed Mode: ${isWindowedMode}`);
+
+    const colorList = new Overlay(this.name, this.version);
+    colorList.addDiv({'id': this.colorListID})
+    // We leave it open so we can add children to the grid
+
+    // Generated by #updateColorList()
+    const colorStatistics = this.updateColorList();
+
+    // For each color in the palette...
+    for (const color of this.palette) {
+
+      // Converts the RGB color to hexdecimal
+      const colorValueHex = '#' + rgbToHex(color.rgb).toUpperCase();
+
+      // Relative Luminance
+      const lumin = calculateRelativeLuminance(color.rgb);
+
+      // Calculates if white or black text would contrast better with the palette color
+      let textColorForPaletteColorBackground = 
+      (((1.05) / (lumin + 0.05)) > ((lumin + 0.05) / 0.05)) 
+      ? 'white' : 'black';
+
+      // However, if the color is "Transparent" (or there is no color ID), then we make the text color transparent
+      if (!color.id) {
+        textColorForPaletteColorBackground = 'transparent';
+      }
+
+      // Changes the luminance of the hover/focus button effect
+      const bgEffectForButtons = (textColorForPaletteColorBackground == 'white') ? 'bm-button-hover-white' : 'bm-button-hover-black';
+
+      // Generated by #updateColorList()
+      const {
+        colorCorrect: colorCorrect,
+        colorCorrectLocalized: colorCorrectLocalized,
+        colorPercent: colorPercent,
+        colorTotal: colorTotal,
+        colorTotalLocalized: colorTotalLocalized,
+        colorIncorrect: colorIncorrect
+      } = colorStatistics[color.id];
+
+      const isColorHidden = !!(this.templateManager.shouldFilterColor.get(color.id) || false);
+
+      // Add the color to the color list DOM
+      if (isWindowedMode) {
+
+        // The star pattern for premium colors
+        const styleBackgroundStar = `background-size: auto 100%; background-repeat: repeat-x; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><path d='M50,5L79,91L2,39L98,39L21,91' fill='${textColorForPaletteColorBackground}' fill-opacity='.1'/></svg>");`;
+
+        // Add windowed mode color DOM to color list
+        colorList.addDiv({'class': 'bm-container bm-filter-color bm-flex-between',
+          // Dataset
+          'data-id': color.id,
+          'data-name': color.name,
+          'data-premium': +color.premium,
+          'data-correct': !Number.isNaN(parseInt(colorCorrect)) ? colorCorrect : '0',
+          'data-total': colorTotal,
+          'data-percent': (colorPercent.slice(-1) == '%') ? colorPercent.slice(0, -1) : '0',
+          'data-incorrect': colorIncorrect || 0
+        }).addDiv({'class': 'bm-filter-container-rgb', 'style': `background-color: rgb(${color.rgb?.map(channel => Number(channel) || 0).join(',')});${color.premium ? styleBackgroundStar : ''}`})
+            .addButton({
+              'class': 'bm-button-trans ' + bgEffectForButtons,
+              'data-state': isColorHidden ? 'hidden' : 'shown',
+              'aria-label': isColorHidden ? `Show the color ${color.name || ''} on templates.` : `Hide the color ${color.name || ''} on templates.`,
+              'innerHTML': isColorHidden ? this.eyeClosed.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`) : this.eyeOpen.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`)},
+              (instance, button) => {
+
+                // When the button is clicked
+                button.onclick = () => {
+                  button.style.textDecoration = 'none';
+                  button.disabled = true;
+                  if (button.dataset['state'] == 'shown') {
+                    button.innerHTML = this.eyeClosed.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`);
+                    button.dataset['state'] = 'hidden';
+                    button.ariaLabel = `Show the color ${color.name || ''} on templates.`;
+                    this.templateManager.shouldFilterColor.set(color.id, true);
+                  } else {
+                    button.innerHTML = this.eyeOpen.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`);
+                    button.dataset['state'] = 'shown';
+                    button.ariaLabel = `Hide the color ${color.name || ''} on templates.`;
+                    this.templateManager.shouldFilterColor.delete(color.id);
+                  }
+                  button.disabled = false;
+                  button.style.textDecoration = '';
+                }
+
+                // Disables the "hide color" button if the color is "Transparent" (or no ID exists)
+                if (!color.id) {button.disabled = true;}
+              }
+            ).buildElement()
+            .addSmall({'textContent': `#${color.id.toString().padStart(2, 0)}`, 'style': `color: ${((color.id == -1) || (color.id == 0)) ? 'white' : textColorForPaletteColorBackground}`}).buildElement()
+            .addHeader(2, {'textContent': color.name, 'style': `color: ${((color.id == -1) || (color.id == 0)) ? 'white' : textColorForPaletteColorBackground}`}).buildElement()
+            .addSmall({'class': 'bm-filter-color-pxl-cnt', 'textContent': `${colorCorrectLocalized} / ${colorTotalLocalized}`, 'style': `color: ${((color.id == -1) || (color.id == 0)) ? 'white' : textColorForPaletteColorBackground}; flex: 1 1 auto; text-align: right;`}).buildElement()
+          .buildElement()
+        .buildElement();
+      } else {
+        // Else we are in fullscreen mode.
+
+        // Add fullscreen mode color DOM to color list
+        colorList.addDiv({'class': 'bm-container bm-filter-color bm-flex-between',
+          'data-id': color.id,
+          'data-name': color.name,
+          'data-premium': +color.premium,
+          'data-correct': !Number.isNaN(parseInt(colorCorrect)) ? colorCorrect : '0',
+          'data-total': colorTotal,
+          'data-percent': (colorPercent.slice(-1) == '%') ? colorPercent.slice(0, -1) : '0',
+          'data-incorrect': colorIncorrect || 0
+        }).addDiv({'class': 'bm-flex-center', 'style': 'flex-direction: column;'})
+            .addDiv({'class': 'bm-filter-container-rgb', 'style': `background-color: rgb(${color.rgb?.map(channel => Number(channel) || 0).join(',')});`})
+              .addButton({
+                'class': 'bm-button-trans ' + bgEffectForButtons,
+                'data-state': isColorHidden ? 'hidden' : 'shown',
+                'aria-label': isColorHidden ? `Show the color ${color.name || ''} on templates.` : `Hide the color ${color.name || ''} on templates.`,
+                'innerHTML': isColorHidden ? this.eyeClosed.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`) : this.eyeOpen.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`)},
+                (instance, button) => {
+
+                  // When the button is clicked
+                  button.onclick = () => {
+                    button.style.textDecoration = 'none';
+                    button.disabled = true;
+                    if (button.dataset['state'] == 'shown') {
+                      button.innerHTML = this.eyeClosed.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`);
+                      button.dataset['state'] = 'hidden';
+                      button.ariaLabel = `Show the color ${color.name || ''} on templates.`;
+                      this.templateManager.shouldFilterColor.set(color.id, true);
+                    } else {
+                      button.innerHTML = this.eyeOpen.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`);
+                      button.dataset['state'] = 'shown';
+                      button.ariaLabel = `Hide the color ${color.name || ''} on templates.`;
+                      this.templateManager.shouldFilterColor.delete(color.id);
+                    }
+                    button.disabled = false;
+                    button.style.textDecoration = '';
+                  }
+
+                  // Disables the "hide color" button if the color is "Transparent" (or no ID exists)
+                  if (!color.id) {button.disabled = true;}
+                }
+              ).buildElement()
+            .buildElement()
+            .addSmall({'textContent': (color.id == -2) ? '???????' : colorValueHex}).buildElement()
+          .buildElement()
+          .addDiv({'class': 'bm-flex-between'})
+            .addHeader(2, {'textContent': (color.premium ? '★ ' : '') + color.name}).buildElement()
+            .addDiv({'class': 'bm-flex-between', 'style': 'gap: 1.5ch;'})
+              .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 pixel${(colorIncorrect == 1) ? '' : 's'}. Completed: ${colorPercent}`}).buildElement()
+          .buildElement()
+        .buildElement();
+      }
+    }
+
+    // Adds the colors to the color container in the filter window
+    colorList.buildOverlay(parentElement);
+  }
+
+  /** Sorts the color list & hides unused colors
+   * @param {string} sortPrimary - The name of the dataset attribute to sort by.
+   * @param {string} sortSecondary - Secondary sort. It can be either 'ascending' or 'descending'.
+   * @param {boolean} showUnused - Should unused colors be displayed in the list to the user?
+   * @since 0.88.222
+   */
+  #sortColorList(sortPrimary, sortSecondary, showUnused) {
+
+    // Update memorised sort settings
+    this.sortPrimary = sortPrimary;
+    this.sortSecondary = sortSecondary;
+    this.showUnused = showUnused;
+
+    const colorList = document.querySelector(`#${this.colorListID}`);
+
+    const colors = Array.from(colorList.children);
+
+    colors.sort((index, nextIndex) => {
+      const indexValue = index.getAttribute('data-' + sortPrimary);
+      const nextIndexValue = nextIndex.getAttribute('data-' + sortPrimary);
+
+      const indexValueNumber = parseFloat(indexValue);
+      const nextIndexValueNumber = parseFloat(nextIndexValue);
+
+      const indexValueNumberIsNumber = !isNaN(indexValueNumber);
+      const nextIndexValueNumberIsNumber = !isNaN(nextIndexValueNumber);
+
+      // If the user wants to show unused colors...
+      if (showUnused) {
+        index.classList.remove('bm-color-hide'); // Show the color
+      } else if (!Number(index.getAttribute('data-total'))) {
+        // ...else if the user wants to hide unused colors, and this color is unused...
+        
+        index.classList.add('bm-color-hide'); // Hide the color
+      }
+
+      // If both index values are numbers...
+      if (indexValueNumberIsNumber && nextIndexValueNumberIsNumber) {
+        // Perform numeric comparison
+        return sortSecondary === 'ascending' ? indexValueNumber - nextIndexValueNumber : nextIndexValueNumber - indexValueNumber;
+      } else {
+        // Otherwise, perform string comparison
+        const indexValueString = indexValue.toLowerCase();
+        const nextIndexValueString = nextIndexValue.toLowerCase();
+        if (indexValueString < nextIndexValueString) return sortSecondary === 'ascending' ? -1 : 1;
+        if (indexValueString > nextIndexValueString) return sortSecondary === 'ascending' ? 1 : -1;
+        return 0;
+      }
+    });
+
+    colors.forEach(color => colorList.appendChild(color));
+  }
+
+  /** (Un)selects all colors in the color list that are visible to the user.
+   * @param {boolean} userWantsUnselect - Does the user want to unselect colors?
+   * @since 0.88.222
+   */
+  #selectColorList(userWantsUnselect) {
+
+    // Gets the colors
+    const colorList = document.querySelector(`#${this.colorListID}`);
+    const colors = Array.from(colorList.children);
+
+    // For each color...
+    for (const color of colors) {
+
+      // Skip this color if it is hidden
+      if (color.classList?.contains('bm-color-hide')) {continue;}
+
+      // Gets the button to click
+      const button = color.querySelector('.bm-filter-container-rgb button');
+      
+      // Exits early if the button is in its proper state
+      if ((button.dataset['state'] == 'hidden') && !userWantsUnselect) {continue;} // If the button is selected, and the user wants to select all buttons, then skip this one
+      if ((button.dataset['state'] == 'shown') && userWantsUnselect) {continue;} // If the button is not selected, and the user wants to unselect all buttons, then skip this one
+      
+      button.click(); // If the button is not in its proper state, then we click it
+    }
+  }
+
+  /** 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, ColorData>}
+   */
+  updateColorList() {
+
+    this.#calculatePixelStatistics(); // Updates the pixel statistics in the class instance variables
+
+    const colorList = document.querySelector(`#${this.colorListID}`);
+
+    const colorStatistics = {};
+
+    // For each color...
+    for (const color of this.palette) {
+
+      // Turns "total" color into a string of a number; "0" if unknown
+      const colorTotal = this.allPixelsColor.get(color.id) ?? 0
+      const colorTotalLocalized = localizeNumber(colorTotal);
+      
+      // This will be displayed if the total pixels for this color is zero
+      let colorCorrect = 0;
+      let colorCorrectLocalized = '0';
+      let colorPercent = localizePercent(1);
+
+      // This will be displayed if the total pixels for this color is non-zero
+      if (colorTotal != 0) {
+
+        // Determines the correct pixels, or the proper fallback
+        colorCorrect = this.allPixelsCorrect.get(color.id) ?? '???';
+        if ((typeof colorCorrect != 'number') && (this.tilesLoadedTotal == this.tilesTotal) && !!color.id) {
+          colorCorrect = 0;
+        }
+
+        colorCorrectLocalized = (typeof colorCorrect == 'string') ? colorCorrect : localizeNumber(colorCorrect);
+        colorPercent = isNaN(colorCorrect / colorTotal) ? '???' : localizePercent(colorCorrect / colorTotal);
+      }
+      // There are four outcomes:
+      // 1. The correct pixel count is displayed, because there are correct pixels.
+      // 2. There are NO correct pixels, and the color is not transparent, but since all tiles are loaded, we know that the correct pixel count is actually 0.
+      // 3. There are NO correct pixels, and the color is not transparent, and not all tiles are loaded. We don't know if there are correct pixels or not, so we display "???" instead.
+      // 4. There are NO correct pixels, and the color is transparent, so we display '???' because tracking the "Transparent" color is currently disabled.
+
+      // Incorrect pixels for this color
+      const colorIncorrect = parseInt(colorTotal) - parseInt(colorCorrect);
+
+      colorStatistics[color.id] = {
+        colorTotal: colorTotal,
+        colorTotalLocalized: colorTotalLocalized,
+        colorCorrect: colorCorrect,
+        colorCorrectLocalized: colorCorrectLocalized,
+        colorPercent: colorPercent,
+        colorIncorrect: colorIncorrect
+      }
+    }
+
+    // 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;}
+
+    const colors = Array.from(colorList.children);
+
+    // For each color...
+    for (const color of colors) {
+
+      const colorID = parseInt(color.dataset['id']);
+
+      // Obtains the data to update then
+      const {
+        colorCorrect: colorCorrect,
+        colorCorrectLocalized: colorCorrectLocalized,
+        colorPercent: colorPercent,
+        colorTotal: colorTotal,
+        colorTotalLocalized: colorTotalLocalized,
+        colorIncorrect: colorIncorrect
+      } = colorStatistics[colorID];
+
+      // Update the dataset
+      color.dataset['correct'] = !Number.isNaN(parseInt(colorCorrect)) ? colorCorrect : '0';
+      color.dataset['total'] = colorTotal;
+      color.dataset['percent'] = (colorPercent.slice(-1) == '%') ? colorPercent.slice(0, -1) : '0';
+      color.dataset['incorrect'] = colorIncorrect || 0;
+
+      // Updates the pixel count if it exists
+      const pixelCount = document.querySelector(`#${this.windowID} .bm-filter-color[data-id="${colorID}"] .bm-filter-color-pxl-cnt`);
+      if (pixelCount) {pixelCount.textContent = `${colorCorrectLocalized} / ${colorTotalLocalized}`;}
+
+      // 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 pixel${(colorIncorrect == 1) ? '' : 's'}. Completed: ${colorPercent}`;}
+    }
+
+    // Since the dataset has changed, we need to sort again
+    // Because if the user wants to sort by pixel count, the order should change
+    this.#sortColorList(this.sortPrimary, this.sortSecondary, this.showUnused);
+  }
+
+  /** Calculates all pixel statistics used in the color filter.
+   * @since 0.90.34
+   */
+  #calculatePixelStatistics() {
+
+    // Resets pixel totals to 0
+    this.allPixelsTotal = 0;
+    this.allPixelsCorrectTotal = 0;
+    this.allPixelsCorrect = new Map();
+    this.allPixelsColor = new Map();
+
+    // Sum the pixel totals across all templates.
+    // If there is no total for a template, it defaults to zero
+    for (const template of this.templateManager.templatesArray) {
+
+      const total = template.pixelCount?.total ?? 0;
+      this.allPixelsTotal += total ?? 0; // Sums the pixels placed as "total" per everything
+
+      const colors = template.pixelCount?.colors ?? new Map();
+
+      // Sums the color pixels placed as "total" per color ID
+      for (const [colorID, colorPixels] of colors) {
+        const _colorPixels = Number(colorPixels) || 0; // Boilerplate
+        const allPixelsColorSoFar = this.allPixelsColor.get(colorID) ?? 0; // The total color pixels for this color ID so far, or zero if none counted so far
+        this.allPixelsColor.set(colorID, allPixelsColorSoFar + _colorPixels);
+      }
+
+      // Object that contains the tiles which contain Maps as correct pixels per tile as the value in the key-value pair
+      const correctObject = template.pixelCount?.correct ?? {};
+
+      this.tilesLoadedTotal += Object.keys(correctObject).length; // Sums the total loaded tiles per template
+      this.tilesTotal += Object.keys(template.chunked).length; // Sums the total tiles per template
+
+      // Sums the pixels placed as "correct" per color ID
+      for (const map of Object.values(correctObject)) { // Per (loaded) tile per template
+        for (const [colorID, correctPixels] of map) { // Per color per (loaded) tile per template
+          const _correctPixels = Number(correctPixels) || 0; // Boilerplate
+          this.allPixelsCorrectTotal += _correctPixels; // Sums the pixels placed as "correct" per everything
+          const allPixelsCorrectSoFar = this.allPixelsCorrect.get(colorID) ?? 0; // The total correct pixels for this color ID so far, or zero if none counted so far
+          this.allPixelsCorrect.set(colorID, allPixelsCorrectSoFar + _correctPixels);
+        }
+      }
+    }
+
+    console.log(`Tiles loaded: ${this.tilesLoadedTotal} / ${this.tilesTotal}`);
+
+    // If the template is complete, and the pixel count is non-zero, and at least 1 template exists, and all template tiles have been loaded this session...
+    if ((this.allPixelsCorrectTotal >= this.allPixelsTotal) && !!this.allPixelsTotal && (this.tilesLoadedTotal == this.tilesTotal)) {
+      // Basically, only run if Blue Marble can confirm with 100% certanty that all (>0) templates are complete.
+      
+      // Create confetti in the color filter window
+      const confettiManager = new ConfettiManager();
+      confettiManager.createConfetti(document.querySelector(`#${this.windowID}`));
+    }
+
+    // Calculates the date & time the user will complete the templates
+    this.timeRemaining = new Date(((this.allPixelsTotal - this.allPixelsCorrectTotal) * 30 * 1000) + Date.now());
+    this.timeRemainingLocalized = localizeDate(this.timeRemaining);
+  }
+}
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/WindowMain.html b/docs/WindowMain.html new file mode 100644 index 0000000..34152ec --- /dev/null +++ b/docs/WindowMain.html @@ -0,0 +1,184 @@ + + + + + + WindowMain - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowMain

+ + + + + + + +
+ +
+ +

+ WindowMain +

+ + +
+ +
+
+ + +
+ + + +

new WindowMain()

+ + + + + +
+

This class handles the overlay UI for the main window of the Blue Marble userscript.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.326
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/WindowMain.js.html b/docs/WindowMain.js.html new file mode 100644 index 0000000..816731e --- /dev/null +++ b/docs/WindowMain.js.html @@ -0,0 +1,316 @@ + + + + + + WindowMain.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowMain.js

+ + + + + + + +
+
+
import ConfettiManager from "./confetttiManager";
+import Overlay from "./Overlay";
+import { getClipboardData } from "./utils";
+import WindowCredts from "./WindowCredits";
+import WindowFilter from "./WindowFilter";
+import WindowWizard from "./WindowWizard";
+
+/** The overlay builder for the main Blue Marble window.
+ * @description This class handles the overlay UI for the main window of the Blue Marble userscript.
+ * @class WindowMain
+ * @since 0.88.326
+ * @see {@link Overlay} for examples
+ */
+export default class WindowMain extends Overlay {
+
+  /** Constructor for the main Blue Marble window
+   * @param {string} name - The name of the userscript
+   * @param {string} version - The version of the userscript
+   * @since 0.88.326
+   * @see {@link Overlay#constructor}
+   */
+  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-main'; // The ID attribute for this window
+    this.windowParent = document.body; // The parent of the window DOM tree
+  }
+
+  /** Creates the main Blue Marble window.
+   * Parent/child relationships in the DOM structure below are indicated by indentation.
+   * @since 0.58.3
+   */
+  buildWindow() {
+
+    // If the main window already exists, throw an error and return early
+    if (document.querySelector(`#${this.windowID}`)) {
+      this.handleDisplayError('Main window already exists!');
+      return;
+    }
+
+    // Creates the window
+    this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window bm-windowed', 'style': 'top: 10px; left: unset; right: 75px;'}, (instance, div) => {
+      // div.onclick = (event) => {
+      //   if (event.target.closest('button, a, input, select')) {return;} // Exit-early if interactive child was clicked
+      //   div.parentElement.appendChild(div); // When the window is clicked on, bring to top
+      // }
+    }).addDragbar()
+        .addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Blue Marble"', '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
+      .buildElement()
+      .addDiv({'class': 'bm-window-content'})
+        .addDiv({'class': 'bm-container'})
+          .addImg({'class': 'bm-favicon', 'src': 'https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png'}, (instance, img) => {
+            // Adds a birthday hat & confetti to the window if it is Blue Marble's birthday
+            const date = new Date();
+            const dayOfTheYear = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 1)) / (1000 * 60 * 60 * 24)) + 1;
+            if (dayOfTheYear == 204) {
+              img.parentNode.style.position = 'relative';
+              img.parentNode.innerHTML = img.parentNode.innerHTML + `<svg viewBox="0 0 9 7" width="2em" height="2em" style="position: absolute; top: -.75em; left: 3.25ch;"><path d="M0,3L9,0L2,7" fill="#0af"/><path d="M0,3A.4,.4 0 1 1 1,5" fill="#a00"/><path d="M1.5,6A1,1 0 0 1 3,6L2,7" fill="#a0f"/><path d="M4,5A.6,.6 0 1 1 5,4" fill="#0a0"/><path d="M6,3A.8,.8 0 1 1 7,2" fill="#fa0"/><path d="M4.5,1.5A1,1 0 0 1 3,2" fill="#aa0"/></svg>`;
+              img.onload = () => {
+                const confettiManager = new ConfettiManager();
+                confettiManager.createConfetti(document.querySelector(`#${this.windowID}`));
+              };
+            }
+          }).buildElement()
+          .addHeader(1, {'textContent': this.name}).buildElement()
+        .buildElement()
+        .addHr().buildElement()
+        .addDiv({'class': 'bm-container'})
+          .addSpan({'id': 'bm-user-droplets', 'textContent': 'Droplets:'}).buildElement()
+          .addBr().buildElement()
+          .addSpan({'id': 'bm-user-nextlevel', 'textContent': 'Next level in...'}).buildElement()
+          .addBr().buildElement()
+          .addSpan({'textContent': 'Charges: '})
+            .addTimer(Date.now(), 1000, {'style': 'font-weight: 700;'}, (instance, timer) => {
+              instance.apiManager.chargeRefillTimerID = timer.id; // Store the timer ID in apiManager so we can update the timer automatically
+            }).buildElement()
+          .buildElement()
+        .buildElement()
+        .addHr().buildElement()
+        .addDiv({'class': 'bm-container'})
+          .addDiv({'class': 'bm-container'})
+            .addButton({'class': 'bm-button-circle bm-button-pin', 'style': 'margin-top: 0;', 'innerHTML': '<svg viewBox="0 0 4 6"><path d="M.5,3.4A2,2 0 1 1 3.5,3.4L2,6"/><circle cx="2" cy="2" r=".7" fill="#fff"/></svg>'},
+              (instance, button) => {
+                button.onclick = () => {
+                  const coords = instance.apiManager?.coordsTilePixel; // Retrieves the coords from the API manager
+                  if (!coords?.[0]) {
+                    instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?');
+                    return;
+                  }
+                  instance.updateInnerHTML('bm-input-tx', coords?.[0] || '');
+                  instance.updateInnerHTML('bm-input-ty', coords?.[1] || '');
+                  instance.updateInnerHTML('bm-input-px', coords?.[2] || '');
+                  instance.updateInnerHTML('bm-input-py', coords?.[3] || '');
+                }
+              }
+            ).buildElement()
+            .addInput({'type': 'number', 'id': 'bm-input-tx', 'class': 'bm-input-coords', 'placeholder': 'Tl X', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
+              input.addEventListener("paste", event => this.#coordinateInputPaste(instance, input, event));
+            }).buildElement()
+            .addInput({'type': 'number', 'id': 'bm-input-ty', 'class': 'bm-input-coords', 'placeholder': 'Tl Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
+              input.addEventListener("paste", event => this.#coordinateInputPaste(instance, input, event));
+            }).buildElement()
+            .addInput({'type': 'number', 'id': 'bm-input-px', 'class': 'bm-input-coords', 'placeholder': 'Px X', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
+              input.addEventListener("paste", event => this.#coordinateInputPaste(instance, input, event));
+            }).buildElement()
+            .addInput({'type': 'number', 'id': 'bm-input-py', 'class': 'bm-input-coords', 'placeholder': 'Px Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
+              input.addEventListener("paste", event => this.#coordinateInputPaste(instance, input, event));
+            }).buildElement()
+          .buildElement()
+          .addDiv({'class': 'bm-container'})
+            .addInputFile({'class': 'bm-input-file', 'textContent': 'Upload Template', 'accept': 'image/png, image/jpeg, image/webp, image/bmp, image/gif'}).buildElement()
+          .buildElement()
+          .addDiv({'class': 'bm-container bm-flex-between'})
+            .addButton({'textContent': 'Disable', 'data-button-status': 'shown'}, (instance, button) => {
+              button.onclick = () => {
+                button.disabled = true; // Disables the button until the transition ends
+                if (button.dataset['buttonStatus'] == 'shown') { // If templates are currently being 'shown' then hide them
+                  instance.apiManager?.templateManager?.setTemplatesShouldBeDrawn(false); // Disables templates from being drawn
+                  button.dataset['buttonStatus'] = 'hidden'; // Swap internal button status tracker
+                  button.textContent = 'Enable'; // Swap button text
+                  instance.handleDisplayStatus(`Disabled templates!`); // Inform the user
+                } else { // In all other cases, we should show templates instead of hiding them
+                  instance.apiManager?.templateManager?.setTemplatesShouldBeDrawn(true); // Allows templates to be drawn
+                  button.dataset['buttonStatus'] = 'shown'; // Swap internal button status tracker
+                  button.textContent = 'Disable'; // Swap button text
+                  instance.handleDisplayStatus(`Enabled templates!`); // Inform the user
+                }
+                button.disabled = false; // Enables the button
+              }
+            }).buildElement()
+            .addButton({'textContent': 'Create'}, (instance, button) => {
+              button.onclick = () => {
+                const input = document.querySelector(`#${this.windowID} .bm-input-file`);
+
+                // Checks to see if the coordinates are valid. Throws an error if they are not
+                const coordTlX = document.querySelector('#bm-input-tx');
+                if (!coordTlX.checkValidity()) {coordTlX.reportValidity(); instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?'); return;}
+                const coordTlY = document.querySelector('#bm-input-ty');
+                if (!coordTlY.checkValidity()) {coordTlY.reportValidity(); instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?'); return;}
+                const coordPxX = document.querySelector('#bm-input-px');
+                if (!coordPxX.checkValidity()) {coordPxX.reportValidity(); instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?'); return;}
+                const coordPxY = document.querySelector('#bm-input-py');
+                if (!coordPxY.checkValidity()) {coordPxY.reportValidity(); instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?'); return;}
+
+                // Kills itself if there is no file
+                if (!input?.files[0]) {instance.handleDisplayError(`No file selected!`); return;}
+
+                instance?.apiManager?.templateManager.createTemplate(input.files[0], input.files[0]?.name.replace(/\.[^/.]+$/, ''), [Number(coordTlX.value), Number(coordTlY.value), Number(coordPxX.value), Number(coordPxY.value)]);
+                instance.handleDisplayStatus(`Drew to canvas!`);
+              }
+            }).buildElement()
+            .addButton({'textContent': 'Filter'}, (instance, button) => {
+              button.onclick = () => this.#buildWindowFilter();
+            }).buildElement()
+          .buildElement()
+          .addDiv({'class': 'bm-container'})
+            .addTextarea({'id': this.outputStatusId, 'placeholder': `Status: Sleeping...\nVersion: ${this.version}`, 'readOnly': true}).buildElement()
+          .buildElement()
+          .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;
+                  const wizard = new WindowWizard(this.name, this.version, templateManager?.schemaVersion, templateManager);
+                  wizard.buildWindow();
+                }
+              }).buildElement()
+              .addButton({'class': 'bm-button-circle', 'innerHTML': '🎨', 'title': 'Template Color Converter'}, (instance, button) => {
+                button.onclick = () => {
+                  window.open('https://pepoafonso.github.io/color_converter_wplace/', '_blank', 'noopener noreferrer');
+                }
+              }).buildElement()
+              .addButton({'class': 'bm-button-circle', 'innerHTML': '🌐', 'title': 'Official Blue Marble Website'}, (instance, button) => {
+                button.onclick = () => {
+                  window.open('https://bluemarble.lol/', '_blank', 'noopener noreferrer');
+                }
+              }).buildElement()
+              .addButton({'class': 'bm-button-circle', 'title': 'Donate to SwingTheVine', 'innerHTML': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#fff" style="width:80%; margin:auto;"><path d="M249.8 75c89.8 0 113 1.1 146.3 4.4 78.1 7.8 123.6 56 123.6 125.2l0 8.9c0 64.3-47.1 116.9-110.8 122.4-5 16.6-12.8 33.2-23.3 49.9-24.4 37.7-73.1 85.3-162.9 85.3l-17.7 0c-73.1 0-129.7-31.6-163.5-89.2-29.9-50.4-33.8-106.4-33.8-181.2 0-73.7 44.4-113.6 96.4-120.2 39.3-5 88.1-5.5 145.7-5.5zm0 41.6c-60.4 0-103.6 .5-136.3 5.5-46 6.7-64.3 32.7-64.3 79.2l.2 25.7c1.2 57.3 7.1 97.1 27.5 134.5 26.6 49.3 74.8 68.2 129.7 68.2l17.2 0c72 0 107-34.9 126.3-65.4 9.4-15.5 17.7-32.7 22.2-54.3l3.3-13.8 19.9 0c44.3 0 82.6-36 82.6-82l0-8.3c0-51.5-32.2-78.7-88.1-85.3-31.6-2.8-50.4-3.9-140.2-3.9zM267 169.2c38.2 0 64.8 31.6 64.8 67 0 32.7-18.3 61-42.1 83.1-15 15-39.3 30.5-55.9 40.5-4.4 2.8-10 4.4-16.7 4.4-5.5 0-10.5-1.7-15.5-4.4-16.6-10-41-25.5-56.5-40.5-21.8-20.8-39.2-46.9-41.3-77l-.2-6.1c0-35.5 25.5-67 64.3-67 22.7 0 38.8 11.6 49.3 27.7 11.6-16.1 27.2-27.7 49.9-27.7zm122.5-3.9c28.3 0 43.8 16.6 43.8 43.2s-15.5 42.7-43.8 42.7c-8.9 0-13.8-5-13.8-11.7l0-62.6c0-6.7 5-11.6 13.8-11.6z"/></svg>'}, (instance, button) => {
+                button.onclick = () => {
+                  window.open('https://ko-fi.com/swingthevine', '_blank', 'noopener noreferrer');
+                }
+              }).buildElement()
+              .addButton({'class': 'bm-button-circle', 'innerHTML': '🤝', 'title': 'Credits'}, (instance, button) => {
+                button.onclick = () => {
+                  const credits = new WindowCredts(this.name, this.version);
+                  credits.buildWindow();
+                }
+              }).buildElement()
+            .buildElement()
+            .addSmall({'textContent': 'Made by SwingTheVine', 'style': 'margin-top: auto;'}).buildElement()
+          .buildElement()
+        .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 a new color filter window.
+   * This is a helper function that creates a new class instance.
+   * This might cause a memory leak. I pray that this is not the case...
+   * @since 0.88.330
+   */
+  #buildWindowFilter() {
+    const windowFilter = new WindowFilter(this); // Creates a new color filter window instance
+    windowFilter.buildWindow();
+  }
+
+  /** Handles pasting into the coordinate input boxes in the main Blue Marble window.
+   * @param {Overlay} instance - The Overlay class instance
+   * @param {HTMLInputElement} input - The input element that was pasted into
+   * @param {ClipboardEvent} event - The event that triggered this
+   * @since 0.88.426
+   */
+  async #coordinateInputPaste(instance, input, event) {
+
+    event.preventDefault(); // Stops the paste so we can process it
+
+    const data = await getClipboardData(event); // Obtains the clipboard text
+
+    const coords = data.split(/[^a-zA-Z0-9]+/) // Split. Delimiter to split on is "alphanumeric" `f00 bar 4` -> `['f00', 'bar', '4', '']`
+      .filter(index => index) // Only preserves non-empty indexes `['f00', 'bar', '4']`
+      .map(Number) // Converts every index to a number `[NaN, NaN, 4]`
+      .filter(number => !isNaN(number) // Removes NaN `[4]`
+    );
+
+    // If there are only two coordinates, and they were pasted into the pixel coords...
+    if ((coords.length == 2) && (input.id == 'bm-input-px')) {
+      // ...then paste into the pixel inputs
+
+      instance.updateInnerHTML('bm-input-px', coords?.[0] || '');
+      instance.updateInnerHTML('bm-input-py', coords?.[1] || '');
+    } else if ((coords.length == 1)) {
+      // Else if there is only 1 coordinate, we paste into the input like normal
+
+      instance.updateInnerHTML(input.id, coords?.[0] || '');
+    } else {
+      // Else we paste like normal
+
+      instance.updateInnerHTML('bm-input-tx', coords?.[0] || '');
+      instance.updateInnerHTML('bm-input-ty', coords?.[1] || '');
+      instance.updateInnerHTML('bm-input-px', coords?.[2] || '');
+      instance.updateInnerHTML('bm-input-py', coords?.[3] || '');
+    }
+  }
+}
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/WindowSettings.html b/docs/WindowSettings.html new file mode 100644 index 0000000..be4d0a3 --- /dev/null +++ b/docs/WindowSettings.html @@ -0,0 +1,184 @@ + + + + + + WindowSettings - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowSettings

+ + + + + + + +
+ +
+ +

+ WindowSettings +

+ + +
+ +
+
+ + +
+ + + +

new WindowSettings()

+ + + + + +
+

This class handles the overlay UI for the settings window of the Blue Marble userscript.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.11
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/WindowSettings.js.html b/docs/WindowSettings.js.html new file mode 100644 index 0000000..7b25c92 --- /dev/null +++ b/docs/WindowSettings.js.html @@ -0,0 +1,156 @@ + + + + + + WindowSettings.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowSettings.js

+ + + + + + + +
+
+
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');
+  }
+}
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/WindowTelemetry.html b/docs/WindowTelemetry.html new file mode 100644 index 0000000..6122090 --- /dev/null +++ b/docs/WindowTelemetry.html @@ -0,0 +1,184 @@ + + + + + + WindowTelemetry - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowTelemetry

+ + + + + + + +
+ +
+ +

+ WindowTelemetry +

+ + +
+ +
+
+ + +
+ + + +

new WindowTelemetry()

+ + + + + +
+

This class handles the overlay UI for the telemetry window.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.339
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/WindowTelemetry.js.html b/docs/WindowTelemetry.js.html new file mode 100644 index 0000000..db16735 --- /dev/null +++ b/docs/WindowTelemetry.js.html @@ -0,0 +1,167 @@ + + + + + + WindowTelemetry.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowTelemetry.js

+ + + + + + + +
+
+
import Overlay from "./Overlay";
+import { escapeHTML } from "./utils";
+
+/** The overlay builder for the telemetry window for Blue Marble.
+ * @description This class handles the overlay UI for the telemetry window.
+ * @class WindowTelemetry
+ * @since 0.88.339
+ * @see {@link Overlay} for examples
+ */
+export default class WindowTelemetry extends Overlay {
+
+  /** Constructor for the telemetry window
+   * @param {string} name - The name of the userscript
+   * @param {string} version - The version of the userscript
+   * @param {number} currentTelemetryVersion - The current "version" of the data collection agreement
+   * @param {string} uuid - The UUID of the user
+   * @since 0.88.339
+   * @see {@link Overlay#constructor}
+   */
+  constructor(name, version, currentTelemetryVersion, uuid) {
+    super(name, version); // Executes the code in the Overlay constructor
+    this.window = null; // Contains the *window* DOM tree
+    this.windowID = 'bm-window-telemetry'; // The ID attribute for this window
+    this.windowParent = document.body; // The parent of the window DOM tree
+
+    this.currentTelemetryVersion = currentTelemetryVersion; // The current telemetry "version". Increment this whenever the data collection agreement is changed!
+    
+    this.uuid = uuid; // The UUID of the user
+  }
+
+  /** Spawns a telemetry window.
+   * If another telemetry window already exists, we DON'T spawn another!
+   * Parent/child relationships in the DOM structure below are indicated by indentation.
+   * @since 0.88.339
+   */
+  async buildWindow() {
+
+    // If the telemetry window already exists, throw an error and return early
+    if (document.querySelector(`#${this.windowID}`)) {
+      this.handleDisplayError('Telemetry window already exists!');
+      return;
+    }
+
+    const browser = await this.apiManager.getBrowserFromUA(navigator.userAgent);
+    const os = this.apiManager.getOS(navigator.userAgent);
+
+    this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window', 'style': 'height: 80vh; z-index: 9998;'})
+      .addDiv({'class': 'bm-window-content'})
+        .addDiv({'class': 'bm-container bm-center-vertically'})
+          .addHeader(1, {'textContent': `${this.name} Telemetry`}).buildElement()
+        .buildElement()
+        .addHr().buildElement()
+        .addDiv({'class': 'bm-container bm-flex-center', 'style': 'gap: 1.5ch; flex-wrap: wrap;'})
+          .addButton({'textContent': 'Enable Telemetry'}, (instance, button) => {
+            button.onclick = () => {
+              this.#setTelemetryValue(this.currentTelemetryVersion);
+              const element = document.getElementById(this.windowID);
+              element?.remove();
+            };
+          }).buildElement()
+          .addButton({'textContent': 'Disable Telemetry'}, (instance, button) => {
+            button.onclick = () => {
+              this.#setTelemetryValue(0);
+              const element = document.getElementById(this.windowID);
+              element?.remove();
+            };
+          }).buildElement()
+          .addButton({'textContent': 'More Information'}, (instance, button) => {
+            button.onclick = () => {
+              window.open('https://github.com/SwingTheVine/Wplace-TelemetryServer#telemetry-data', '_blank', 'noopener noreferrer');
+            }
+          }).buildElement()
+        .buildElement()
+        .addDiv({'class': 'bm-container bm-scrollable'})
+          .addDiv({'class': 'bm-container'})
+            .addHeader(2, {'textContent': 'Legal'}).buildElement()
+            .addP({'textContent': `We collect anonymous telemetry data such as your browser, OS, and script version to make the experience better for everyone. The data is never shared personally. The data is never sold. You can turn this off by pressing the "Disable" button, but keeping it on helps us improve features and reliability faster. Thank you for supporting ${this.name}!`}).buildElement()
+          .buildElement()
+          .addHr().buildElement()
+          .addDiv({'class': 'bm-container'})
+            .addHeader(2, {'textContent': 'Non-Legal Summary'}).buildElement()
+            .addP({'innerHTML': `You can disable telemetry by pressing the "Disable" button. If you would like to read more about what information we collect, press the "More Information" button.<br>This is the data <em>stored</em> on our servers:`}).buildElement()
+            .addUl()
+              .addLi({'innerHTML': `A unique identifier (UUIDv4) generated by Blue Marble. This enables our telemetry to function without tracking your actual user ID.<br>Your UUID is: <b>${escapeHTML(this.uuid)}</b>`}).buildElement()
+              .addLi({'innerHTML': `The version of Blue Marble you are using.<br>Your version is: <b>${escapeHTML(this.version)}</b>`}).buildElement()
+              .addLi({'innerHTML': `Your browser type, which is used to determine Blue Marble outages and browser popularity.<br>Your browser type is: <b>${escapeHTML(browser)}</b>`}).buildElement()
+              .addLi({'innerHTML': `Your OS type, which is used to determine Blue Marble outages and OS popularity.<br>Your OS type is: <b>${escapeHTML(os)}</b>`}).buildElement()
+              .addLi({'innerHTML': `The date and time that Blue Marble sent the telemetry information.`}).buildElement()
+            .buildElement()
+            .addP({'innerHTML': `All of the data mentioned above is <b>aggregated every hour</b>. This means every hour, anything that could even remotly be considered "personal data" is deleted from our server. Here, "aggregated" data means things like "42 people used Blue Marble on Google Chrome this hour", which can't be used to identify anyone in particular.`}).buildElement()
+          .buildElement()
+        .buildElement()
+      .buildElement()
+    .buildElement().buildOverlay(this.windowParent);
+  }
+
+  /** Enables or disables telemetry based on the value passed in.
+   * A value of zero will always disable telemetry.
+   * A numeric, non-zero value will enable telemetry until the telemetry agreement is changed.
+   * @param {number} value - The value to set the telemetry to
+   * @since 0.88.339
+   */
+  #setTelemetryValue(value) {
+    const userSettings = JSON.parse(GM_getValue('bmUserSettings', '{}'));
+    userSettings.telemetry = value;
+    GM.setValue('bmUserSettings', JSON.stringify(userSettings));
+  }
+}
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/WindowWizard.html b/docs/WindowWizard.html new file mode 100644 index 0000000..b9c13a1 --- /dev/null +++ b/docs/WindowWizard.html @@ -0,0 +1,184 @@ + + + + + + WindowWizard - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowWizard

+ + + + + + + +
+ +
+ +

+ WindowWizard +

+ + +
+ +
+
+ + +
+ + + +

new WindowWizard()

+ + + + + +
+

Wizard that manages template updates & recovery

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.434
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/WindowWizard.js.html b/docs/WindowWizard.js.html new file mode 100644 index 0000000..1fa7b0c --- /dev/null +++ b/docs/WindowWizard.js.html @@ -0,0 +1,363 @@ + + + + + + WindowWizard.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

WindowWizard.js

+ + + + + + + +
+
+
import Overlay from "./Overlay";
+import Template from "./Template";
+import TemplateManager from "./templateManager";
+import { encodedToNumber, escapeHTML, getWplaceVersion, localizeDate, localizeNumber, sleep } from "./utils";
+
+/** Wizard that manages template updates & recovery
+ * @class WindowWizard
+ * @since 0.88.434
+ * @see {@link Overlay} for examples
+ */
+export default class WindowWizard extends Overlay {
+
+  /** Constructor for the Template Wizard window
+   * @param {string} name - The name of the userscript
+   * @param {string} version - The version of the userscript
+   * @param {string} schemaVersionBleedingEdge - The bleeding edge of schema versions for Blue Marble
+   * @param {TemplateManager} [templateManager=undefined] - (Optional) The TemplateManager class instance
+   * @since 0.88.434
+   * @see {@link Overlay#constructor} for examples
+   */
+  constructor(name, version, schemaVersionBleedingEdge, templateManager = undefined) {
+    super(name, version); // Executes the code in the Overlay constructor
+    this.window = null; // Contains the *window* DOM tree
+    this.windowID = 'bm-window-wizard'; // The ID attribute for this window
+    this.windowParent = document.body; // The parent of the window DOM tree
+
+    // Retrieves data from storage
+    this.currentJSON = JSON.parse(GM_getValue('bmTemplates', '{}')); // The current Blue Marble storage
+    this.scriptVersion = this.currentJSON?.scriptVersion; // Script version when template was created
+    this.schemaVersion = this.currentJSON?.schemaVersion; // Schema version when template was created
+
+    this.schemaHealth = undefined; // Current schema health. This is: 'Good', 'Poor', 'Bad', or 'Dead' for full match, MINOR mismatch, MAJOR mismatch, and unknown, respectively.
+    this.schemaVersionBleedingEdge = schemaVersionBleedingEdge; // Latest schema version
+
+    this.templateManager = templateManager;
+  }
+
+  /** Spawns a Template Wizard window.
+   * If another template wizard window already exists, we DON'T spawn another!
+   * Parent/child relationships in the DOM structure below are indicated by indentation.
+   * @since 0.88.434
+   */
+  buildWindow() {
+
+    // If a template wizard window already exists, close it
+    if (document.querySelector(`#${this.windowID}`)) {
+      document.querySelector(`#${this.windowID}`).remove();
+      return;
+    }
+
+    let style = ''; // Window style
+
+    // If the main window does not exist yet...
+    if (!document.querySelector(`#bm-window-main`)) {
+      style = style.concat('z-index: 9001;').trim();
+    }
+    // Forces the Wizard window to show above the main window if and only if the schema is bad when Blue Marble loads for the first time this session
+
+    // Creates a new template wizard window
+    this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window', 'style': style}, (instance, div) => {
+      // div.onclick = (event) => {
+      //   if (event.target.closest('button, a, input, select')) {return;} // Exit-early if interactive child was clicked
+      //   div.parentElement.appendChild(div); // When the window is clicked on, bring to top
+      // }
+    }).addDragbar()
+        .addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Template Wizard"', '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
+        .addButton({'class': 'bm-button-circle', 'textContent': '✖', 'aria-label': 'Close window "Template Wizard"'}, (instance, button) => {
+          button.onclick = () => {document.querySelector(`#${this.windowID}`)?.remove();};
+          button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
+        }).buildElement()
+      .buildElement()
+      .addDiv({'class': 'bm-window-content'})
+        .addDiv({'class': 'bm-container bm-center-vertically'})
+          .addHeader(1, {'textContent': 'Template Wizard'}).buildElement()
+        .buildElement()
+        .addHr().buildElement()
+        .addDiv({'class': 'bm-container'})
+          .addHeader(2, {'textContent': 'Status'}).buildElement()
+          .addP({'id': 'bm-wizard-status', 'textContent': 'Loading template storage status...'}).buildElement()
+        .buildElement()
+        .addDiv({'class': 'bm-container bm-scrollable'})
+          .addHeader(2, {'textContent': 'Detected templates:'}).buildElement()
+          // Detected templates will show up here
+        .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`);
+
+    this.#displaySchemaHealth(); // Displays template storage health to the user
+    this.#displayTemplateList(); // Displays a list of all templates in the template storage
+  }
+
+  /** Determines how "healthy" the template storage is.
+   * @since 0.88.436
+   */
+  #displaySchemaHealth() {
+
+    // SemVer -> string[]
+    const schemaVersionArray = this.schemaVersion.split(/[-\.\+]/);
+    const schemaVersionBleedingEdgeArray = this.schemaVersionBleedingEdge.split(/[-\.\+]/);
+
+    // Calculates the health that is displayed as a banner
+    let schemaHealthBanner = '';
+    // If the MAJOR version is up-to-date...
+    if (schemaVersionArray[0] == schemaVersionBleedingEdgeArray[0]) {
+
+      // ...AND IF the MINOR version is up-to-date...
+      if (schemaVersionArray[1] == schemaVersionBleedingEdgeArray[1]) {
+        schemaHealthBanner = 'Template storage health: <b style="color:#0f0;">Healthy!</b><br>No futher action required. (Reason: Semantic version matches)';
+        this.schemaHealth = 'Good';
+      } else { // ...else, the MINOR version is out-of-date
+        schemaHealthBanner = 'Template storage health: <b style="color:#ff0;">Poor!</b><br>You can still use your template, but some features may not work. It is recommended that you update Blue Marble\'s template storage. (Reason: MINOR version mismatch)';
+        this.schemaHealth = 'Poor';
+      }
+    } else if (schemaVersionArray[0] < schemaVersionBleedingEdgeArray[0]) {
+      // ...ELSE IF the MAJOR version is out-of-date
+      
+      schemaHealthBanner = 'Template storage health: <b style="color:#f00;">Bad!</b><br>It is guaranteed that some features are broken. You <em>might</em> still be able to use the template. It is HIGHLY recommended that you download all templates and update Blue Marble\'s template storage before continuing. (Reason: MAJOR version mismatch)';
+      this.schemaHealth = 'Bad';
+    } else {
+      // ...ELSE the Semantic version is unknown
+
+      schemaHealthBanner = 'Template storage health: <b style="color:#f00">Dead!</b><br>Blue Marble can not load the template storage. (Reason: MAJOR version unknown)';
+      this.schemaHealth = 'Dead';
+    }
+
+    // Further recovery directions (only displayed if health is NOT 'Good')
+    const recoveryInstructions = `<hr style="margin:.5ch">If you want to continue using your current templates, then make sure the template storage (schema) is up-to-date.<br>If you don't want to update the template storage, then downgrade Blue Marble to version <b>${escapeHTML(this.scriptVersion)}</b> to continue using your templates.<br>Alternatively, if you don't care about corrupting the templates listed below, you can fix any issues with the template storage by uploading a new template.`;
+
+    // Obtains the last time Wplace was updated
+    const wplaceUpdateTime = getWplaceVersion();
+    let wplaceUpdateTimeLocalized = wplaceUpdateTime ? localizeDate(wplaceUpdateTime) : '???';
+
+    // Display schema health to user
+    this.updateInnerHTML('#bm-wizard-status', `${schemaHealthBanner}<br>Your templates were created during Blue Marble version <b>${escapeHTML(this.scriptVersion)}</b> with schema version <b>${escapeHTML(this.schemaVersion)}</b>.<br>The current Blue Marble version is <b>${escapeHTML(this.version)}</b> and requires schema version <b>${escapeHTML(this.schemaVersionBleedingEdge)}</b>.<br>Wplace was last updated on <b>${wplaceUpdateTimeLocalized}</b>.${(this.schemaHealth != 'Good') ? recoveryInstructions : ''}`);
+    
+    // Create button options (only if schema is not 'Dead')
+    const buttonOptions = new Overlay(this.name, this.version);
+    if (this.schemaHealth != 'Dead') {
+      buttonOptions.addDiv({'class': 'bm-container bm-flex-center bm-center-vertically', 'style': 'gap: 1.5ch;'})
+        buttonOptions.addButton({'textContent': 'Download all templates'}, (instance, button) => {
+          button.onclick = () => {
+            button.disabled = true;
+            this.templateManager.downloadAllTemplatesFromStorage().then(() => {
+              button.disabled = false;
+            })
+          };
+        }).buildElement();
+      // Leave the container open for the next button to be added
+    }
+    // If the schema health is Poor or Bad, then show update option
+    if ((this.schemaHealth == 'Poor') || (this.schemaHealth == 'Bad')) {
+      buttonOptions.addButton({'textContent': `Update template storage to ${this.schemaVersionBleedingEdge}`}, (instance, button) => {
+        button.onclick = () => {
+
+          button.disabled = true; // Disables the button
+
+          // Converts the template schema from 1.x.x to 2.x.x
+          this.#convertSchema_1_x_x_To_2_x_x(true);
+        };
+      }).buildElement();
+    }
+
+    // Add the button options DOM tree to the actual DOM tree
+    buttonOptions.buildElement().buildOverlay(document.querySelector('#bm-wizard-status').parentNode);
+  }
+
+  /** Displays loaded templates to the user.
+   * @since 0.88.441
+   */
+  #displayTemplateList() {
+
+    const templates = this.currentJSON?.templates; // Templates in user storage
+
+    // If there is at least one template loaded...
+    if (Object.keys(templates).length > 0) {
+
+      // Obtains the parent element for the template list
+      const templateListParentElement = document.querySelector(`#${this.windowID} .bm-scrollable`);
+
+      // Creates the template list DOM tree
+      const templateList = new Overlay(this.name, this.version);
+      templateList.addDiv({'id': 'bm-wizard-tlist', 'class': 'bm-container'})
+
+      // For each template...
+      for (const template in templates) {
+
+        const templateKey = template; // The identification key for the template. E.g., "0 $Z"
+        const templateValue = templates[template]; // The actual content of the template
+
+        // If the template is a direct child of the templates Object...
+        if (templates.hasOwnProperty(template)) {
+
+          // Obtain template information
+          const templateKeyArray = templateKey.split(' '); // E.g., "0 $Z" -> ["0", "$Z"]
+          const sortID = Number(templateKeyArray?.[0]); // Sort ID of the template
+          const authorID = encodedToNumber(templateKeyArray?.[1] || '0', this.templateManager.encodingBase); // User ID of the person who exported the template
+          const displayName = templateValue.name || `Template ${sortID || ''}`; // Display name of the template
+          const coords = templateValue?.coords?.split(',').map(Number); // "1,2,3,4" -> [1, 2, 3, 4]
+          const totalPixelCount = templateValue.pixels?.total ?? undefined;
+          const templateImage = undefined; // TODO: Add template image
+
+          // Localization of information to display to the user
+          const sortIDLocalized = (typeof sortID == 'number') ? localizeNumber(sortID) : '???';
+          const authorIDLocalized = (typeof authorID == 'number') ? localizeNumber(authorID) : '???';
+          const totalPixelCountLocalized = (typeof totalPixelCount == 'number') ? localizeNumber(totalPixelCount) : '???';
+
+          templateList.addDiv({'class': 'bm-container bm-flex-center'})
+            .addDiv({'class': 'bm-flex-center', 'style': 'flex-direction: column; gap: 0;'})
+              .addDiv({'class': 'bm-wizard-template-container-image', 'textContent': templateImage || '🖼️'})
+                // TODO: Add image element and SVG fallback
+              .buildElement()
+              .addSmall({'textContent': `#${sortIDLocalized}`}).buildElement()
+            .buildElement()
+            .addDiv({'class': 'bm-flex-center bm-wizard-template-container-flavor'})
+              .addHeader(3, {'textContent': displayName}).buildElement()
+              .addSpan({'textContent': `Uploaded by user #${authorIDLocalized}`}).buildElement()
+              .addSpan({'textContent': `Coordinates: ${coords.join(', ')}`}).buildElement()
+              .addSpan({'textContent': `Total Pixels: ${totalPixelCountLocalized}`}).buildElement()
+            .buildElement()
+          .buildElement()
+        }
+      }
+
+      // Adds the template list to the real DOM tree
+      templateList.buildElement().buildOverlay(templateListParentElement);
+    }
+  }
+
+  /** Converts schema version 1.0.0 to schema version 2.0.0.
+   * @param {boolean} shouldWindowWizardOpen - Should we open a new Template Wizard window after schema conversion? This will close any Template Wizard already open.
+   * @since 0.88.504
+   */
+  async #convertSchema_1_x_x_To_2_x_x(shouldWindowWizardOpen) {
+
+    // Creates loading screen
+    if (shouldWindowWizardOpen) {
+      
+      // Obtains the Template Wizard window content container
+      const windowContent = document.querySelector(`#${this.windowID} .bm-window-content`);
+
+      // Deletes all content in the Template Wizard window content container
+      windowContent.innerHTML = '';
+
+      const loadingScreen = new Overlay(this.name, this.version);
+      loadingScreen.addDiv({'class': 'bm-container'})
+        .addDiv({'class': 'bm-container bm-center-vertically'})
+          .addHeader(1, {'textContent': 'Template Wizard'}).buildElement()
+        .buildElement()
+        .addHr().buildElement()
+        .addDiv({'class': 'bm-container'})
+          .addHeader(2, {'textContent': 'Status'}).buildElement()
+          .addP({'textContent': 'Updating template storage. Please wait...'}).buildElement()
+        .buildElement()
+      .buildElement().buildOverlay(windowContent);
+    }
+
+    // Deletes the bmCoords value set in 1.0.0 which is unused in 2.0.0
+    GM_deleteValue('bmCoords');
+
+    // Obtains the templates from JSON storage
+    const templates = this.currentJSON?.templates;
+
+    // If there is at least one template loaded...
+    if (Object.keys(templates).length > 0) {
+
+      // For each template loaded...
+      for (const [key, template] of Object.entries(templates)) {
+
+        // If the template is a direct child of the templates Object...
+        if (templates.hasOwnProperty(key)) {
+
+          // Creates a dummy Template class instance
+          const _template = new Template({
+            displayName: template.name,
+            chunked: template.tiles
+          });
+
+          _template.calculateCoordsFromChunked(); // Updates `Template.coords`
+
+          // Converts the template to a Blob
+          const blob = await this.templateManager.convertTemplateToBlob(_template);
+
+          // Uses the information from the dummy Template class instance to make the actual Template
+          await this.templateManager.createTemplate(blob, _template.displayName, _template.coords);
+        }
+      }
+    }
+
+    // If it has been requested that we open a new Template Wizard window, we do so
+    if (shouldWindowWizardOpen) {
+      console.log(`Restarting Template Wizard...`);
+      document.querySelector(`#${this.windowID}`).remove();
+      new WindowWizard(this.name, this.version, this.schemaVersionBleedingEdge, this.templateManager).buildWindow();
+    }
+  }
+}
+
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/apiManager.js.html b/docs/apiManager.js.html new file mode 100644 index 0000000..1816554 --- /dev/null +++ b/docs/apiManager.js.html @@ -0,0 +1,363 @@ + + + + + + apiManager.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

apiManager.js

+ + + + + + + +
+
+
/** ApiManager class for handling API requests, responses, and interactions.
+ * Note: Fetch spying is done in main.js, not here.
+ * @class ApiManager
+ * @since 0.11.1
+ */
+
+import TemplateManager from "./templateManager.js";
+import { consoleError, escapeHTML, localizeNumber, 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.chargeRefillTimerID = ''; // Contains the Charge refill timer element ID attribute so we can update the timer.
+    this.coordsTilePixel = []; // Contains the last detected tile/pixel coordinate pair requested
+    this.templateCoordsTilePixel = []; // Contains the last "enabled" template coords
+  }
+
+  /** Determines if the spontaneously received 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 or Wplace is offline!\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'];
+
+          // Obtains the refill timer for charges
+          if (this.chargeRefillTimerID.length != 0) {
+            const chargeRefillTimer = document.querySelector('#' + this.chargeRefillTimerID);
+            
+            // If the refill timer exists...
+            if (chargeRefillTimer) {
+              
+              /** Obtains the information about the user's charges @type {{cooldownMs: number, count: number, max: number}} */
+              const chargeData = dataJSON['charges'];
+  
+              // Date that the user's charges will be refilled
+              chargeRefillTimer.dataset['endDate'] = Date.now() + ((chargeData['max'] - chargeData['count']) * chargeData['cooldownMs']);
+            }
+          }
+
+          // Updates displayed droplet information
+          overlay.updateInnerHTML('bm-user-droplets', `Droplets: <b>${localizeNumber(dataJSON['droplets'])}</b>`); // Updates the text content of the droplets field
+          overlay.updateInnerHTML('bm-user-nextlevel', `Next level in <b>${localizeNumber(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); // Retrieves the coordinates that Wplace displays for this region
+
+          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) {
+            // We use the pixel numbers to find this element because it is the only identifiable piece of information, assuming the website can load in non-Engligh languages.
+
+            const elementTextTrimmed = element.textContent.trim(); // Stores the text of the span element, without leading or trailing spaces
+
+            // If the text content of the element includes both coordinates seperatly (avoids failure when the comma seperator changes due to localization)
+            if (elementTextTrimmed.includes(displayTP[0]) && elementTextTrimmed.includes(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]})`;
+              
+              // All 4 coordinate labels, IDs, and values
+              const coordsLabel = ['Tl X:', 'Tl Y:', 'Px X:', 'Px Y:'];
+              const coordsID = ['bm-tile-x', 'bm-tile-y', 'bm-pixel-x', 'bm-pixel-y'];
+              const coordsCombined = [...coordsTile, ...coordsPixel];
+
+              // 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.style = 'display: flex; flex-wrap: wrap; gap: 0 1ch; font-size: small;';
+
+                // For each of the 4 coordinates...
+                for (const [coordIndex, coordValue] of coordsCombined.entries()) {
+
+                  const coordElement = document.createElement('span'); // Creates a `<span>` element
+
+                  coordElement.id = coordsID[coordsCombined.indexOf(coordValue) ?? '']; // Applys the ID to the coord element
+
+                  // Outputs something like "Tl X: 483"
+                  coordElement.textContent = `${coordsLabel[coordIndex] ?? '??:'} ${coordValue}`;
+                  // Or if the amount of labels is less than the provided values, it outputs something like "??: 483" instead of failing
+
+                  displayCoords.appendChild(coordElement); // Adds the span coordinate as a child for the flexbox container
+                }
+
+                // Adds the display coordinate flexbox container to the pixel info menu
+                element.parentNode.parentNode.parentNode.insertAdjacentElement('afterend', displayCoords);
+              } else {
+                
+                // For each of the 4 coordinates...
+                for (const [coordIndex, coordID] of coordsID.entries()) {
+
+                  const coordElement = document.getElementById(coordID); // Obtains the coordinate element
+
+                  // Outputs something like "Tl X: 483"
+                  coordElement.textContent = `${coordsLabel[coordIndex] ?? '??:'} ${coordsCombined[coordIndex]}`;
+                  // Or if the amount of labels is less than the provided values, it outputs something like "??: 483" instead of failing
+                }
+              }
+            }
+          }
+          break;
+        
+        case 'tile':
+        case 'tiles':
+
+          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 timer = Date.now();
+          const templateBlob = await this.templateManager.drawTemplateOnTile(blobData, tileCoordsTile);
+          console.log(`Finished loading the tile in ${(Date.now() - timer) / 1000} seconds!`);
+
+          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;
+      }
+    });
+  }
+
+  // Sends a heartbeat to the telemetry server
+  async sendHeartbeat(version) {
+
+    console.log('Sending heartbeat to telemetry server...');
+
+    let userSettings = GM_getValue('bmUserSettings', '{}')
+    userSettings = JSON.parse(userSettings);
+
+    if (!userSettings || !userSettings.telemetry || !userSettings.uuid) {
+      console.log('Telemetry is disabled, not sending heartbeat.');
+      return; // If telemetry is disabled, do not send heartbeat
+    }
+
+    const ua = navigator.userAgent;
+    let browser = await this.getBrowserFromUA(ua);
+    let os = this.getOS(ua);
+
+    GM_xmlhttpRequest({
+      method: 'POST',
+      url: 'https://telemetry.thebluecorner.net/heartbeat',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      data: JSON.stringify({
+        uuid: userSettings.uuid,
+        version: version,
+        browser: browser,
+        os: os,
+      }),
+      onload: (response) => {
+        if (response.status !== 200) {
+          consoleError('Failed to send heartbeat:', response.statusText);
+        }
+      },
+      onerror: (error) => {
+        consoleError('Error sending heartbeat:', error);
+      }
+    });
+  }
+
+  async getBrowserFromUA(ua = navigator.userAgent) {
+    ua = ua || "";
+
+    // Opera
+    if (ua.includes("OPR/") || ua.includes("Opera")) return "Opera";
+
+    // Edge (Chromium-based uses "Edg/")
+    if (ua.includes("Edg/")) return "Edge";
+
+    // Vivaldi
+    if (ua.includes("Vivaldi")) return "Vivaldi";
+
+    // Yandex
+    if (ua.includes("YaBrowser")) return "Yandex";
+
+    // Kiwi (not guaranteed, but typically shows "Kiwi")
+    if (ua.includes("Kiwi")) return "Kiwi";
+
+    // Brave (doesn't expose in UA by default; heuristic via Brave/ token in some versions)
+    if (ua.includes("Brave")) return "Brave";
+
+    // Firefox
+    if (ua.includes("Firefox/")) return "Firefox";
+
+    // Chrome (catch-all for Chromium browsers)
+    if (ua.includes("Chrome/")) return "Chrome";
+
+    // Safari (must be after Chrome check)
+    if (ua.includes("Safari/")) return "Safari";
+
+    // Brave special check
+    if (navigator.brave && typeof navigator.brave.isBrave === "function") {
+      if (await navigator.brave.isBrave()) return "Brave";
+    }
+
+    // Fallback
+    return 'Unknown';
+  }
+
+  getOS(ua = navigator.userAgent) {
+    ua = ua || "";
+
+    if (/Windows NT 11/i.test(ua)) return "Windows 11";
+    if (/Windows NT 10/i.test(ua)) return "Windows 10";
+    if (/Windows NT 6\.3/i.test(ua)) return "Windows 8.1";
+    if (/Windows NT 6\.2/i.test(ua)) return "Windows 8";
+    if (/Windows NT 6\.1/i.test(ua)) return "Windows 7";
+    if (/Windows NT 6\.0/i.test(ua)) return "Windows Vista";
+    if (/Windows NT 5\.1|Windows XP/i.test(ua)) return "Windows XP";
+
+    if (/Mac OS X 10[_\.]15/i.test(ua)) return "macOS Catalina";
+    if (/Mac OS X 10[_\.]14/i.test(ua)) return "macOS Mojave";
+    if (/Mac OS X 10[_\.]13/i.test(ua)) return "macOS High Sierra";
+    if (/Mac OS X 10[_\.]12/i.test(ua)) return "macOS Sierra";
+    if (/Mac OS X 10[_\.]11/i.test(ua)) return "OS X El Capitan";
+    if (/Mac OS X 10[_\.]10/i.test(ua)) return "OS X Yosemite";
+    if (/Mac OS X 10[_\.]/i.test(ua)) return "macOS"; // Generic fallback
+
+    if (/Android/i.test(ua)) return "Android";
+    if (/iPhone|iPad|iPod/i.test(ua)) return "iOS";
+
+    if (/Linux/i.test(ua)) return "Linux";
+
+    return "Unknown";
+  }
+}
+
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/confetttiManager.js.html b/docs/confetttiManager.js.html new file mode 100644 index 0000000..3197472 --- /dev/null +++ b/docs/confetttiManager.js.html @@ -0,0 +1,123 @@ + + + + + + confetttiManager.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

confetttiManager.js

+ + + + + + + +
+
+
import { colorpalette } from "./utils";
+
+/** Manages the confetti animation.
+ * @description Manages any confetti animation used by Blue Marble.
+ * @class ConfettiManager
+ * @since 0.88.356
+ */
+export default class ConfettiManager {
+
+  /** The constructor for the confetti manager.
+   * @since 0.88.356
+   */
+  constructor() {
+
+    // Shards of confetti to spawn
+    this.confettiCount = Math.ceil((80 / 1300) * window.innerWidth);
+    // Number of confetti is based on screen width
+
+    this.colorPalette = colorpalette.slice(1); // Color palette for confetti
+  }
+
+  /** Immedently creates confetti inside the parent element.
+   * @param {HTMLElement} parentElement - The parent element to create confetti inside of
+   * @since 0.88.356
+   */
+  createConfetti(parentElement) {
+
+    const confettiContainer = document.createElement('div'); // Creates confetti container
+
+    // For each piece of confetti to spawn...
+    for (let currentCount = 0; currentCount < this.confettiCount; currentCount++) {
+
+      // Creates a new confetti shard
+      const confettiShard = document.createElement('confetti-piece');
+
+      // Randomly generates the variables that the CSS needs
+      confettiShard.style.setProperty('--x', `${Math.random() * 100}vw`);
+      confettiShard.style.setProperty('--delay', `${Math.random() * 2}s`);
+      confettiShard.style.setProperty('--duration', `${3 + Math.random() * 3}s`);
+      confettiShard.style.setProperty('--rot', `${Math.random() * 360}deg`);
+      confettiShard.style.setProperty('--size', `${6 + Math.random() * 6}px`);
+      confettiShard.style.backgroundColor = `rgb(${this.colorPalette[Math.floor(Math.random() * this.colorPalette.length)].rgb.join(',')})`;
+
+      // Deletes the confetti shard when the animation ends
+      confettiShard.onanimationend = () => {
+        if (confettiShard.parentNode.childElementCount <= 1) {
+          confettiShard.parentNode.remove(); // Deletes the container instead if no confetti is left
+        } else {
+          confettiShard.remove(); // Removes the confetti shard
+        }
+      }
+
+      confettiContainer.appendChild(confettiShard); // Adds the confetti shard to the container
+    }
+
+    // Adds the container to the parent element
+    parentElement.appendChild(confettiContainer);
+  }
+}
+
+// Creates a custom element called <confetti-piece>
+class BlueMarbleConfettiPiece extends HTMLElement {}
+customElements.define('confetti-piece', BlueMarbleConfettiPiece);
+
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/fonts/OpenSans-Bold-webfont.eot b/docs/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 0000000..5d20d91 Binary files /dev/null and b/docs/fonts/OpenSans-Bold-webfont.eot differ diff --git a/docs/fonts/OpenSans-Bold-webfont.svg b/docs/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 0000000..3ed7be4 --- /dev/null +++ b/docs/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Bold-webfont.woff b/docs/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 0000000..1205787 Binary files /dev/null and b/docs/fonts/OpenSans-Bold-webfont.woff differ diff --git a/docs/fonts/OpenSans-BoldItalic-webfont.eot b/docs/fonts/OpenSans-BoldItalic-webfont.eot new file mode 100644 index 0000000..1f639a1 Binary files /dev/null and b/docs/fonts/OpenSans-BoldItalic-webfont.eot differ diff --git a/docs/fonts/OpenSans-BoldItalic-webfont.svg b/docs/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100644 index 0000000..6a2607b --- /dev/null +++ b/docs/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-BoldItalic-webfont.woff b/docs/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 0000000..ed760c0 Binary files /dev/null and b/docs/fonts/OpenSans-BoldItalic-webfont.woff differ diff --git a/docs/fonts/OpenSans-Italic-webfont.eot b/docs/fonts/OpenSans-Italic-webfont.eot new file mode 100644 index 0000000..0c8a0ae Binary files /dev/null and b/docs/fonts/OpenSans-Italic-webfont.eot differ diff --git a/docs/fonts/OpenSans-Italic-webfont.svg b/docs/fonts/OpenSans-Italic-webfont.svg new file mode 100644 index 0000000..e1075dc --- /dev/null +++ b/docs/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Italic-webfont.woff b/docs/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 0000000..ff652e6 Binary files /dev/null and b/docs/fonts/OpenSans-Italic-webfont.woff differ diff --git a/docs/fonts/OpenSans-Light-webfont.eot b/docs/fonts/OpenSans-Light-webfont.eot new file mode 100644 index 0000000..1486840 Binary files /dev/null and b/docs/fonts/OpenSans-Light-webfont.eot differ diff --git a/docs/fonts/OpenSans-Light-webfont.svg b/docs/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 0000000..11a472c --- /dev/null +++ b/docs/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Light-webfont.woff b/docs/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 0000000..e786074 Binary files /dev/null and b/docs/fonts/OpenSans-Light-webfont.woff differ diff --git a/docs/fonts/OpenSans-LightItalic-webfont.eot b/docs/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 0000000..8f44592 Binary files /dev/null and b/docs/fonts/OpenSans-LightItalic-webfont.eot differ diff --git a/docs/fonts/OpenSans-LightItalic-webfont.svg b/docs/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 0000000..431d7e3 --- /dev/null +++ b/docs/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-LightItalic-webfont.woff b/docs/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 0000000..43e8b9e Binary files /dev/null and b/docs/fonts/OpenSans-LightItalic-webfont.woff differ diff --git a/docs/fonts/OpenSans-Regular-webfont.eot b/docs/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 0000000..6bbc3cf Binary files /dev/null and b/docs/fonts/OpenSans-Regular-webfont.eot differ diff --git a/docs/fonts/OpenSans-Regular-webfont.svg b/docs/fonts/OpenSans-Regular-webfont.svg new file mode 100644 index 0000000..25a3952 --- /dev/null +++ b/docs/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Regular-webfont.woff b/docs/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 0000000..e231183 Binary files /dev/null and b/docs/fonts/OpenSans-Regular-webfont.woff differ diff --git a/docs/fonts/OpenSans-Semibold-webfont.eot b/docs/fonts/OpenSans-Semibold-webfont.eot new file mode 100755 index 0000000..d8375dd Binary files /dev/null and b/docs/fonts/OpenSans-Semibold-webfont.eot differ diff --git a/docs/fonts/OpenSans-Semibold-webfont.svg b/docs/fonts/OpenSans-Semibold-webfont.svg new file mode 100755 index 0000000..eec4db8 --- /dev/null +++ b/docs/fonts/OpenSans-Semibold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Semibold-webfont.ttf b/docs/fonts/OpenSans-Semibold-webfont.ttf new file mode 100755 index 0000000..b329084 Binary files /dev/null and b/docs/fonts/OpenSans-Semibold-webfont.ttf differ diff --git a/docs/fonts/OpenSans-Semibold-webfont.woff b/docs/fonts/OpenSans-Semibold-webfont.woff new file mode 100755 index 0000000..28d6ade Binary files /dev/null and b/docs/fonts/OpenSans-Semibold-webfont.woff differ diff --git a/docs/fonts/OpenSans-SemiboldItalic-webfont.eot b/docs/fonts/OpenSans-SemiboldItalic-webfont.eot new file mode 100755 index 0000000..0ab1db2 Binary files /dev/null and b/docs/fonts/OpenSans-SemiboldItalic-webfont.eot differ diff --git a/docs/fonts/OpenSans-SemiboldItalic-webfont.svg b/docs/fonts/OpenSans-SemiboldItalic-webfont.svg new file mode 100755 index 0000000..7166ec1 --- /dev/null +++ b/docs/fonts/OpenSans-SemiboldItalic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf b/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf new file mode 100755 index 0000000..d2d6318 Binary files /dev/null and b/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf differ diff --git a/docs/fonts/OpenSans-SemiboldItalic-webfont.woff b/docs/fonts/OpenSans-SemiboldItalic-webfont.woff new file mode 100755 index 0000000..d4dfca4 Binary files /dev/null and b/docs/fonts/OpenSans-SemiboldItalic-webfont.woff differ diff --git a/docs/global.html b/docs/global.html new file mode 100644 index 0000000..3e8192f --- /dev/null +++ b/docs/global.html @@ -0,0 +1,18797 @@ + + + + + + Global - Documentation + + + + + + + + + + + + + + + + + +
+ +

Global

+ + + + + + + +
+ +
+ +

+ +

+ + +
+ +
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + +

Members

+ + + +
+

(constant) colorpalette

+ + + + +
+

The color palette used by wplace.live

+
+ + + + + +
+ + + + +
Since:
+
  • 0.78.0
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + +
+ + + + + +

Methods

+ + + +
+ + + +

addBr(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.11
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the br that are NOT shared between all overlay br elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the br.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addButton(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.12
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the button that are NOT shared between all overlay button elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the button.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addButtonHelp(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.12
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the button that are NOT shared between all overlay button elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the button.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Examples
+ +
// 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>
+ +
// 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>
+ +
+ +
+ + +
+ + + +

addCaption(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a table caption to the overlay. +This caption element will have properties shared between all caption elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the caption that are NOT shared between all overlay caption elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the caption.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <caption> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addCaption({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <caption id="foo" class="bar">Foobar.</caption>
+</body>
+ +
+ +
+ + +
+ + + +

addCheckbox(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.10
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the checkbox that are NOT shared between all overlay checkbox elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the checkbox.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addDetails(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a details to the overlay. +This details element will have properties shared between all details elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.96
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the details that are NOT shared between all overlay details elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the details.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <details> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addDetails({'id': 'foo'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <details id="foo" class="bar"></details>
+</body>
+ +
+ +
+ + +
+ + + +

addDiv(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the div that are NOT shared between all overlay div elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the div.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addDragbar(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a dragbar div element to the overlay. +This dragbar element will have properties shared between all dragbar elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.145
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the dragbar that are NOT shared between all overlay dragbars. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the dragbar.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all dragbar elements have a shared class (e.g. {'className': 'bar'})
+overlay.addDragbar({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <div id="foo" class="bar">Foobar.</div>
+</body>
+ +
+ +
+ + +
+ + + +

addFieldset(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a fieldset to the overlay. +This fieldset element will have properties shared between all fieldset elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.246
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the fieldset that are NOT shared between all overlay fieldset elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the fieldset.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <fieldset> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addFieldset({'id': 'foo'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <fieldset id="foo" class="bar"></fieldset>
+</body>
+ +
+ +
+ + +
+ + + +

addForm(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a form to the overlay. +This form element will have properties shared between all form elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.246
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the form that are NOT shared between all overlay form elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the form.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <form> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addForm({'id': 'foo'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <form id="foo" class="bar"></form>
+</body>
+ +
+ +
+ + +
+ + + +

addHeader(level, additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.7
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
level + + +number + + + + + + + + + + + + +

The header level. Must be between 1 and 6 (inclusive)

+ +
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the header that are NOT shared between all overlay header elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the header.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addHr(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.7
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the hr that are NOT shared between all overlay hr elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the hr.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addImg(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the img that are NOT shared between all overlay img elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the img.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addInput(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.13
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the input that are NOT shared between all overlay input elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the input.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addInputFile(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.17
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the file input that are NOT shared between all overlay file input elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the file input.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addLegend(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a legend to the overlay. +This legend element will have properties shared between all legend elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.246
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the legend that are NOT shared between all overlay legend elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the legend.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <legend> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addLegend({'id': 'foo', textContent: 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <legend id="foo" class="bar">
+    "Foobar."
+  </legend>
+</body>
+ +
+ +
+ + +
+ + + +

addLi(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a list item to the overlay. +This li element will have properties shared between all li elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the li that are NOT shared between all overlay li elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the li.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <li> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addLi({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <li id="foo" class="bar">Foobar.</li>
+</body>
+ +
+ +
+ + +
+ + + +

addMenu(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a menu to the overlay. +This menu element will have properties shared between all menu elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the menu that are NOT shared between all overlay menu elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the menu.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <menu> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addMenu({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <menu id="foo" class="bar">Foobar.</menu>
+</body>
+ +
+ +
+ + +
+ + + +

addOl(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds an ordered list to the overlay. +This ol element will have properties shared between all ol elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the ol that are NOT shared between all overlay ol elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the ol.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <ol> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addOl({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <ol id="foo" class="bar">Foobar.</ol>
+</body>
+ +
+ +
+ + +
+ + + +

addOption(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds an option to the overlay. +This option element will have properties shared between all option elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.244
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the option that are NOT shared between all overlay option elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the option.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <option> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addOption({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <option id="foo" class="bar">Foobar.</option>
+</body>
+ +
+ +
+ + +
+ + + +

addP(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the p that are NOT shared between all overlay p elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the p.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addSelect(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a label & select element to the overlay. +This select element will have properties shared between all select elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.243
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the checkbox that are NOT shared between all overlay select elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the label/select elements.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all select elements have a shared class (e.g. {'className': 'bar'})
+overlay.addSelect({'id': 'foo', 'textContent': 'Foobar: '}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <label for="foo">
+    "Foobar: "
+  </label>
+  <select id="foo" class="bar"></select>
+</body>
+ +
+ +
+ + +
+ + + +

addSmall(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.55.8
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the small that are NOT shared between all overlay small elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the small.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addSpan(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a span to the overlay. +This span element will have properties shared between all span elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.55.8
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the span that are NOT shared between all overlay span elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the span.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <span> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addSpan({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <span id="foo" class="bar">Foobar.</span>
+</body>
+ +
+ +
+ + +
+ + + +

addSummary(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a summary to the overlay. +This summary element will have properties shared between all summary elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.96
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the summary that are NOT shared between all overlay summary elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the summary.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <summary> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addSummary({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <summary id="foo" class="bar">Foobar.</summary>
+</body>
+ +
+ +
+ + +
+ + + +

addTable(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a table to the overlay. +This table element will have properties shared between all table elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the table that are NOT shared between all overlay table elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the table.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <table> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addTable({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <table id="foo" class="bar">Foobar.</table>
+</body>
+ +
+ +
+ + +
+ + + +

addTbody(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a table body to the overlay. +This tbody element will have properties shared between all tbody elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the tbody that are NOT shared between all overlay tbody elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the tbody.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <tbody> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addTbody({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <tbody id="foo" class="bar">Foobar.</tbody>
+</body>
+ +
+ +
+ + +
+ + + +

addTd(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a table data cell to the overlay. +This td element will have properties shared between all td elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the td that are NOT shared between all overlay td elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the td.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <td> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addTd({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <td id="foo" class="bar">Foobar.</td>
+</body>
+ +
+ +
+ + +
+ + + +

addTextarea(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.13
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the textarea that are NOT shared between all overlay textarea elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the textarea.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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>
+ +
+ +
+ + +
+ + + +

addTfoot(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a table footer to the overlay. +This tfoot element will have properties shared between all tfoot elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the tfoot that are NOT shared between all overlay tfoot elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the tfoot.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <tfoot> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addTfoot({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <tfoot id="foo" class="bar">Foobar.</tfoot>
+</body>
+ +
+ +
+ + +
+ + + +

addTh(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a table header (label) cell to the overlay. +This th element will have properties shared between all th elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the th that are NOT shared between all overlay th elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the th.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <th> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addTh({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <th id="foo" class="bar">Foobar.</th>
+</body>
+ +
+ +
+ + +
+ + + +

addThead(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a table header to the overlay. +This thead element will have properties shared between all thead elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the thead that are NOT shared between all overlay thead elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the thead.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <thead> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addThead({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <thead id="foo" class="bar">Foobar.</thead>
+</body>
+ +
+ +
+ + +
+ + + +

addTimer(endDateopt, updateIntervalopt, additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a timer time element to the overlay. +This timer will countdown until it reaches the end date that was passed in. +Additionally, you can update the end date by changing the endDate dataset attribute on the element. +Timer elements are not localized. Output is HH:MM:SS with no units. +This timer will have properties shared between all timers in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.313
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
endDate + + +Date + + + + + + <optional>
+ + + + + +
+ + Date.now() + + +

The time to count down to.

+ +
updateInterval + + +number + + + + + + <optional>
+ + + + + +
+ + 500 + + +

The time in milliseconds to update the display of the timer. Default is 500 milliseconds.

+ +
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the timer that are NOT shared between all overlay timers. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the timer.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all timers have a shared class (e.g. {'className': 'bar'})
+overlay.addTimer(Date.now() + 2211632704000, 500, {'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <time id="bm-timer-dh8fhw80" class="bar" datetime="PT27H34M56S" data-end-date="1771749296000">27:34:56</div>
+</body>
+ +
+ +
+ + +
+ + + +

addTr(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds a table row to the overlay. +This tr element will have properties shared between all tr elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the tr that are NOT shared between all overlay tr elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the tr.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <tr> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addTr({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <tr id="foo" class="bar">Foobar.</tr>
+</body>
+ +
+ +
+ + +
+ + + +

addUl(additionalPropertiesopt, callbackopt) → {Overlay}

+ + + + + +
+

Adds an unordered list to the overlay. +This ul element will have properties shared between all ul elements in the overlay. +You can override the shared properties by using a callback.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
additionalProperties + + +Object.<string, any> + + + + + + <optional>
+ + + + + +
+ + {} + + +

The DOM properties of the ul that are NOT shared between all overlay ul elements. These should be camelCase.

+ +
callback + + +function + + + + + + <optional>
+ + + + + +
+ + ()=>{} + + +

Additional JS modification to the ul.

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
Example
+ +
// Assume all <ul> elements have a shared class (e.g. {'className': 'bar'})
+overlay.addUl({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body);
+// Output:
+// (Assume <body> already exists in the webpage)
+<body>
+  <ul id="foo" class="bar">Foobar.</ul>
+</body>
+ +
+ +
+ + +
+ + + +

base64ToUint8(base64) → {Uint8Array}

+ + + + + +
+

Decodes a base 64 encoded Uint8 array using the browser's built-in ASCII to binary function

+
+ + + + + +
+ + + + +
Since:
+
  • 0.72.9
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
base64 + + +Uint8Array + + + + +

The base 64 encoded Uint8Array to convert

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Uint8Array + + +
+
+ + +
+

The decoded Uint8Array

+
+ + +
+ + + +
+ + +
+ + + +

buildElement() → {Overlay}

+ + + + + +
+

Finishes building an element. +Call this after you are finished adding children. +If the element will have no children, call it anyways.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Overlay + + +
+
+ + +
+

Overlay class instance (this)

+
+ + +
+ + + +
+
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);
+ +
+ +
+ + +
+ + + +

buildHighlight()

+ + + + + +
+

Builds the highlight section of the window. +This should be overriden by SettingsManager

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.11
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

buildHighlight()

+ + + + + +
+

Builds the "highlight" category of the settings window

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.18
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+
    +
  • WindowSettings#buildHighlight
  • +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

buildOverlay(parent)

+ + + + + +
+

Finishes building the overlay and displays it. +Call this when you are done chaining methods.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
parent + + +HTMLElement + + + + +

The parent HTMLElement this overlay should be appended to as a child.

+ +
+ + + + + + + + + + + + + + + + +
+
Example
+ +
overlay
+  .addDiv()
+    .addP().buildElement()
+  .buildElement()
+.buildOverlay(document.body); // Adds DOM structure to document body
+// <div><p></p></div>
+ +
+ +
+ + +
+ + + +

buildTemplate()

+ + + + + +
+

Builds the template section of the window. +This should be overriden by SettingsManager

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.68
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

buildTemplate()

+ + + + + +
+

Build the "template" category of settings window

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.68
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+
    +
  • WindowSettings#buildTemplate
  • +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

buildWindow()

+ + + + + +
+

Creates the main Blue Marble window. +Parent/child relationships in the DOM structure below are indicated by indentation.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.58.3
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

buildWindow()

+ + + + + +
+

Spawns a Color Filter window. +If another color filter window already exists, we DON'T spawn another! +Parent/child relationships in the DOM structure below are indicated by indentation.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.149
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

(async) buildWindow()

+ + + + + +
+

Spawns a telemetry window. +If another telemetry window already exists, we DON'T spawn another! +Parent/child relationships in the DOM structure below are indicated by indentation.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.339
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

buildWindow()

+ + + + + +
+

Spawns a Template Wizard window. +If another template wizard window already exists, we DON'T spawn another! +Parent/child relationships in the DOM structure below are indicated by indentation.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.434
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

buildWindow()

+ + + + + +
+

Spawns a Credits window. +If another credits window already exists, we DON'T spawn another! +Parent/child relationships in the DOM structure below are indicated by indentation.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.90.9
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

buildWindow()

+ + + + + +
+

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
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

buildWindowed()

+ + + + + +
+

Spawns a windowed Color Filter window. +If another color filter window already exists, we DON'T spawn another! +Parent/child relationships in the DOM structure below are indicated by indentation.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.90.35
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

calculateCanvasTransparency(param) → {boolean}

+ + + + + +
+

Detects if the canvas is transparent.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.75
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
param + + +Object + + + + +

Object that contains the parameters for the function

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
bitmap + + +ImageBitmap + + + + +

The bitmap template image

+ +
bitmapParams + + +Array.<number, number, number, number> + + + + +

The parameters to obtain the template tile image from the bitmap

+ +
transCanvas + + +OffscreenCanvas +| + +HTMLCanvasElement + + + + +

The canvas to draw to in order to calculate this

+ +
transContext + + +OffscreenCanvasRenderingContext2D + + + + +

The context for the transparent canvas to draw to

+ +
+ + +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +boolean + + +
+
+ + +
+

Is the canvas transparent? If transparent, then true is returned. Otherwise, false.

+
+ + +
+ + + +
+ + +
+ + + +

calculateCoordsFromChunked()

+ + + + + +
+

Calculates top left coordinate of template. +It uses Template.chunked to update Template.coords

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.504
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

calculateRelativeLuminance(array) → {Number}

+ + + + + +
+

Calcualtes the relative luminance of an RGB value

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.180
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
array + + +Array.<Number, Number, Number> + + + + +

The RGB values as an array

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Number + + +
+
+ + +
+

The relative luminance as a Number

+
+ + +
+ + + +
+ + +
+ + + +

colorpaletteForBlueMarble()

+ + + + + +
+

Processes the palette used for Blue Marble. +Each ID is sorted from smallest to largest. +Color ID's are integers, which can be negative. +Custom colors have been added for the Blue Marble purposes. +Wplace palette colors have not been modified.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.6
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

consoleError(…args)

+ + + + + +
+

Bypasses terser's stripping of console function calls. +This is so the non-obfuscated code will contain debugging console calls, but the distributed version won't. +However, the distributed version needs to call the console somehow, so this wrapper function is how. +This is the same as console.error().

+
+ + + + + +
+ + + + +
Since:
+
  • 0.58.13
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
args + + +any + + + + + + + + + + <repeatable>
+ +
+

Arguments to be passed into the error() function of the Console

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

consoleLog(…args)

+ + + + + +
+

Bypasses terser's stripping of console function calls. +This is so the non-obfuscated code will contain debugging console calls, but the distributed version won't. +However, the distributed version needs to call the console somehow, so this wrapper function is how. +This is the same as console.log().

+
+ + + + + +
+ + + + +
Since:
+
  • 0.58.9
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
args + + +any + + + + + + + + + + <repeatable>
+ +
+

Arguments to be passed into the log() function of the Console

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

consoleWarn(…args)

+ + + + + +
+

Bypasses terser's stripping of console function calls. +This is so the non-obfuscated code will contain debugging console calls, but the distributed version won't. +However, the distributed version needs to call the console somehow, so this wrapper function is how. +This is the same as console.warn().

+
+ + + + + +
+ + + + +
Since:
+
  • 0.58.13
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
args + + +any + + + + + + + + + + <repeatable>
+ +
+

Arguments to be passed into the warn() function of the Console

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

(async) convertTemplateToBlob() → {Promise.<Blob>}

+ + + + + +
+

Converts a Template class instance into a Blob. +Specifically, this takes Template.chunked and converts it to a Blob.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.504
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Promise.<Blob> + + +
+
+ + +
+

A Promise of a Blob PNG image of the template

+
+ + +
+ + + +
+ + +
+ + + +

createConfetti(parentElement)

+ + + + + +
+

Immedently creates confetti inside the parent element.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.356
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
parentElement + + +HTMLElement + + + + +

The parent element to create confetti inside of

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

(async) createJSON() → {Object}

+ + + + + +
+

Creates the JSON object to store templates in

+
+ + + + + +
+ + + + +
Since:
+
  • 0.65.4
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Object + + +
+
+ + +
+

The JSON object

+
+ + +
+ + + +
+ + +
+ + + +

createObserverBody(target) → {Observers}

+ + + + + +
+

Creates the MutationObserver for document.body

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
target + + +HTMLElement + + + + +

Targeted element to watch

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Observers + + +
+
+ + +
+

this (Observers class)

+
+ + +
+ + + +
+ + +
+ + + +

(async) createTemplate(blob, name, coords)

+ + + + + +
+

Creates the template from the inputed file blob

+
+ + + + + +
+ + + + +
Since:
+
  • 0.65.77
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
blob + + +File + + + + +

The file blob to create a template from

+ +
name + + +string + + + + +

The display name of the template

+ +
coords + + +Array.<number, number, number, number> + + + + +

The coordinates of the top left corner of the template

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

(async) createTemplateTiles(tileSize, paletteBM, shouldSkipTransTiles, shouldAggSkipTransTiles) → {Object}

+ + + + + +
+

Creates chunks of the template for each tile.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.65.4
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tileSize + + +Number + + + + +

Size of the tile as determined by templateManager

+ +
paletteBM + + +Object + + + + +

An collection of Uint32Arrays containing the palette BM uses

+ +
shouldSkipTransTiles + + +boolean + + + + +

Should transparent tiles be skipped over when creating the template?

+ +
shouldAggSkipTransTiles + + +boolean + + + + +

Should transparent tiles be aggressively skipped over when creating the template?

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Object + + +
+
+ + +
+

Collection of template bitmaps & buffers organized by tile coordinates

+
+ + +
+ + + +
+ + +
+ + + +

deleteTemplate()

+ + + + + +
+

Deletes a template from the JSON object. +Also delete's the corrosponding Template class instance

+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

(async) disableTemplate()

+ + + + + +
+

Disables the template from view

+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

(async) downloadAllTemplates()

+ + + + + +
+

Downloads all templates loaded.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.499
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

(async) downloadAllTemplatesFromStorage()

+ + + + + +
+

Downloads all templates from Blue Marble's template storage.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.474
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

(async) downloadTemplate(template)

+ + + + + +
+

Downloads the template passed-in.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.499
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
template + + +Template + + + + +

The template class instance to download

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

(async) drawTemplateOnTile(tileBlob, tileCoords)

+ + + + + +
+

Draws all templates on the specified tile. +This method handles the rendering of template overlays on individual tiles.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.65.77
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tileBlob + + +File + + + + +

The pixels that are placed on a tile

+ +
tileCoords + + +Array.<number> + + + + +

The tile coordinates [x, y]

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

encodedToNumber(encoded, encoding) → {number}

+ + + + + +
+

Decodes a number from a custom encoded string.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.448
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
encoded + + +string + + + + +

The encoded string

+ +
encoding + + +string + + + + +

The characters to use when decoding

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +number + + +
+
+ + +
+

Decoded number

+
+ + +
+ + + +
+
Example
+ +
const encode = '012abcABC'; // Base 9
+console.log(encodedToNumber('0', encode));     // 0
+console.log(encodedToNumber('c', encode));     // 5
+console.log(encodedToNumber('1A', encode));    // 15
+console.log(encodedToNumber('1BCaA', encode)); // 12345
+ +
+ +
+ + +
+ + + +

escapeHTML(text) → {string}

+ + + + + +
+

Sanitizes HTML to display as plain-text. +This prevents some Cross Site Scripting (XSS). +This is handy when you are displaying user-made data, and you must use innerHTML.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.44.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
text + + +string + + + + +

The text to sanitize

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +string + + +
+
+ + +
+

HTML escaped string

+
+ + +
+ + + +
+
Example
+ +
const paragraph = document.createElement('p');
+paragraph.innerHTML = escapeHTML('<u>Foobar.</u>');
+// Output:
+// (Does not include the paragraph element)
+// (Output is not HTML formatted)
+<p>
+  "<u>Foobar.</u>"
+</p>
+ +
+ +
+ + +
+ + + +

getClipboardData(eventopt) → {string}

+ + + + + +
+

Handles reading from the clipboard. +Assume this only returns text. +Assume this requires user input.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.426
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
event + + +ClipboardEvent + + + + + + <optional>
+ + + + + +
+

(Optional) The clipboard event that triggered this to run

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +string + + +
+
+ + +
+

The clipboard data as a string

+
+ + +
+ + + +
+ + +
+ + + +

getObserverBody() → {MutationObserver}

+ + + + + +
+

Retrieves the MutationObserver that watches document.body

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +MutationObserver + + +
+
+ + + +
+ + + +
+ + +
+ + + +

getWplaceVersion() → {Date|undefined}

+ + + + + +
+

Returns a Date of when Wplace was last updated. +This is obtained from a certain DOM element which contains the version of Wplace.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.90.25
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Date +| + +undefined + + +
+
+ + +
+
    +
  • The date that Wplace was last updated, as a Date.
  • +
+
+ + +
+ + + +
+ + +
+ + + +

handleDisplayError(text)

+ + + + + +
+

Handles error display. +This will output plain text into the output Status box. +Additionally, this will output an error to the console.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.41.6
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
text + + +string + + + + +

The error text to display.

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

handleDisplayStatus(text)

+ + + + + +
+

Handles status display. +This will output plain text into the output Status box. +Additionally, this will output an info message to the console.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.58.4
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
text + + +string + + + + +

The status text to display.

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

handleDrag(moveMeSelector, iMoveThingsSelector)

+ + + + + +
+

Handles dragging of the overlay. +Uses requestAnimationFrame for smooth animations and GPU-accelerated transforms. +Make sure to use the appropriate CSS selectors.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.8.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
moveMeSelector + + +string + + + + +

The element to be moved

+ +
iMoveThingsSelector + + +string + + + + +

The drag handle element

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

handleMinimization(button)

+ + + + + +
+

Handles the minimization logic for windows spawned by Blue Marble

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.142
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
button + + +HTMLButtonElement + + + + +

The UI button that triggered this minimization event

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

hexToRGB(hex) → {Array.<number, number, number>}

+ + + + + +
+

Converts a hexdecimal color to an RGB color. +Alpha channel not supported.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.90.31
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
hex + + +string + + + + +

Hex color code as string

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Array.<number, number, number> + + +
+
+ + +
+

RGB color as an Array

+
+ + +
+ + + +
+ + +
+ + + +

importJSON(json)

+ + + + + +
+

Imports the JSON object, and appends it to any JSON object already loaded

+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
json + + +string + + + + +

The JSON string to parse

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

inject(callback)

+ + + + + +
+

Injects code into the client +This code will execute outside of TamperMonkey's sandbox

+
+ + + + + +
+ + + + +
Since:
+
  • 0.11.15
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
callback + + +* + + + + +

The code to execute

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

localizeDate(date) → {string}

+ + + + + +
+

Returns the localized date format.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.472
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
date + + +number + + + + +

The date to localize

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +string + + +
+
+ + +
+

Localized date as a string

+
+ + +
+ + + +
+ + +
+ + + +

localizeDuration(durationTotalMs) → {string}

+ + + + + +
+

Returns the localized duration format.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.472
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
durationTotalMs + + +number + + + + +

The duration to localize, in milliseconds

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +string + + +
+
+ + +
+

Localized duration as a string

+
+ + +
+ + + +
+ + +
+ + + +

localizeNumber(number) → {string}

+ + + + + +
+

Returns the localized number format.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.472
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
number + + +number + + + + +

The number to localize

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +string + + +
+
+ + +
+

Localized number as a string

+
+ + +
+ + + +
+ + +
+ + + +

localizePercent(percent) → {string}

+ + + + + +
+

Returns the localized percentage format.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.472
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
percent + + +number + + + + +

The percentage to localize

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +string + + +
+
+ + +
+

Localized percentage as a string

+
+ + +
+ + + +
+ + +
+ + + +

negativeSafeModulo(a, b) → {number}

+ + + + + +
+

Negative-Safe Modulo. You can pass negative numbers into this.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.55.8
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • osuplace
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
a + + +number + + + + +

The first number

+ +
b + + +number + + + + +

The second number

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +number + + +
+
+ + +
+

Result

+
+ + +
+ + + +
+ + +
+ + + +

numberToEncoded(number, encoding) → {string}

+ + + + + +
+

Encodes a number into a custom encoded string.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.65.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
number + + +number + + + + +

The number to encode

+ +
encoding + + +string + + + + +

The characters to use when encoding

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +string + + +
+
+ + +
+

Encoded string

+
+ + +
+ + + +
+
Example
+ +
const encode = '012abcABC'; // Base 9
+console.log(numberToEncoded(0, encode)); // 0
+console.log(numberToEncoded(5, encode)); // c
+console.log(numberToEncoded(15, encode)); // 1A
+console.log(numberToEncoded(12345, encode)); // 1BCaA
+ +
+ +
+ + +
+ + + +

observe(observer, watchChildList, watchSubtree)

+ + + + + +
+

Observe a MutationObserver

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
observer + + +MutationObserver + + + + + + +

The MutationObserver

+ +
watchChildList + + +boolean + + + + + + false + + +

(Optional) Should childList be watched? False by default

+ +
watchSubtree + + +boolean + + + + + + false + + +

(Optional) Should childList be watched? False by default

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

observeBlack()

+ + + + + +
+

Observe the black color, and add the "Move" button.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.66.3
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

rgbToHex(red, greenopt, blueopt) → {string}

+ + + + + +
+

Converts an RGB color to hexdecimal color. +Octothorpe not included.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.90.31
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
red + + +number +| + +Array.<number, number, number> + + + + + + + + + + +

The Red channel of the RGB color, or all three channels as an Array

+ +
green + + +number + + + + + + <optional>
+ + + + + +
+

The Green channel of the RGB color

+ +
blue + + +number + + + + + + <optional>
+ + + + + +
+

The Blue channel of the RGB color

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +string + + +
+
+ + +
+

Hex color code as string

+
+ + +
+ + + +
+ + +
+ + + +

selectAllCoordinateInputs() → {Array.<Element>}

+ + + + + +
+

Returns the coordinate input fields

+
+ + + + + +
+ + + + +
Since:
+
  • 0.74.0
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Array.<Element> + + +
+
+ + +
+

The 4 coordinate Inputs

+
+ + +
+ + + +
+ + +
+ + + +

serverTPtoDisplayTP(tile, pixel) → {Array.<number, number>}

+ + + + + +
+

Converts the server tile-pixel coordinate system to the displayed tile-pixel coordinate system.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.42.4
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
tile + + +Array.<string, string> + + + + +

The tile to convert

+ +
pixel + + +Array.<string, string> + + + + +

The pixel to convert

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Array.<number, number> + + +
+
+ + +
+

Tile and pixel coordinate pair

+
+ + +
+ + + +
+
Example
+ +
console.log(serverTPtoDisplayTP(['12', '123'], ['34', '567'])); // [34, 3567]
+ +
+ +
+ + +
+ + + +

setApiManager(apiManager)

+ + + + + +
+

Populates the apiManager variable with the apiManager class.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.41.4
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
apiManager + + +ApiManager + + + + +

The apiManager class instance

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

setSettingsManager(settingsManager)

+ + + + + +
+

Populates the settingsManager variable with the settingsManager class.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.11
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
settingsManager + + +SettingsManager + + + + +

The settingsManager class instance

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

setSettingsManager(settingsManager)

+ + + + + +
+

Updates the stored instance of the SettingsManager.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.54
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
settingsManager + + +SettingsManager + + + + +

The settings manager instance

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

setTemplatesShouldBeDrawn(value)

+ + + + + +
+

Sets the templatesShouldBeDrawn boolean to a value.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.73.7
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
value + + +boolean + + + + +

The value to set the boolean to

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

setWindowMain(windowMain)

+ + + + + +
+

Updates the stored instance of the main window.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.54
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
windowMain + + +WindowMain + + + + +

The main window instance

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

sleep(time) → {Promise}

+ + + + + +
+

Halts execution of this specific userscript, for the specified time. +This will not block the thread.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.483
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
time + + +number + + + + +

Time to wait in milliseconds

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Promise + + +
+
+ + +
+

Promise of a setTimeout()

+
+ + +
+ + + +
+ + +
+ + + +

spontaneousResponseListener(overlay)

+ + + + + +
+

Determines if the spontaneously received 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

+
+ + + + + +
+ + + + +
Since:
+
  • 0.11.1
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
overlay + + +Overlay + + + + +

The Overlay class instance

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

toggleFlag(flagName, stateopt)

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.60
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
flagName + + +string + + + + + + + + + + +

The name of the flag to toggle

+ +
state + + +boolean + + + + + + <optional>
+ + + + + +
+

(Optional) The state to change the flag to

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

uint8ToBase64(uint8) → {Uint8Array}

+ + + + + +
+

Converts a Uint8 array to base64 using the browser's built-in binary to ASCII function

+
+ + + + + +
+ + + + +
Since:
+
  • 0.72.9
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
uint8 + + +Uint8Array + + + + +

The Uint8Array to convert

+ +
+ + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Uint8Array + + +
+
+ + +
+

The base64 encoded Uint8Array

+
+ + +
+ + + +
+ + +
+ + + +

updateColorList() → {Object.<number, ColorData>}

+ + + + + +
+

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
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
Returns:
+ + + +
+
+ Type: +
+
+ +Object.<number, ColorData> + + +
+
+ + + +
+ + + +
+ + +
+ + + +

updateInnerHTML(id, html, doSafeopt)

+ + + + + +
+

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.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.24.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
id + + +string + + + + + + + + + + + + +

The ID of the element to change

+ +
html + + +string + + + + + + + + + + + + +

The HTML/text to update with

+ +
doSafe + + +boolean + + + + + + <optional>
+ + + + + +
+ + false + + +

(Optional) Should textContent be used instead of innerHTML to avoid XSS? False by default

+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ + + +

(async) updateUserStorage()

+ + + + + +
+

Updates the user settings in userscript storage

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.39
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +

viewCanvasInNewTab(canvas, lifeDurationopt)

+ + + + + +
+

View the canvas in a new tab.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.484
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
canvas + + +HTMLCanvasElement +| + +OffscreenCanvas + + + + + + + + + + + + +

The canvas to view

+ +
lifeDuration + + +number + + + + + + <optional>
+ + + + + +
+ + 60_000 + + +

(Optional) The lifetime of the URL blob in milliseconds

+ +
+ + + + + + + + + + + + + + + + +
+ + + + +

Type Definitions

+ + + +
+

ColorData

+ + + + +
+

The information about a specific color on the palette.

+
+ + + + + +
Properties:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
colorTotal + + +number +| + +string + + + +
colorTotalLocalized + + +string + + + +
colorCorrect + + +number +| + +string + + + +
colorCorrectLocalized + + +string + + + +
colorPercent + + +string + + + +
colorIncorrect + + +number + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Type:
+
    +
  • + +Object + + +
  • +
+ + + + + +
+ + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..bb085e4 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,162 @@ + + + + + + Home - Documentation + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +

+ main.js +

+ + +
+ +
+
+ + +

The main file. Everything in the userscript is executed from here.

+ + + + + +
+ + + + +
Since:
+
  • 0.0.0
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/main.js.html b/docs/main.js.html new file mode 100644 index 0000000..44ce400 --- /dev/null +++ b/docs/main.js.html @@ -0,0 +1,344 @@ + + + + + + main.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

main.js

+ + + + + + + +
+
+
/** @file The main file. Everything in the userscript is executed from here.
+ * @since 0.0.0
+ */
+
+import Observers from './observers.js';
+import ApiManager from './apiManager.js';
+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
+const consoleStyle = 'color: cornflowerblue;'; // The styling for the console logs
+
+/** Injects code into the client
+ * This code will execute outside of TamperMonkey's sandbox
+ * @param {*} callback - The code to execute
+ * @since 0.11.15
+ */
+function inject(callback) {
+    const script = document.createElement('script');
+    script.setAttribute('bm-name', name); // Passes in the name value
+    script.setAttribute('bm-cStyle', consoleStyle); // Passes in the console style value
+    script.textContent = `(${callback})();`;
+    document.documentElement?.appendChild(script);
+    script.remove();
+}
+
+/** What code to execute instantly in the client (webpage) to spy on fetch calls.
+ * This code will execute outside of TamperMonkey's sandbox.
+ * @since 0.11.15
+ */
+inject(() => {
+
+  const script = document.currentScript; // Gets the current script HTML Script Element
+  const name = script?.getAttribute('bm-name') || 'Blue Marble'; // Gets the name value that was passed in. Defaults to "Blue Marble" if nothing was found
+  const consoleStyle = script?.getAttribute('bm-cStyle') || ''; // Gets the console style value that was passed in. Defaults to no styling if nothing was found
+  const fetchedBlobQueue = new Map(); // Blobs being processed
+
+  window.addEventListener('message', (event) => {
+    const { source, endpoint, blobID, blobData, blink } = event.data;
+
+    const elapsed = Date.now() - blink;
+
+    // Since this code does not run in the userscript, we can't use consoleLog().
+    console.groupCollapsed(`%c${name}%c: ${fetchedBlobQueue.size} Recieved IMAGE message about blob "${blobID}"`, consoleStyle, '');
+    console.log(`Blob fetch took %c${String(Math.floor(elapsed/60000)).padStart(2,'0')}:${String(Math.floor(elapsed/1000) % 60).padStart(2,'0')}.${String(elapsed % 1000).padStart(3,'0')}%c MM:SS.mmm`, consoleStyle, '');
+    console.log(fetchedBlobQueue);
+    console.groupEnd();
+
+    // The modified blob won't have an endpoint, so we ignore any message without one.
+    if ((source == 'blue-marble') && !!blobID && !!blobData && !endpoint) {
+
+      const callback = fetchedBlobQueue.get(blobID); // Retrieves the blob based on the UUID
+
+      // If the blobID is a valid function...
+      if (typeof callback === 'function') {
+
+        callback(blobData); // ...Retrieve the blob data from the blobID function
+      } else {
+        // ...else the blobID is unexpected. We don't know what it is, but we know for sure it is not a blob. This means we ignore it.
+
+        consoleWarn(`%c${name}%c: Attempted to retrieve a blob (%s) from queue, but the blobID was not a function! Skipping...`, consoleStyle, '', blobID);
+      }
+
+      fetchedBlobQueue.delete(blobID); // Delete the blob from the queue, because we don't need to process it again
+    }
+  });
+
+  // Spys on "spontaneous" fetch requests made by the client
+  const originalFetch = window.fetch; // Saves a copy of the original fetch
+
+  // Overrides fetch
+  window.fetch = async function(...args) {
+
+    const response = await originalFetch.apply(this, args); // Sends a fetch
+    const cloned = response.clone(); // Makes a copy of the response
+
+    // Retrieves the endpoint name. Unknown endpoint = "ignore"
+    const endpointName = ((args[0] instanceof Request) ? args[0]?.url : args[0]) || 'ignore';
+
+    // Check Content-Type to only process JSON
+    const contentType = cloned.headers.get('content-type') || '';
+    if (contentType.includes('application/json')) {
+
+
+      // Since this code does not run in the userscript, we can't use consoleLog().
+      console.log(`%c${name}%c: Sending JSON message about endpoint "${endpointName}"`, consoleStyle, '');
+
+      // Sends a message about the endpoint it spied on
+      cloned.json()
+        .then(jsonData => {
+          window.postMessage({
+            source: 'blue-marble',
+            endpoint: endpointName,
+            jsonData: jsonData
+          }, '*');
+        })
+        .catch(err => {
+          console.error(`%c${name}%c: Failed to parse JSON: `, consoleStyle, '', err);
+        });
+    } else if (contentType.includes('image/') && (!endpointName.includes('openfreemap') && !endpointName.includes('maps'))) {
+      // Fetch custom for all images but opensourcemap
+
+      const blink = Date.now(); // Current time
+
+      const blob = await cloned.blob(); // The original blob
+
+      // Since this code does not run in the userscript, we can't use consoleLog().
+      console.log(`%c${name}%c: ${fetchedBlobQueue.size} Sending IMAGE message about endpoint "${endpointName}"`, consoleStyle, '');
+
+      // Returns the manipulated blob
+      return new Promise((resolve) => {
+        const blobUUID = crypto.randomUUID(); // Generates a random UUID
+
+        // Store the blob while we wait for processing
+        fetchedBlobQueue.set(blobUUID, (blobProcessed) => {
+          // The response that triggers when the blob is finished processing
+
+          // Creates a new response
+          resolve(new Response(blobProcessed, {
+            headers: cloned.headers,
+            status: cloned.status,
+            statusText: cloned.statusText
+          }));
+
+          // Since this code does not run in the userscript, we can't use consoleLog().
+          console.log(`%c${name}%c: ${fetchedBlobQueue.size} Processed blob "${blobUUID}"`, consoleStyle, '');
+        });
+
+        window.postMessage({
+          source: 'blue-marble',
+          endpoint: endpointName,
+          blobID: blobUUID,
+          blobData: blob,
+          blink: blink
+        });
+      }).catch(exception => {
+        const elapsed = Date.now();
+        console.error(`%c${name}%c: Failed to Promise blob!`, consoleStyle, '');
+        console.groupCollapsed(`%c${name}%c: Details of failed blob Promise:`, consoleStyle, '');
+        console.log(`Endpoint: ${endpointName}\nThere are ${fetchedBlobQueue.size} blobs processing...\nBlink: ${blink.toLocaleString()}\nTime Since Blink: ${String(Math.floor(elapsed/60000)).padStart(2,'0')}:${String(Math.floor(elapsed/1000) % 60).padStart(2,'0')}.${String(elapsed % 1000).padStart(3,'0')} MM:SS.mmm`);
+        console.error(`Exception stack:`, exception);
+        console.groupEnd();
+      });
+
+      // cloned.blob().then(blob => {
+      //   window.postMessage({
+      //     source: 'blue-marble',
+      //     endpoint: endpointName,
+      //     blobData: blob
+      //   }, '*');
+      // });
+    }
+
+    return response; // Returns the original response
+  };
+});
+
+// Imports the CSS file from dist folder on github
+const cssOverlay = GM_getResourceText("CSS-BM-File");
+GM_addStyle(cssOverlay);
+
+// Injection point for the Roboto Mono font file (only if this is the Standalone version)
+const robotoMonoInjectionPoint = 'robotoMonoInjectionPoint';
+
+// If the Roboto Mono injection point contains '@font-face'...
+if (!!(robotoMonoInjectionPoint.indexOf('@font-face') + 1)) {
+  // A very hacky way of doing truthy/falsy logic
+  
+  console.log(`Loading Roboto Mono as a file...`);
+  GM_addStyle(robotoMonoInjectionPoint); // Add the Roboto Mono font-faces that were injected.
+} else {
+  // Else, no Roboto Mono was found. We need to use a stylesheet.
+  
+  // Imports the Roboto Mono font family as a stylesheet
+  var stylesheetLink = document.createElement('link');
+  stylesheetLink.href = 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap';
+  stylesheetLink.rel = 'preload';
+  stylesheetLink.as = 'style';
+  stylesheetLink.onload = function () {
+    this.onload = null;
+    this.rel = 'stylesheet';
+  };
+  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); // 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
+
+
+console.log(userSettings);
+console.log(Object.keys(userSettings).length);
+
+// If the user does not have a UUID yet, make a new one.
+if (Object.keys(userSettings).length == 0) {
+  const uuid = crypto.randomUUID(); // Generates a random UUID
+  console.log(uuid);
+  GM.setValue('bmUserSettings', JSON.stringify({
+    'uuid': uuid
+  }));
+}
+
+setInterval(() => apiManager.sendHeartbeat(version), 1000 * 60 * 30); // Sends a heartbeat every 30 minutes
+
+// The current "version" of the data collection agreement
+// Increment by 1 to retrigger the telemetry window
+const currentTelemetryVersion = 1;
+
+// The last "version" of the data collection agreement that the user agreed too
+const previousTelemetryVersion = userSettings?.telemetry;
+console.log(`Telemetry is ${!(previousTelemetryVersion == undefined)}`);
+
+// If the user has not agreed to the current data collection terms, we need to show the Telemetry window.
+if ((previousTelemetryVersion == undefined) || (previousTelemetryVersion > currentTelemetryVersion)) {
+  const windowTelemetry = new WindowTelemetry(name, version, currentTelemetryVersion, userSettings?.uuid);
+  windowTelemetry.setApiManager(apiManager);
+  windowTelemetry.buildWindow(); // Asks the user if they want to enable telemetry
+}
+
+windowMain.buildWindow(); // Builds the main Blue Marble window
+
+apiManager.spontaneousResponseListener(windowMain); // Reads spontaneous fetch responces
+
+observeBlack(); // Observes the black palette color
+
+consoleLog(`%c${name}%c (${version}) userscript has loaded!`, 'color: cornflowerblue;', '');
+
+/** Observe the black color, and add the "Move" button.
+ * @since 0.66.3
+ */
+function observeBlack() {
+  const observer = new MutationObserver((mutations, observer) => {
+
+    const black = document.querySelector('#color-1'); // Attempt to retrieve the black color element for anchoring
+
+    if (!black) {return;} // Black color does not exist yet. Kills iteself
+
+    let move = document.querySelector('#bm-button-move'); // Tries to find the move button
+
+    // If the move button does not exist, we make a new one
+    if (!move) {
+      move = document.createElement('button');
+      move.id = 'bm-button-move';
+      move.textContent = 'Move ↑';
+      move.className = 'btn btn-soft';
+      move.onclick = function() {
+        const roundedBox = this.parentNode.parentNode.parentNode.parentNode; // Obtains the rounded box
+        const shouldMoveUp = (this.textContent == 'Move ↑');
+        roundedBox.parentNode.className = roundedBox.parentNode.className.replace(shouldMoveUp ? 'bottom' : 'top', shouldMoveUp ? 'top' : 'bottom'); // Moves the rounded box to the top
+        roundedBox.style.borderTopLeftRadius = shouldMoveUp ? '0px' : 'var(--radius-box)';
+        roundedBox.style.borderTopRightRadius = shouldMoveUp ? '0px' : 'var(--radius-box)';
+        roundedBox.style.borderBottomLeftRadius = shouldMoveUp ? 'var(--radius-box)' : '0px';
+        roundedBox.style.borderBottomRightRadius = shouldMoveUp ? 'var(--radius-box)' : '0px';
+        this.textContent = shouldMoveUp ? 'Move ↓' : 'Move ↑';
+      }
+
+      // Attempts to find the "Paint Pixel" element for anchoring
+      const paintPixel = black.parentNode.parentNode.parentNode.parentNode.querySelector('h2');
+
+      paintPixel.parentNode?.appendChild(move); // Adds the move button
+    }
+  });
+
+  observer.observe(document.body, { childList: true, subtree: true });
+}
+
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/module.exports_module.exports.html b/docs/module.exports_module.exports.html new file mode 100644 index 0000000..26b7d60 --- /dev/null +++ b/docs/module.exports_module.exports.html @@ -0,0 +1,3189 @@ + + + + + + exports - Documentation + + + + + + + + + + + + + + + + + +
+ +

exports

+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(name, version)

+ + + + + +
+

Constructor for the Overlay class.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.0.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+ +
+ + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
name + + +string + + + + +

The name of the userscript

+ +
version + + +string + + + + +

The version of the userscript

+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(templateManager)

+ + + + + +
+

Constructor for ApiManager class

+
+ + + + + +
+ + + + +
Since:
+
  • 0.11.34
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
templateManager + + +TemplateManager + + + + + + +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports()

+ + + + + +
+

The constructor for the observer class

+
+ + + + + +
+ + + + +
Since:
+
  • 0.43.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(name, version)

+ + + + + +
+

The constructor for the TemplateManager class.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.55.8
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
name + + +string + + + + +

The name of the userscript

+ +
version + + +string + + + + +

The version of the userscript (SemVer as string)

+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(paramsopt)

+ + + + + +
+

The constructor for the Template class with enhanced pixel tracking.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.65.2
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
params + + +Object + + + + + + <optional>
+ + + + + +
+ + {} + + +

Object containing all optional parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
displayName + + +string + + + + + + <optional>
+ + + + + +
+ + 'My template' + + +

The display name of the template

+ +
sortID + + +number + + + + + + <optional>
+ + + + + +
+ + 0 + + +

The sort number of the template for rendering priority

+ +
authorID + + +string + + + + + + <optional>
+ + + + + +
+ + '' + + +

The user ID of the person who exported the template (prevents sort ID collisions)

+ +
url + + +string + + + + + + <optional>
+ + + + + +
+ + '' + + +

The URL to the source image

+ +
file + + +File + + + + + + <optional>
+ + + + + +
+ + null + + +

The template file (pre-processed File or processed bitmap)

+ +
coords + + +Array.<number, number, number, number> + + + + + + <optional>
+ + + + + +
+ + null + + +

The coordinates of the top left corner as (tileX, tileY, pixelX, pixelY)

+ +
chunked + + +Object + + + + + + <optional>
+ + + + + +
+ + null + + +

The affected chunks of the template, and their template for each chunk as a bitmap

+ +
chunked32 + + +Object + + + + + + <optional>
+ + + + + +
+ + {} + + +

The affected chunks of the template, and their template for each chunk as a Uint32Array

+ +
tileSize + + +number + + + + + + <optional>
+ + + + + +
+ + 1000 + + +

The size of a tile in pixels (assumes square tiles)

+ +
pixelCount + + +Object + + + + + + <optional>
+ + + + + +
+ + {total:0, colors:Map} + + +

Total number of pixels in the template (calculated automatically during processing)

+ +
+ + +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(name, version)

+ + + + + +
+

Constructor for the main Blue Marble window

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.326
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+
    +
  • Overlay#constructor
  • +
+
+ + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
name + + +string + + + + +

The name of the userscript

+ +
version + + +string + + + + +

The version of the userscript

+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(executor)

+ + + + + +
+

Constructor for the color filter window

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.329
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+
    +
  • Overlay#constructor
  • +
+
+ + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
executor + + +* + + + + +

The executing class

+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(name, version, currentTelemetryVersion, uuid)

+ + + + + +
+

Constructor for the telemetry window

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.339
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+
    +
  • Overlay#constructor
  • +
+
+ + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
name + + +string + + + + +

The name of the userscript

+ +
version + + +string + + + + +

The version of the userscript

+ +
currentTelemetryVersion + + +number + + + + +

The current "version" of the data collection agreement

+ +
uuid + + +string + + + + +

The UUID of the user

+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports()

+ + + + + +
+

The constructor for the confetti manager.

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.356
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(name, version, schemaVersionBleedingEdge, templateManageropt)

+ + + + + +
+

Constructor for the Template Wizard window

+
+ + + + + +
+ + + + +
Since:
+
  • 0.88.434
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+
    +
  • Overlay#constructor for examples
  • +
+
+ + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDescription
name + + +string + + + + + + + + + + +

The name of the userscript

+ +
version + + +string + + + + + + + + + + +

The version of the userscript

+ +
schemaVersionBleedingEdge + + +string + + + + + + + + + + +

The bleeding edge of schema versions for Blue Marble

+ +
templateManager + + +TemplateManager + + + + + + <optional>
+ + + + + +
+

(Optional) The TemplateManager class instance

+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(name, version)

+ + + + + +
+

Constructor for the Credits window

+
+ + + + + +
+ + + + +
Since:
+
  • 0.90.9
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+
    +
  • Overlay#constructor for examples
  • +
+
+ + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
name + + +string + + + + +

The name of the userscript

+ +
version + + +string + + + + +

The version of the userscript

+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(name, version)

+ + + + + +
+

Constructor for the Settings window

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.11
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + +
See:
+
+
    +
  • Overlay#constructor for examples
  • +
+
+ + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
name + + +string + + + + +

The name of the userscript

+ +
version + + +string + + + + +

The version of the userscript

+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+ +
+ +

+ exports +

+ + +
+ +
+
+ + +
+ + + +

new exports(name, version, userSettings)

+ + + + + +
+

Constructor for the SettingsManager class

+
+ + + + + +
+ + + + +
Since:
+
  • 0.91.11
+ + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
name + + +string + + + + +

The name of the userscript

+ +
version + + +string + + + + +

The version of the userscript

+ +
userSettings + + +Object + + + + +

The user settings as an object

+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/docs/observers.js.html b/docs/observers.js.html new file mode 100644 index 0000000..c17282e --- /dev/null +++ b/docs/observers.js.html @@ -0,0 +1,121 @@ + + + + + + observers.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

observers.js

+ + + + + + + +
+
+
/** This class contains all MutationObservers used (which is 1 probably).
+ * This is not an object, but rather a "collection" of functions (in a class).
+ * @class Observers
+ * @since 0.43.2
+ */
+export default class Observers {
+
+  /** The constructor for the observer class
+   * @since 0.43.2
+   */
+  constructor() {
+    this.observerBody = null;
+    this.observerBodyTarget = null;
+    this.targetDisplayCoords = '#bm-display-coords';
+  }
+
+  /** Creates the MutationObserver for document.body
+   * @param {HTMLElement} target - Targeted element to watch
+   * @returns {Observers} this (Observers class)
+   * @since 0.43.2
+   */
+  createObserverBody(target) {
+
+    this.observerBodyTarget = target;
+
+    this.observerBody = new MutationObserver((mutations) => {
+      for (const mutation of mutations) {
+        for (const node of mutation.addedNodes) {
+
+          if (!(node instanceof HTMLElement)) {continue;} // Does not track non-HTMLElements
+          
+          if (node.matches?.(this.targetDisplayCoords)) {
+
+          }
+        }
+      }
+    })
+
+    return this;
+  }
+
+  /** Retrieves the MutationObserver that watches document.body
+   * @returns {MutationObserver}
+   * @since 0.43.2
+   */
+  getObserverBody() {
+    return this.observerBody;
+  }
+
+  /** Observe a MutationObserver
+   * @param {MutationObserver} observer - The MutationObserver
+   * @param {boolean} watchChildList - (Optional) Should childList be watched? False by default
+   * @param {boolean} watchSubtree - (Optional) Should childList be watched? False by default
+   * @since 0.43.2
+   */
+  observe(observer, watchChildList=false, watchSubtree=false) {
+    observer.observe(this.observerBodyTarget, {
+      childList: watchChildList,
+      subtree: watchSubtree
+    });
+  }
+}
+
+
+ + + + +
+ +
+ + + + + + + diff --git a/docs/scripts/linenumber.js b/docs/scripts/linenumber.js new file mode 100644 index 0000000..8d52f7e --- /dev/null +++ b/docs/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(function() { + var source = document.getElementsByClassName('prettyprint source linenums'); + var i = 0; + var lineNumber = 0; + var lineId; + var lines; + var totalLines; + var anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = 'line' + lineNumber; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/docs/scripts/prettify/Apache-License-2.0.txt b/docs/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/docs/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/scripts/prettify/lang-css.js b/docs/scripts/prettify/lang-css.js new file mode 100644 index 0000000..041e1f5 --- /dev/null +++ b/docs/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/docs/scripts/prettify/prettify.js b/docs/scripts/prettify/prettify.js new file mode 100644 index 0000000..eef5ad7 --- /dev/null +++ b/docs/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p + + + + + settingsManager.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

settingsManager.js

+ + + + + + + +
+
+
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()
+  }
+}
+
+
+ + + + +
+ +
+ +
+ Generated by JSDoc 4.0.5 on Tue Mar 17 2026 01:31:43 GMT+0000 (Coordinated Universal Time) using the Minami theme. +
+ + + + + diff --git a/docs/styles/jsdoc-default.css b/docs/styles/jsdoc-default.css new file mode 100644 index 0000000..c14e3b9 --- /dev/null +++ b/docs/styles/jsdoc-default.css @@ -0,0 +1,692 @@ +@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,500i,500,600,600i|Roboto); + +* { + box-sizing: border-box +} + +html, body { + height: 100%; + width: 100%; +} + +body { + color: #4d4e53; + background-color: white; + margin: 0 auto; + padding: 0; + font-family: 'Source Sans Pro', Helvetica, sans-serif; + font-size: 16px; + line-height: 160%; +} + +a, +a:active { + color: #0095dd; + text-decoration: none; +} + +a:hover { + text-decoration: underline +} + +p, ul, ol, blockquote { + margin-bottom: 1em; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Roboto', sans-serif; +} + +h1, h2, h3, h4, h5, h6 { + color: #000; + font-weight: 400; + margin: 0; +} + +h1 { + font-weight: 300; + font-size: 48px; + margin: 1em 0 .5em; +} + +h1.page-title {margin-bottom: 10px;font-size: 34px;font-weight: 300;border-bottom: solid 2px #ddd;padding: .5em 0 .5em;margin-top: 0;} + +h2 { + font-size: 32px; + margin: 1.2em 0 .8em; + font-weight: bold; +} + +h3 { + /* margin-top: 1em; */ + /* margin-bottom: 16px; */ + /* font-weight: bold; */ + padding: 0; + margin: 1em 0 .6em; + font-size: 28px; + /* border-bottom: 1px solid #eee; */ + /* padding-bottom: 15px; */ +} + +h4 { + font-size: 18px; + margin: 1em 0 .2em; + color: #4d4e53; + /* border-bottom: 1px solid #eee; */ + padding-bottom: 8px; +} + +h5, .container-overview .subsection-title { + font-size: 120%; + /* letter-spacing: -0.01em; */ + margin: 20px 0 5px; +} + +h6 { + font-size: 100%; + letter-spacing: -0.01em; + margin: 6px 0 3px 0; + font-style: italic; +} + +tt, code, kbd, samp { + font-family: Consolas, Monaco, 'Andale Mono', monospace; + background: #f4f4f4; + padding: 1px 5px; + border-radius: 5px; + font-size: 14px; +} + +blockquote { + display: block; + border-left: 4px solid #eee; + margin: 0; + padding-left: 1em; + color: #888; +} + +.class-description { + font-size: 130%; + line-height: 140%; + margin-bottom: 1em; + margin-top: 1em; +} + +.class-description:empty { + margin: 0 +} + +/** Container **/ +#main { + float: right; + min-width: 360px; + width: calc(100% - 250px); + padding: 0 30px 20px 30px; +} + +header { + display: block +} + +section { + display: block; + background-color: #fff; + padding: 0; +} + +.variation { + display: none +} + +.signature-attributes { + font-size: 60%; + color: #aaa; + font-style: italic; + font-weight: lighter; +} + +/** Readme **/ + +.readme { + font-size: 16px; +} + +.readme h1, +.readme h2, +.readme h3, +.readme h4, +.readme h5 { + margin-top: 1em; + margin-bottom: 16px; + font-weight: bold; + padding: 0; +} + +.readme h1 { + font-size: 2em; + padding-bottom: 0.3em; +} + +.readme h2 { + font-size: 1.75em; + padding-bottom: 0.3em; +} + +.readme h3 { + font-size: 1.5em; + background-color: transparent; +} + +.readme h4 { + font-size: 1.25em; +} + +.readme h5 { + font-size: 1em; +} + +.readme img { + max-width: 100%; +} + +.readme ul, .readme ol { + padding-left: 2em; +} + +.readme pre > code { + font-size: 0.85em; +} + +.readme table { + margin-bottom: 1em; + border-collapse: collapse; + border-spacing: 0; +} + +.readme table tr { + background-color: #fff; + border-top: 1px solid #ccc; +} + +.readme table th, +.readme table td { + padding: 6px 13px; + border: 1px solid #ddd; +} + +.readme table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +/** Nav **/ +nav { + float: left; + display: block; + width: 250px; + background: #fff; + overflow: auto; + position: fixed; + height: 100%; + padding: 10px; + border-right: 1px solid #eee; + /* box-shadow: 0 0 3px rgba(0,0,0,0.1); */ +} + +nav li { + list-style: none; + padding: 0; + margin: 0; +} + +.nav-heading { + margin-top: 10px; + font-weight: bold; +} + +.nav-heading a { + color: #888; + font-size: 14px; + display: inline-block; +} + +.nav-item-type { + /* margin-left: 5px; */ + width: 18px; + height: 18px; + display: inline-block; + text-align: center; + border-radius: 0.2em; + margin-right: 5px; + font-weight: bold; + line-height: 20px; + font-size: 13px; +} + +.type-function { + background: #B3E5FC; + color: #0288D1; +} + +.type-class { + background: #D1C4E9; + color: #4527A0; +} + +.type-member { + background: #C8E6C9; + color: #388E3C; +} + +.type-module { + background: #E1BEE7; + color: #7B1FA2; +} + + +/** Footer **/ +footer { + color: hsl(0, 0%, 28%); + margin-left: 250px; + display: block; + padding: 30px; + font-style: italic; + font-size: 90%; + border-top: 1px solid #eee; +} + +.ancestors { + color: #999 +} + +.ancestors a { + color: #999 !important; + text-decoration: none; +} + +.clear { + clear: both +} + +.important { + font-weight: bold; + color: #950B02; +} + +.yes-def { + text-indent: -1000px +} + +.type-signature { + color: #aaa +} + +.name, .signature { + font-family: Consolas, Monaco, 'Andale Mono', monospace +} + +.details { + margin-top: 14px; + border-left: 2px solid #DDD; + line-height: 30px; +} + +.details dt { + width: 120px; + float: left; + padding-left: 10px; +} + +.details dd { + margin-left: 70px +} + +.details ul { + margin: 0 +} + +.details ul { + list-style-type: none +} + +.details li { + margin-left: 30px +} + +.details pre.prettyprint { + margin: 0 +} + +.details .object-value { + padding-top: 0 +} + +.description { + margin-bottom: 1em; + margin-top: 1em; +} + +.code-caption { + font-style: italic; + font-size: 107%; + margin: 0; +} + +.prettyprint { + font-size: 13px; + border: 1px solid #ddd; + border-radius: 3px; + box-shadow: 0 1px 3px hsla(0, 0%, 0%, 0.05); + overflow: auto; +} + +.prettyprint.source { + width: inherit +} + +.prettyprint code { + font-size: 12px; + line-height: 18px; + display: block; + background-color: #fff; + color: #4D4E53; +} + +.prettyprint code:empty:before { + content: ''; +} + +.prettyprint > code { + padding: 15px +} + +.prettyprint .linenums code { + padding: 0 15px +} + +.prettyprint .linenums li:first-of-type code { + padding-top: 15px +} + +.prettyprint code span.line { + display: inline-block +} + +.prettyprint.linenums { + padding-left: 70px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.prettyprint.linenums ol { + padding-left: 0 +} + +.prettyprint.linenums li { + border-left: 3px #ddd solid +} + +.prettyprint.linenums li.selected, .prettyprint.linenums li.selected * { + background-color: lightyellow +} + +.prettyprint.linenums li * { + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.params, .props { + border-spacing: 0; + border: 1px solid #ddd; + border-collapse: collapse; + border-radius: 3px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + width: 100%; + font-size: 14px; + /* margin-left: 15px; */ +} + +.params .name, .props .name, .name code { + color: #4D4E53; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 100%; +} + +.params td, .params th, .props td, .props th { + margin: 0px; + text-align: left; + vertical-align: top; + padding: 10px; + display: table-cell; +} + +.params td { + border-top: 1px solid #eee +} + +.params thead tr, .props thead tr { + background-color: #fff; + font-weight: bold; +} + +.params .params thead tr, .props .props thead tr { + background-color: #fff; + font-weight: bold; +} + +.params td.description > p:first-child, .props td.description > p:first-child { + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child, .props td.description > p:last-child { + margin-bottom: 0; + padding-bottom: 0; +} + +dl.param-type { + /* border-bottom: 1px solid hsl(0, 0%, 87%); */ + margin: 0; + padding: 0; + font-size: 16px; +} + +.param-type dt, .param-type dd { + display: inline-block +} + +.param-type dd { + font-family: Consolas, Monaco, 'Andale Mono', monospace; + display: inline-block; + padding: 0; + margin: 0; + font-size: 14px; +} + +.disabled { + color: #454545 +} + +/* navicon button */ +.navicon-button { + display: none; + position: relative; + padding: 2.0625rem 1.5rem; + transition: 0.25s; + cursor: pointer; + user-select: none; + opacity: .8; +} +.navicon-button .navicon:before, .navicon-button .navicon:after { + transition: 0.25s; +} +.navicon-button:hover { + transition: 0.5s; + opacity: 1; +} +.navicon-button:hover .navicon:before, .navicon-button:hover .navicon:after { + transition: 0.25s; +} +.navicon-button:hover .navicon:before { + top: .825rem; +} +.navicon-button:hover .navicon:after { + top: -.825rem; +} + +/* navicon */ +.navicon { + position: relative; + width: 2.5em; + height: .3125rem; + background: #000; + transition: 0.3s; + border-radius: 2.5rem; +} +.navicon:before, .navicon:after { + display: block; + content: ""; + height: .3125rem; + width: 2.5rem; + background: #000; + position: absolute; + z-index: -1; + transition: 0.3s 0.25s; + border-radius: 1rem; +} +.navicon:before { + top: .625rem; +} +.navicon:after { + top: -.625rem; +} + +/* open */ +.nav-trigger:checked + label:not(.steps) .navicon:before, +.nav-trigger:checked + label:not(.steps) .navicon:after { + top: 0 !important; +} + +.nav-trigger:checked + label .navicon:before, +.nav-trigger:checked + label .navicon:after { + transition: 0.5s; +} + +/* Minus */ +.nav-trigger:checked + label { + transform: scale(0.75); +} + +/* × and + */ +.nav-trigger:checked + label.plus .navicon, +.nav-trigger:checked + label.x .navicon { + background: transparent; +} + +.nav-trigger:checked + label.plus .navicon:before, +.nav-trigger:checked + label.x .navicon:before { + transform: rotate(-45deg); + background: #FFF; +} + +.nav-trigger:checked + label.plus .navicon:after, +.nav-trigger:checked + label.x .navicon:after { + transform: rotate(45deg); + background: #FFF; +} + +.nav-trigger:checked + label.plus { + transform: scale(0.75) rotate(45deg); +} + +.nav-trigger:checked ~ nav { + left: 0 !important; +} + +.nav-trigger:checked ~ .overlay { + display: block; +} + +.nav-trigger { + position: fixed; + top: 0; + clip: rect(0, 0, 0, 0); +} + +.overlay { + display: none; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; + background: hsla(0, 0%, 0%, 0.5); + z-index: 1; +} + +.section-method { + margin-bottom: 30px; + padding-bottom: 30px; + border-bottom: 1px solid #eee; +} + +@media only screen and (min-width: 320px) and (max-width: 680px) { + body { + overflow-x: hidden; + } + + nav { + background: #FFF; + width: 250px; + height: 100%; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: -250px; + z-index: 3; + padding: 0 10px; + transition: left 0.2s; + } + + .navicon-button { + display: inline-block; + position: fixed; + top: 1.5em; + right: 0; + z-index: 2; + } + + #main { + width: 100%; + min-width: 360px; + } + + #main h1.page-title { + margin: 1em 0; + } + + #main section { + padding: 0; + } + + footer { + margin-left: 0; + } +} + +@media only print { + nav { + display: none; + } + + #main { + float: none; + width: 100%; + } +} diff --git a/docs/styles/prettify-jsdoc.css b/docs/styles/prettify-jsdoc.css new file mode 100644 index 0000000..834a866 --- /dev/null +++ b/docs/styles/prettify-jsdoc.css @@ -0,0 +1,111 @@ +/* JSDoc prettify.js theme */ + +/* plain text */ +.pln { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* string content */ +.str { + color: hsl(104, 100%, 24%); + font-weight: normal; + font-style: normal; +} + +/* a keyword */ +.kwd { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a comment */ +.com { + font-weight: normal; + font-style: italic; +} + +/* a type name */ +.typ { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a literal value */ +.lit { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* punctuation */ +.pun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp open bracket */ +.opn { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp close bracket */ +.clo { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a markup tag name */ +.tag { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute name */ +.atn { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute value */ +.atv { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a declaration */ +.dec { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a variable name */ +.var { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a function name */ +.fun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} diff --git a/docs/styles/prettify-tomorrow.css b/docs/styles/prettify-tomorrow.css new file mode 100644 index 0000000..81e74d1 --- /dev/null +++ b/docs/styles/prettify-tomorrow.css @@ -0,0 +1,132 @@ +/* Tomorrow Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +/* plain text */ +.pln { + color: #4d4d4c; } + +@media screen { + /* string content */ + .str { + color: hsl(104, 100%, 24%); } + + /* a keyword */ + .kwd { + color: hsl(240, 100%, 50%); } + + /* a comment */ + .com { + color: hsl(0, 0%, 60%); } + + /* a type name */ + .typ { + color: hsl(240, 100%, 32%); } + + /* a literal value */ + .lit { + color: hsl(240, 100%, 40%); } + + /* punctuation */ + .pun { + color: #000000; } + + /* lisp open bracket */ + .opn { + color: #000000; } + + /* lisp close bracket */ + .clo { + color: #000000; } + + /* a markup tag name */ + .tag { + color: #c82829; } + + /* a markup attribute name */ + .atn { + color: #f5871f; } + + /* a markup attribute value */ + .atv { + color: #3e999f; } + + /* a declaration */ + .dec { + color: #f5871f; } + + /* a variable name */ + .var { + color: #c82829; } + + /* a function name */ + .fun { + color: #4271ae; } } +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; } + + .kwd { + color: #006; + font-weight: bold; } + + .com { + color: #600; + font-style: italic; } + + .typ { + color: #404; + font-weight: bold; } + + .lit { + color: #044; } + + .pun, .opn, .clo { + color: #440; } + + .tag { + color: #006; + font-weight: bold; } + + .atn { + color: #404; } + + .atv { + color: #060; } } +/* Style */ +/* +pre.prettyprint { + background: white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 12px; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; } +*/ + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; } + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L4, +li.L5, +li.L6, +li.L7, +li.L8, +li.L9 { + /* */ } + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + /* */ } diff --git a/docs/templateManager.js.html b/docs/templateManager.js.html new file mode 100644 index 0000000..dae4472 --- /dev/null +++ b/docs/templateManager.js.html @@ -0,0 +1,1055 @@ + + + + + + templateManager.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

templateManager.js

+ + + + + + + +
+
+
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.
+ * This class handles all external requests for template modification, creation, and analysis.
+ * It serves as the central coordinator between template instances and the user interface.
+ * @class TemplateManager
+ * @since 0.55.8
+ * @example
+ * // JSON structure for a template made in schema version 2.0.0.
+ * // Note: The pixel "colors" Object contains more than 2 keys.
+ * // Note: The template tiles are stored as base64 PNG images.
+ * {
+ *   "whoami": "BlueMarble",
+ *   "scriptVersion": "1.13.0",
+ *   "schemaVersion": "2.0.0",
+ *   "templates": {
+ *     "0 $Z": {
+ *       "name": "My Template",
+ *       "enabled": true,
+ *       "pixels": {
+ *         "total": 40399,
+ *         "colors": {
+ *           "-2": 40000,
+ *           "0": 399
+ *         }
+ *       }
+ *       "tiles": {
+ *         "1231,0047,183,593": "iVBORw0KGgoAAAANSUhEUgAA",
+ *         "1231,0048,183,000": "AAAFCAYAAACNbyblAAAAHElEQVQI12P4"
+ *       }
+ *     },
+ *     "1 $Z": {
+ *       "name": "My Template",
+ *       "URL": "https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/dist/assets/Favicon.png",
+ *       "URLType": "template",
+ *       "enabled": false,
+ *       "pixels": {
+ *         "total": 40399,
+ *         "colors": {
+ *           "-2": 40000,
+ *           "0": 399
+ *         }
+ *       }
+ *       "tiles": {
+ *         "375,1846,276,188": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA",
+ *         "376,1846,000,188": "data:image/png;AAAFCAYAAACNbyblAAAAHElEQVQI12P4"
+ *       }
+ *     }
+ *   }
+ * }
+ * @example
+ * // JSON structure for a template made in schema version 1.0.0.
+ * // Note: The template tiles are stored as base64 PNG images.
+ * {
+ *   "whoami": "BlueMarble",
+ *   "scriptVersion": "1.13.0",
+ *   "schemaVersion": "1.0.0",
+ *   "templates": {
+ *     "0 $Z": {
+ *       "name": "My Template",
+ *       "enabled": true,
+ *       "coords": "2000, 230, 45, 201"
+ *       "palette": {
+ *         "0,0,0": {
+ *            "count": 123,
+ *            "enabled": true
+ *         },
+ *         "255,255,255": {
+ *            "count": 1315,
+ *            "enabled": false
+ *         }
+ *       }
+ *       "tiles": {
+ *         "1231,0047,183,593": "iVBORw0KGgoAAAANSUhEUgAA",
+ *         "1231,0048,183,000": "AAAFCAYAAACNbyblAAAAHElEQVQI12P4"
+ *       }
+ *     }
+ *   }
+ * }
+ */
+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) {
+
+    // Meta
+    this.name = name; // Name of userscript
+    this.version = version; // Version of userscript
+    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
+    this.tileSize = 1000; // The number of pixels in a tile. Assumes the tile is square
+    this.drawMult = 3; // The enlarged size for each pixel. E.g. when "3", a 1x1 pixel becomes a 1x1 pixel inside a 3x3 area. MUST BE ODD
+    this.paletteTolerance = 3; // Tolerance for how close an RGB value has to be in order to be considered a color. A tolerance of "3" means the sum of the RGB can be up to 3 away from the actual value.
+    this.paletteBM = colorpaletteForBlueMarble(this.paletteTolerance); // Retrieves the color palette BM will use as an Object containing multiple Uint32Arrays
+    
+    // Template
+    this.template = null; // The template image.
+    this.templateState = ''; // The state of the template ('blob', 'proccessing', 'template', etc.)
+    /** An Array of Template classes @type {Array<Template>} */
+    this.templatesArray = []; // All Template instnaces currently loaded (Template)
+    this.templatesJSON = null; // All templates currently loaded (JSON)
+    this.templatesShouldBeDrawn = true; // Should ALL templates be drawn to the canvas?
+    this.templatePixelsCorrect = null; // An object where the keys are the tile coords, and the values are Maps (BM palette color IDs) containing the amount of correctly placed pixels for that tile in this template
+    /** Will contain all color ID's to filter @type {Map<number, boolean>} */
+    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
+   */
+  async createJSON() {
+    return {
+      "whoami": this.name.replace(' ', ''), // Name of userscript without spaces
+      "scriptVersion": this.version, // Version of userscript
+      "schemaVersion": this.schemaVersion, // Version of JSON schema
+      "templates": {} // The templates
+    };
+  }
+
+  /** Creates the template from the inputed file blob
+   * @param {File} blob - The file blob to create a template from
+   * @param {string} name - The display name of the template
+   * @param {Array<number, number, number, number>} coords - The coordinates of the top left corner of the template
+   * @since 0.65.77
+   */
+  async createTemplate(blob, name, coords) {
+
+    // Creates the JSON object if it does not already exist
+    if (!this.templatesJSON) {this.templatesJSON = await this.createJSON(); console.log(`Creating JSON...`);}
+
+    this.windowMain.handleDisplayStatus(`Creating template at ${coords.join(', ')}...`);
+
+    // Creates a new template instance
+    const template = new Template({
+      displayName: name,
+      sortID: 0, // Object.keys(this.templatesJSON.templates).length || 0, // Uncomment this to enable multiple templates (1/2)
+      authorID: numberToEncoded(this.userID || 0, this.encodingBase),
+      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, shouldSkipTransTiles, shouldAggSkipTransTiles); // Chunks the tiles
+    
+    template.chunked = templateTiles; // Stores the chunked tile bitmaps
+
+    // Converts total pixel Object/Map variables into JSON-ready format
+    const _pixels = { "total": template.pixelCount.total, "colors": Object.fromEntries(template.pixelCount.colors) }
+
+    // Appends a child into the templates object
+    // The child's name is the number of templates already in the list (sort order) plus the encoded player ID
+    this.templatesJSON.templates[`${template.sortID} ${template.authorID}`] = {
+      "name": template.displayName, // Display name of template
+      "coords": coords.join(', '), // The coords of the template
+      "enabled": true,
+      "pixels": _pixels, // The total pixels in the template
+      "tiles": templateTilesBuffers // Stores the chunked tile buffers
+    };
+
+    this.templatesArray = []; // Remove this to enable multiple templates (2/2)
+    this.templatesArray.push(template); // Pushes the Template object instance to the Template Array
+
+    this.windowMain.handleDisplayStatus(`Template created at ${coords.join(', ')}!`);
+
+    console.log(Object.keys(this.templatesJSON.templates).length);
+    console.log(this.templatesJSON);
+    console.log(this.templatesArray);
+    console.log(JSON.stringify(this.templatesJSON));
+
+    await this.#storeTemplates();
+  }
+
+  /** Generates a {@link Template} class instance from the JSON object template.
+   * {@link createTemplate()} will create a class instance and save to template storage.
+   * `#loadTemplate()` will create a class instance without saving to the template storage.
+   * @param {Object} template - The template to load
+   * @since 0.88.504
+   */
+  #loadTemplate(templateObject) {
+
+    // Calculates the pixel count
+    const pixelCount = {
+      total: templateObject.pixels?.total,
+      colors: new Map(Object.entries(templateObject.pixels?.colors || {}).map(([key, value]) => [Number(key), value]))
+    };
+
+    // Creates the template
+    const template = new Template({
+      displayName: templateObject.displayName,
+      sortID: Object.keys(this.templatesJSON.templates).length || 0,
+      authorID: numberToEncoded(this.userID || 0, this.encodingBase),
+      pixelCount: pixelCount,
+      chunked: templateObject.tiles
+    });
+
+    template.calculateCoordsFromChunked(); // Updates `Template.coords`
+
+    this.templatesArray.push(template);
+  }
+
+  /** Stores the JSON object of the loaded templates into TamperMonkey (GreaseMonkey) storage.
+   * @since 0.72.7
+   */
+  async #storeTemplates() {
+    GM.setValue('bmTemplates', JSON.stringify(this.templatesJSON));
+  }
+
+  /** Deletes a template from the JSON object.
+   * Also delete's the corrosponding {@link Template} class instance
+   */
+  deleteTemplate() {
+
+  }
+
+  /** Disables the template from view
+   */
+  async disableTemplate() {
+
+    // Creates the JSON object if it does not already exist
+    if (!this.templatesJSON) {this.templatesJSON = await this.createJSON(); console.log(`Creating JSON...`);}
+
+
+  }
+
+  /** Downloads all templates loaded.
+   * @since 0.88.499
+   */
+  async downloadAllTemplates() {
+
+    consoleLog(`Downloading all templates...`);
+
+    console.log(this.templatesArray);
+
+    // For each template loaded...
+    for (const template of this.templatesArray) {
+
+      await this.downloadTemplate(template); // Downloads the template
+
+      await sleep(500); // Avoids download throttling from the browser
+    }
+  }
+
+  /** Downloads all templates from Blue Marble's template storage.
+   * @since 0.88.474
+   */
+  async downloadAllTemplatesFromStorage() {
+
+    // Templates in user storage
+    const templates = JSON.parse(GM_getValue('bmTemplates', '{}'))?.templates;
+
+    console.log(templates);
+
+    // If there is at least one template loaded...
+    if (Object.keys(templates).length > 0) {
+
+      // For each template loaded...
+      for (const [key, template] of Object.entries(templates)) {
+
+        // If the template is a direct child of the templates Object...
+        if (templates.hasOwnProperty(key)) {
+          
+          // Downloads the template using a dummy Template instance
+          await this.downloadTemplate(new Template({
+            displayName: template.name,
+            sortID: key.split(' ')?.[0],
+            authorID: key.split(' ')?.[1],
+            chunked: template.tiles
+          }));
+
+          await sleep(500); // Avoids download throttling from the browser
+        }
+      }
+    }
+  }
+
+  /** Downloads the template passed-in.
+   * @param {Template} template - The template class instance to download
+   * @since 0.88.499
+   */
+  async downloadTemplate(template) {
+
+    template.calculateCoordsFromChunked(); // Updates `Template.coords`
+
+    // Constructs the file name to download as
+    const templateFileName = `${template.coords.join('-')}_${template.displayName.replaceAll(' ', '-')}`;
+
+    // Converts `Template.chunked` to a blob
+    const blob = await this.convertTemplateToBlob(template);
+
+    // Downloads the template
+    await GM.download({
+      url: URL.createObjectURL(blob),
+      name: templateFileName + '.png',
+      conflictAction: 'uniquify',
+      onload: () => {consoleLog(`Download of template '${templateFileName}' complete!`);},
+      onerror: (error, details) => {consoleError(`Download of template '${templateFileName}' failed because ${error}! Details: ${details}`);},
+      ontimeout: () => {consoleWarn(`Download of template '${templateFileName}' has timed out!`);}
+    });
+  }
+
+  /** Converts a Template class instance into a Blob. 
+   * Specifically, this takes `Template.chunked` and converts it to a Blob.
+   * @since 0.88.504
+   * @returns {Promise<Blob>} A Promise of a Blob PNG image of the template
+   */
+  async convertTemplateToBlob(template) {
+
+    console.log(template);
+
+    const templateTiles64 = template.chunked; // Tiles of template image as base 64
+
+    // Sorts the keys of the tiles (Object -> Array)
+    const templateTileKeysSorted = Object.keys(templateTiles64).sort();
+
+    // Turns the base64 tiles into Images
+    const templateTilesImageSorted = await Promise.all(templateTileKeysSorted.map(tileKey => convertBase64ToImage(templateTiles64[tileKey])));
+
+    // Absolute pixel coordinates for smallest (top left) and largest (bottom right) pixel coordinates
+    let absoluteSmallestX = Infinity;
+    let absoluteSmallestY = Infinity;
+    let absoluteLargestX = 0;
+    let absoluteLargestY = 0;
+
+    // Calculates the minimum and maximum (X, Y) absolute coordinates
+    templateTileKeysSorted.forEach((key, index) => {
+
+      // Deconstructs the tile coordinates
+      const [tileX, tileY, pixelX, pixelY] = key.split(',').map(Number);
+
+      const tileImage = templateTilesImageSorted[index]; // Obtains the image for this tile
+
+      // Calculates the absolute pixel coordinates for this tile
+      const absoluteX = (tileX * this.tileSize) + pixelX;
+      const absoluteY = (tileY * this.tileSize) + pixelY;
+
+      // Record the smallest/largest absolute coordinates if and only if this tile is the smallest/largest. Otherwise, use previous best
+      absoluteSmallestX = Math.min(absoluteSmallestX, absoluteX);
+      absoluteSmallestY = Math.min(absoluteSmallestY, absoluteY);
+      absoluteLargestX = Math.max(absoluteLargestX, absoluteX + (tileImage.width / this.drawMult));
+      absoluteLargestY = Math.max(absoluteLargestY, absoluteY + (tileImage.height / this.drawMult));
+    })
+
+    console.log(`Absolute coordinates: (${absoluteSmallestX}, ${absoluteSmallestY}) and (${absoluteLargestX}, ${absoluteLargestY})`);
+
+    // Calculates the template/canvas width and height
+    const templateWidth = absoluteLargestX - absoluteSmallestX;
+    const templateHeight = absoluteLargestY - absoluteSmallestY;
+    const canvasWidth = templateWidth * this.drawMult;
+    const canvasHeight = templateHeight * this.drawMult;
+
+    console.log(`Template Width: ${templateWidth}\nTemplate Height: ${templateHeight}\nCanvas Width: ${canvasWidth}\nCanvas Height: ${canvasHeight}`);
+
+    // Creates a new canvas the size of the template
+    const canvas = new OffscreenCanvas(canvasWidth, canvasHeight);
+    const context = canvas.getContext('2d');
+
+    // For each tile...
+    templateTileKeysSorted.forEach((key, index) => {
+
+      // Deconstructs the tile coordinates
+      const [tileX, tileY, pixelX, pixelY] = key.split(',').map(Number);
+
+      const tileImage = templateTilesImageSorted[index]; // Obtains the image for this tile
+
+      // Calculates the absolute pixel coordinates for this tile
+      const absoluteX = (tileX * this.tileSize) + pixelX;
+      const absoluteY = (tileY * this.tileSize) + pixelY;
+
+      console.log(`Drawing tile (${tileX}, ${tileY}, ${pixelX}, ${pixelY}) (${absoluteX}, ${absoluteY}) at (${absoluteX - absoluteSmallestX}, ${absoluteY - absoluteSmallestY}) on the canvas...`);
+
+      // Draws the tile to the canvas
+      context.drawImage(tileImage, (absoluteX - absoluteSmallestX) * this.drawMult, (absoluteY - absoluteSmallestY) * this.drawMult, tileImage.width, tileImage.height);
+    })
+
+    // The expanded template is now on the canvas
+
+    context.globalCompositeOperation = "destination-over"; // Draw under the canvas (new draws only show in place of transparent pixels)
+
+    // Extends the template vertically to create columns
+    context.drawImage(canvas, 0, -1);
+    context.drawImage(canvas, 0, 1);
+
+    // Extends the columns horizontally to become a solid template
+    context.drawImage(canvas, -1, 0);
+    context.drawImage(canvas, 1, 0);
+
+    const smallCanvas = new OffscreenCanvas(templateWidth, templateHeight);
+    const smallContext = smallCanvas.getContext("2d");
+
+    smallContext.imageSmoothingEnabled = false; // Forces nearest neighbor scaling algorithm
+
+    // Downscale the template
+    smallContext.drawImage(
+      canvas,
+      0, 0, templateWidth * this.drawMult, templateHeight * this.drawMult, // Source image size
+      0, 0, templateWidth, templateHeight // Small canvas size
+    );
+
+    // Returns a blob
+    return smallCanvas.convertToBlob({ type: 'image/png' });
+
+    /** Turns a chunked base 64 string template tile into an Image template tile
+     * @param {string} base64 - Base64 string of image data (without URI header)
+     * @since 0.88.474
+     * @returns {Promise} Promise to load a new Image()
+     */
+    function convertBase64ToImage(base64) {
+      return new Promise((resolve, reject) => {
+        const image = new Image();
+        image.onload = () => resolve(image);
+        image.onerror = reject;
+        image.src = "data:image/png;base64," + base64;
+      });
+    }
+  }
+
+  /** Draws all templates on the specified tile.
+   * This method handles the rendering of template overlays on individual tiles.
+   * @param {File} tileBlob - The pixels that are placed on a tile
+   * @param {Array<number>} tileCoords - The tile coordinates [x, y]
+   * @since 0.65.77
+   */
+  async drawTemplateOnTile(tileBlob, tileCoords) {
+
+    // Returns early if no templates should be drawn
+    if (!this.templatesShouldBeDrawn) {return tileBlob;}
+
+    const drawSize = this.tileSize * this.drawMult; // Calculate draw multiplier for scaling
+
+    // Format tile coordinates with proper padding for consistent lookup
+    tileCoords = tileCoords[0].toString().padStart(4, '0') + ',' + tileCoords[1].toString().padStart(4, '0');
+
+    console.log(`Searching for templates in tile: "${tileCoords}"`);
+
+    const templateArray = this.templatesArray; // Stores a copy for sorting
+    console.log(templateArray);
+
+    // Sorts the array of Template class instances. 0 = first = lowest draw priority
+    templateArray.sort((a, b) => {return a.sortID - b.sortID;});
+
+    console.log(templateArray);
+
+    // Retrieves the relavent template tile blobs
+    const templatesToDraw = templateArray
+      .map(template => {
+        const matchingTiles = Object.keys(template.chunked).filter(tile =>
+          tile.startsWith(tileCoords)
+        );
+
+        if (matchingTiles.length === 0) {return null;} // Return null when nothing is found
+
+        // Retrieves the blobs of the templates for this tile
+        const matchingTileBlobs = matchingTiles.map(tile => {
+
+          const coords = tile.split(','); // [x, y, x, y] Tile/pixel coordinates
+          
+          return {
+            instance: template,
+            bitmap: template.chunked[tile],
+            chunked32: template.chunked32?.[tile],
+            tileCoords: [coords[0], coords[1]],
+            pixelCoords: [coords[2], coords[3]]
+          }
+        });
+
+        return matchingTileBlobs?.[0];
+      })
+    .filter(Boolean);
+
+    console.log(templatesToDraw);
+
+    const templateCount = templatesToDraw?.length || 0; // Number of templates to draw on this tile
+    console.log(`templateCount = ${templateCount}`);
+
+    if (templateCount > 0) {
+      
+      // Calculate total pixel count for templates actively being displayed in this tile
+      const totalPixels = templateArray
+        .filter(template => {
+          // Filter templates to include only those with tiles matching current coordinates
+          // This ensures we count pixels only for templates actually being rendered
+          const matchingTiles = Object.keys(template.chunked).filter(tile =>
+            tile.startsWith(tileCoords)
+          );
+          return matchingTiles.length > 0;
+        })
+        .reduce((sum, template) => sum + (template.pixelCount.total || 0), 0);
+      
+      // Format pixel count with locale-appropriate thousands separators for better readability
+      // Examples: "1,234,567" (US), "1.234.567" (DE), "1 234 567" (FR)
+      const pixelCountFormatted = localizeNumber(totalPixels);
+      
+      // Display status information about the templates being rendered
+      this.windowMain.handleDisplayStatus(
+        `Displaying ${templateCount} template${templateCount == 1 ? '' : 's'}.\nTotal pixels: ${pixelCountFormatted}`
+      );
+    } else {
+      //this.overlay.handleDisplayStatus(`Displaying ${templateCount} templates.`);
+      this.windowMain.handleDisplayStatus(`Sleeping\nVersion: ${this.version}`);
+      return tileBlob; // No templates are on this tile. Return the original tile early
+    }
+    
+    const tileBitmap = await createImageBitmap(tileBlob);
+
+    const canvas = new OffscreenCanvas(drawSize, drawSize);
+    const context = canvas.getContext('2d');
+
+    context.imageSmoothingEnabled = false; // Nearest neighbor
+
+    // Tells the canvas to ignore anything outside of this area
+    context.beginPath();
+    context.rect(0, 0, drawSize, drawSize);
+    context.clip();
+
+    context.clearRect(0, 0, drawSize, drawSize); // Draws transparent background
+    context.drawImage(tileBitmap, 0, 0, drawSize, drawSize); // Draw tile to canvas
+
+    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) {
+      console.log(`Template:`);
+      console.log(template);
+
+      const templateHasErased = !!template.instance.pixelCount?.colors?.get(-1); // Does this template have Erased (#deface) pixels?
+
+      // Obtains the template (for only this tile) as a Uint32Array
+      let templateBeforeFilter32 = template.chunked32.slice();
+      // Remove the `.slice()` and colors, once disabled, can never be re-enabled
+
+      const coordXtoDrawAt = Number(template.pixelCoords[0]) * this.drawMult;
+      const coordYtoDrawAt = Number(template.pixelCoords[1]) * this.drawMult;
+
+      // Draws the template to the tile if there are no colors to filter, and there are no Erased pixels
+      if ((this.shouldFilterColor.size == 0) && !templateHasErased) {
+        context.drawImage(template.bitmap, coordXtoDrawAt, coordYtoDrawAt);
+      }
+
+      // If we failed to get the template for this tile, we use a shoddy, buggy, failsafe
+      if (!templateBeforeFilter32) {
+        const templateBeforeFilter = context.getImageData(coordXtoDrawAt, coordYtoDrawAt, template.bitmap.width, template.bitmap.height);
+        templateBeforeFilter32 = new Uint32Array(templateBeforeFilter.data.buffer);
+      }
+
+      // Take the pre-filter template ImageData + the pre-filter tile ImageData, and use that to calculate the correct pixels
+      const timer = Date.now();
+      const {
+        correctPixels: pixelsCorrect,
+        filteredTemplate: templateAfterFilter
+      } = this.#calculateCorrectPixelsOnTile_And_FilterTile({
+        tile: tileBeforeTemplates32,
+        template: templateBeforeFilter32,
+        templateInfo: [coordXtoDrawAt, coordYtoDrawAt, template.bitmap.width, template.bitmap.height],
+        highlightPattern: highlightPattern,
+        highlightDisabled: highlightDisabled
+      });
+
+      let pixelsCorrectTotal = 0;
+      const transparentColorID = 0;
+
+      // For each color with correct pixels placed for this template...
+      for (const [color, total] of pixelsCorrect) {
+
+        if (color == transparentColorID) {continue;} // Skip Transparent color
+
+        pixelsCorrectTotal += total; // Add the current total for this color to the summed total of all correct
+      }
+
+      // 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
+      // 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);
+      }
+
+      console.log(`Finished calculating correct pixels & filtering colors for the tile ${tileCoords} in ${(Date.now() - timer) / 1000} seconds!\nThere are ${pixelsCorrectTotal} correct pixels.`);
+
+      // If "correct" does not exist as a key of the object "pixelCount", we create it
+      if (typeof template.instance.pixelCount['correct'] == 'undefined') {
+        template.instance.pixelCount['correct'] = {};
+      }
+
+      // Adds the correct pixel Map to the template instance
+      template.instance.pixelCount['correct'][tileCoords] = pixelsCorrect;
+    }
+
+    return await canvas.convertToBlob({ type: 'image/png' });
+  }
+
+  /** Imports the JSON object, and appends it to any JSON object already loaded
+   * @param {string} json - The JSON string to parse
+   */
+  importJSON(json) {
+
+    console.log(`Importing JSON...`);
+    console.log(json);
+
+    // If the passed in JSON is a Blue Marble template object...
+    if (json?.whoami == 'BlueMarble') {
+      this.#parseBlueMarble(json); // ...parse the template object as Blue Marble
+    }
+  }
+
+  /** Parses the Blue Marble JSON object
+   * @param {string} json - The JSON string to parse
+   * @since 0.72.13
+   */
+  async #parseBlueMarble(json) {
+
+    console.log(`Parsing BlueMarble...`);
+
+    const templates = json.templates;
+
+    console.log(`BlueMarble length: ${Object.keys(templates).length}`);
+
+    const schemaVersion = json?.schemaVersion;
+    const schemaVersionArray = schemaVersion.split(/[-\.\+]/); // SemVer -> string[]
+    const schemaVersionBleedingEdge = this.schemaVersion.split(/[-\.\+]/); // SemVer -> string[]
+    const scriptVersion = json?.scriptVersion;
+
+    console.log(`BlueMarble Template Schema: ${schemaVersion}; Script Version: ${scriptVersion}`);
+
+    // If MAJOR version is up-to-date...
+    if (schemaVersionArray[0] == schemaVersionBleedingEdge[0]) {
+
+      // If MINOR version is NOT up-to-date...
+      if (schemaVersionArray[1] != schemaVersionBleedingEdge[1]) {
+
+        // Spawns a new Template Wizard
+        const windowWizard = new WindowWizard(this.name, this.version, this.schemaVersion, this);
+        windowWizard.buildWindow();
+      }
+
+      // Load using the latest schema loader. It will be fine, probably...
+      this.templatesArray = await loadSchema({
+        tileSize: this.tileSize,
+        drawMult: this.drawMult,
+        templatesArray: this.templatesArray
+      });
+
+    } else if (schemaVersionArray[0] < schemaVersionBleedingEdge[0]) {
+      // Else if the MAJOR verison is out-of-date
+
+      // Spawns a new Template Wizard
+      const windowWizard = new WindowWizard(this.name, this.version, this.schemaVersion, this);
+      windowWizard.buildWindow();
+    
+    } else {
+      // We don't know what the schema is. Unsupported?
+
+      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
+     * @param {Object} params - Object containing parameters
+     * @param {number} params.tileSize - Size of tile
+     * @param {number} params.drawMult - Tile scale multiplier
+     * @param {Array<Template>} params.templatesArray - Array of Template instances
+     * @since 0.88.434
+     */
+    async function loadSchema({
+      tileSize: tileSize,
+      drawMult: drawMult,
+      templatesArray: templatesArray
+    }) {
+
+      // Run only if there are templates saved
+      if (Object.keys(templates).length > 0) {
+  
+        // For each template...
+        for (const template in templates) {
+  
+          const templateKey = template; // The identification key for the template. E.g., "0 $Z"
+          const templateValue = templates[template]; // The actual content of the template
+          console.log(`Template Key: ${templateKey}`);
+  
+          if (templates.hasOwnProperty(template)) {
+  
+            const templateKeyArray = templateKey.split(' '); // E.g., "0 $Z" -> ["0", "$Z"]
+            const sortID = Number(templateKeyArray?.[0]); // Sort ID of the template
+            const authorID = templateKeyArray?.[1] || '0'; // User ID of the person who exported the template
+            const displayName = templateValue.name || `Template ${sortID || ''}`; // Display name of the template
+            //const coords = templateValue?.coords?.split(',').map(Number); // "1,2,3,4" -> [1, 2, 3, 4]
+  
+            const pixelCount = {
+              total: templateValue.pixels?.total,
+              colors: new Map(Object.entries(templateValue.pixels?.colors || {}).map(([key, value]) => [Number(key), value]))
+            };
+  
+            const tilesbase64 = templateValue.tiles;
+            const templateTiles = {}; // Stores the template bitmap tiles for each tile.
+            const templateTiles32 = {}; // Stores the template Uint32Array tiles for each tile.
+  
+            const actualTileSize = tileSize * drawMult;
+  
+            for (const tile in tilesbase64) {
+              console.log(tile);
+              if (tilesbase64.hasOwnProperty(tile)) {
+                const encodedTemplateBase64 = tilesbase64[tile];
+                const templateUint8Array = base64ToUint8(encodedTemplateBase64); // Base 64 -> Uint8Array
+  
+                const templateBlob = new Blob([templateUint8Array], { type: "image/png" }); // Uint8Array -> Blob
+                const templateBitmap = await createImageBitmap(templateBlob) // Blob -> Bitmap
+                templateTiles[tile] = templateBitmap;
+  
+                // Converts to Uint32Array
+                const canvas = new OffscreenCanvas(actualTileSize, actualTileSize);
+                const context = canvas.getContext('2d');
+                context.drawImage(templateBitmap, 0, 0);
+                const imageData = context.getImageData(0, 0, templateBitmap.width, templateBitmap.height);
+                templateTiles32[tile] = new Uint32Array(imageData.data.buffer);
+              }
+            }
+  
+            // Creates a new Template class instance
+            const template = new Template({
+              displayName: displayName,
+              sortID: sortID || this.templatesArray?.length || 0,
+              authorID: authorID || '',
+              //coords: coords,
+            });
+            template.pixelCount = pixelCount;
+            template.chunked = templateTiles;
+            template.chunked32 = templateTiles32;
+            
+            templatesArray.push(template);
+            console.log(this.templatesArray);
+            console.log(`^^^ This ^^^`);
+          }
+        }
+      }
+
+      return templatesArray
+    }
+  }
+
+  /** Parses the OSU! Place JSON object
+   */
+  #parseOSU() {
+
+  }
+
+  /** Sets the `templatesShouldBeDrawn` boolean to a value.
+   * @param {boolean} value - The value to set the boolean to
+   * @since 0.73.7
+   */
+  setTemplatesShouldBeDrawn(value) {
+    this.templatesShouldBeDrawn = value;
+  }
+
+  /** 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,
+    highlightPattern: highlightPattern,
+    highlightDisabled: highlightDisabled
+  }) {
+
+    // Size of a pixel in actuality
+    const pixelSize = this.drawMult;
+
+    // Tile information
+    const tileWidth = this.tileSize * pixelSize;
+    const tileHeight = tileWidth;
+    const tilePixelOffsetY = -1; // Shift off of target template pixel to target on tile. E.g. "-1" would be the pixel above the template pixel on the tile
+    const tilePixelOffsetX = 0; // Shift off of target template pixel to target on tile. E.g. "-1" would be the pixel to the left of the template pixel on the tile
+
+    // Template information
+    const templateCoordX = templateInformation[0];
+    const templateCoordY = templateInformation[1];
+    const templateWidth = templateInformation[2];
+    const templateHeight = templateInformation[3];
+    const tolerance = this.paletteTolerance;
+
+    //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
+    const _colorpalette = new Map(); // Temp color palette
+
+    // 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
+        const tileColumn = (templateCoordX + templateColumn) + tilePixelOffsetX; // Template offset + current column
+        
+        // Retrieves the targeted pixels
+        const tilePixelAbove = tile32[(tileRow * tileWidth) + tileColumn];
+        const templatePixel = template32[(templateRow * templateWidth) + templateColumn];
+
+        // Obtains the alpha channel of the targeted pixels
+        const templatePixelAlpha = (templatePixel >>> 24) & 0xFF;
+        const tilePixelAlpha = (tilePixelAbove >>> 24) & 0xFF;
+
+        // 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)) {
+
+          // Sets template pixel to match tile background (which removes the template pixel from the user's view)
+          template32[(templateRow * templateWidth) + templateColumn] = tilePixelAbove;
+        }
+        // -----  END OF COLOR FILTER  -----
+
+        // -----        ERASED         -----
+        // If this pixel on the template is the Erased (#deface) color...
+        if (bestTemplateColorID == -1) {
+
+          const blackTrans = 0x20000000; // Black translucent color for Erased pixels
+
+          // If Erased color should be filtered
+          if (this.shouldFilterColor.get(bestTemplateColorID)) {
+            template32[(templateRow * templateWidth) + templateColumn] = 0x00000000; // Center (black, 0% opacity)
+          } else {
+            // Don't filter Erased color
+
+            // If the tile row and tile column are even,
+            // Or the tile row and tile column are odd...
+            if (((tileRow / pixelSize) & 1) == ((tileColumn / pixelSize) & 1)) {
+
+              // Sets the template pixels to be a semi-transparent, black grid
+              template32[(templateRow * templateWidth) + templateColumn] = blackTrans; // Center
+              template32[((templateRow - 1) * templateWidth) + (templateColumn - 1)] = blackTrans; // Top Left
+              template32[((templateRow - 1) * templateWidth) + (templateColumn + 1)] = blackTrans; // Top Right
+              template32[((templateRow + 1) * templateWidth) + (templateColumn - 1)] = blackTrans; // Bottom Left
+              template32[((templateRow + 1) * templateWidth) + (templateColumn + 1)] = blackTrans; // Bottom Right
+            } else {
+              // Else, either the row or column is odd, and the other is even.
+
+              // Sets the template pixels to the the inverse of a semi-transparent, black grid
+              template32[(templateRow * templateWidth) + templateColumn] = 0x00000000; // Center (black, 0% opacity)
+              template32[((templateRow - 1) * templateWidth) + (templateColumn)] = blackTrans; // Top Center
+              template32[((templateRow + 1) * templateWidth) + (templateColumn)] = blackTrans; // Bottom Center
+              template32[((templateRow) * templateWidth) + (templateColumn - 1)] = blackTrans; // Middle Left
+              template32[((templateRow) * templateWidth) + (templateColumn + 1)] = blackTrans; // Middle Right
+            }
+          }
+        }
+        // -----     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)) {
+
+          // Increments the count by 1 for the Erased (#deface) color.
+          // If the color ID has not been counted yet, default to 1
+          const colorIDcount = _colorpalette.get(bestTemplateColorID);
+          _colorpalette.set(bestTemplateColorID, colorIDcount ? colorIDcount + 1 : 1);
+          continue;
+        }
+        // If the code passes this point, the pixel is not a correct Erased color.
+
+        // If either pixel is transparent...
+        if ((templatePixelAlpha <= tolerance) || (tilePixelAlpha <= tolerance)) {
+          continue; // ...we skip it. We can't match the RGB color of transparent pixels.
+        }
+        // If the code passes this point, both pixels are opaque & not Erased.
+
+        // 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).
+        // If the color ID has not been counted yet, default to 1
+        const colorIDcount = _colorpalette.get(bestTemplateColorID);
+        _colorpalette.set(bestTemplateColorID, colorIDcount ? colorIDcount + 1 : 1);
+      }
+    }
+
+    console.log(`List of template pixels that match the tile:`);
+    console.log(_colorpalette);
+    return { correctPixels: _colorpalette, filteredTemplate: template32 };
+  }
+}
+
+
+
+ + + + +
+ +
+ +
+ Generated by JSDoc 4.0.5 on Tue Mar 17 2026 01:31:43 GMT+0000 (Coordinated Universal Time) using the Minami theme. +
+ + + + + diff --git a/docs/utils.js.html b/docs/utils.js.html new file mode 100644 index 0000000..e877652 --- /dev/null +++ b/docs/utils.js.html @@ -0,0 +1,546 @@ + + + + + + utils.js - Documentation + + + + + + + + + + + + + + + + + +
+ +

utils.js

+ + + + + + + +
+
+

+/** Returns a Date of when Wplace was last updated.
+ * This is obtained from a certain DOM element which contains the version of Wplace.
+ * @since 0.90.25
+ * @returns {Date | undefined} - The date that Wplace was last updated, as a Date.
+ */
+export function getWplaceVersion() {
+  const wplaceVersionElement = [...document.querySelectorAll(`body > div > .hidden`)].filter(match => /version:/i.test(match.textContent));
+  if (wplaceVersionElement[0]) { // If there is at least one match...
+    const wplaceUpdateTime = wplaceVersionElement[0].textContent?.match(/\d+/); // Obtain the last update time, which is Unix Epoch in milliseconds
+    return wplaceUpdateTime ? new Date(Number(wplaceUpdateTime[0])) : undefined; // Return the time as a Date, or undefined
+  }
+  return undefined;
+}
+
+/** Halts execution of this specific userscript, for the specified time.
+ * This will not block the thread.
+ * @param {number} time - Time to wait in milliseconds
+ * @since 0.88.483
+ * @returns {Promise} Promise of a setTimeout()
+ */
+export function sleep(time) {
+  return new Promise(resolve => setTimeout(resolve, time));
+}
+
+/** View the canvas in a new tab.
+ * @param {HTMLCanvasElement | OffscreenCanvas} canvas - The canvas to view
+ * @param {number} [lifeDuration=60_000] - (Optional) The lifetime of the URL blob in milliseconds
+ * @since 0.88.484
+ */
+export async function viewCanvasInNewTab(canvas, lifeDuration = 60_000) {
+  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), lifeDuration); // Destroys the blob after this time (default of 1 minute)
+}
+
+/** Returns the localized number format.
+ * @param {number} number - The number to localize
+ * @since 0.88.472
+ * @returns {string} Localized number as a string
+ */
+export function localizeNumber(number) {
+  const numberFormat = new Intl.NumberFormat();
+  return numberFormat.format(number);
+}
+
+/** Returns the localized percentage format.
+ * @param {number} percent - The percentage to localize
+ * @since 0.88.472
+ * @returns {string} Localized percentage as a string
+ */
+export function localizePercent(percent) {
+  const percentFormat = new Intl.NumberFormat(undefined, {
+    style: 'percent',
+    minimumFractionDigits: 2,
+    maximumFractionDigits: 2
+  });
+  return percentFormat.format(percent);
+}
+
+/** Returns the localized date format.
+ * @param {number} date - The date to localize
+ * @since 0.88.472
+ * @returns {string} Localized date as a string
+ */
+export function localizeDate(date) {
+  const options = {
+    month: 'long', // July
+    day: 'numeric', // 23
+    hour: '2-digit', // 17
+    minute: '2-digit', // 47
+    second: '2-digit' // 00
+  };
+  return date.toLocaleString(undefined, options);
+}
+
+/** Returns the localized duration format.
+ * @param {number} durationTotalMs - The duration to localize, in milliseconds
+ * @since 0.88.472
+ * @returns {string} Localized duration as a string
+ */
+export function localizeDuration(durationTotalMs) {
+
+  // "Total" indicates it is the total time for that unit. E.g. 62 minutes is "62" minutes.
+  const durationTotalSec = Math.floor(durationTotalMs / 1000);
+  const durationTotalHr = Math.floor(durationTotalSec / 3600);
+
+  // "Only" indicates it is formatted in that unit. E.g. 62 minutes is "2" minutes.
+  const durationOnlySec = Math.floor(durationTotalSec % 60);
+  const durationOnlyMin = Math.floor((durationTotalSec % 3600) / 60);
+
+  // Duration Object for localization
+  const duration = {
+    hours: durationTotalHr,
+    minutes: durationOnlyMin,
+    seconds: durationOnlySec
+  };
+
+  // Options Object for localization
+  const options = {
+    style: 'short'
+  };
+
+  return new Intl.DurationFormat(undefined, options).format(duration);
+}
+
+/** Sanitizes HTML to display as plain-text.
+ * This prevents some Cross Site Scripting (XSS).
+ * This is handy when you are displaying user-made data, and you *must* use innerHTML.
+ * @param {string} text - The text to sanitize
+ * @returns {string} HTML escaped string
+ * @since 0.44.2
+ * @example
+ * const paragraph = document.createElement('p');
+ * paragraph.innerHTML = escapeHTML('<u>Foobar.</u>');
+ * // Output:
+ * // (Does not include the paragraph element)
+ * // (Output is not HTML formatted)
+ * <p>
+ *   "<u>Foobar.</u>"
+ * </p>
+ */
+export function escapeHTML(text) {
+  const div = document.createElement('div'); // Creates a div
+  div.textContent = text; // Puts the text in a PLAIN-TEXT property
+  return div.innerHTML; // Returns the HTML property of the div
+}
+
+/** Converts the server tile-pixel coordinate system to the displayed tile-pixel coordinate system.
+ * @param {Array<string, string>} tile - The tile to convert
+ * @param {Array<string, string>} pixel - The pixel to convert
+ * @returns {Array<number, number>} Tile and pixel coordinate pair
+ * @since 0.42.4
+ * @example
+ * console.log(serverTPtoDisplayTP(['12', '123'], ['34', '567'])); // [34, 3567]
+ */
+export function serverTPtoDisplayTP(tile, pixel) {
+  return [((parseInt(tile[0]) % 4) * 1000) + parseInt(pixel[0]), ((parseInt(tile[1]) % 4) * 1000) + parseInt(pixel[1])];
+}
+
+/** Negative-Safe Modulo. You can pass negative numbers into this.
+ * @param {number} a - The first number
+ * @param {number} b - The second number
+ * @returns {number} Result
+ * @author osuplace
+ * @since 0.55.8
+ */
+export function negativeSafeModulo(a, b) {
+  return (a % b + b) % b;
+}
+
+/** Bypasses terser's stripping of console function calls.
+ * This is so the non-obfuscated code will contain debugging console calls, but the distributed version won't.
+ * However, the distributed version needs to call the console somehow, so this wrapper function is how.
+ * This is the same as `console.log()`.
+ * @param {...any} args - Arguments to be passed into the `log()` function of the Console
+ * @since 0.58.9
+ */
+export function consoleLog(...args) {((consoleLog) => consoleLog(...args))(console.log);}
+
+/** Bypasses terser's stripping of console function calls.
+ * This is so the non-obfuscated code will contain debugging console calls, but the distributed version won't.
+ * However, the distributed version needs to call the console somehow, so this wrapper function is how.
+ * This is the same as `console.error()`.
+ * @param {...any} args - Arguments to be passed into the `error()` function of the Console
+ * @since 0.58.13
+ */
+export function consoleError(...args) {((consoleError) => consoleError(...args))(console.error);}
+
+/** Bypasses terser's stripping of console function calls.
+ * This is so the non-obfuscated code will contain debugging console calls, but the distributed version won't.
+ * However, the distributed version needs to call the console somehow, so this wrapper function is how.
+ * This is the same as `console.warn()`.
+ * @param {...any} args - Arguments to be passed into the `warn()` function of the Console
+ * @since 0.58.13
+ */
+export function consoleWarn(...args) {((consoleWarn) => consoleWarn(...args))(console.warn);}
+
+/** Encodes a number into a custom encoded string.
+ * @param {number} number - The number to encode
+ * @param {string} encoding - The characters to use when encoding
+ * @since 0.65.2
+ * @returns {string} Encoded string
+ * @example
+ * const encode = '012abcABC'; // Base 9
+ * console.log(numberToEncoded(0, encode)); // 0
+ * console.log(numberToEncoded(5, encode)); // c
+ * console.log(numberToEncoded(15, encode)); // 1A
+ * console.log(numberToEncoded(12345, encode)); // 1BCaA
+ */
+export function numberToEncoded(number, encoding) {
+
+  if (number === 0) return encoding[0]; // End quickly if number equals 0. No special calculation needed
+
+  let result = ''; // The encoded string
+  const base = encoding.length; // The number of characters used, which determines the base
+
+  // Base conversion algorithm
+  while (number > 0) {
+    result = encoding[number % base] + result; // Find's the character's encoded value determined by the modulo of the base
+    number = Math.floor(number / base); // Divides the number by the base so the next iteration can find the next modulo character
+  }
+
+  return result; // The final encoded string
+}
+
+/** Decodes a number from a custom encoded string.
+ * @param {string} encoded - The encoded string
+ * @param {string} encoding - The characters to use when decoding
+ * @since 0.88.448
+ * @returns {number} Decoded number
+ * @example
+ * const encode = '012abcABC'; // Base 9
+ * console.log(encodedToNumber('0', encode));     // 0
+ * console.log(encodedToNumber('c', encode));     // 5
+ * console.log(encodedToNumber('1A', encode));    // 15
+ * console.log(encodedToNumber('1BCaA', encode)); // 12345
+ */
+export function encodedToNumber(encoded, encoding) {
+
+  let decodedNumber = 0; // The decoded number
+  const base = encoding.length; // The number of characters used, which determins the base
+
+  // For every character in the encoded string...
+  for (const character of encoded) {
+
+    const decodedCharacter = encoding.indexOf(character); // Decodes the character
+
+    // If no matching decode was found for this character...
+    if (decodedCharacter == -1) {
+      consoleError(`Invalid character '${character}' encountered whilst decoding! Is the decode alphabet/base incorrect?`);
+    }
+
+    decodedNumber = (decodedNumber * base) + decodedCharacter; // Adds the decoded character to the final number
+  }
+
+  return decodedNumber; // Returns the decoded number
+}
+
+/** Converts a Uint8 array to base64 using the browser's built-in binary to ASCII function
+ * @param {Uint8Array} uint8 - The Uint8Array to convert
+ * @returns {Uint8Array} The base64 encoded Uint8Array
+ * @since 0.72.9
+ */
+export function uint8ToBase64(uint8) {
+  let binary = '';
+  for (let i = 0; i < uint8.length; i++) {
+    binary += String.fromCharCode(uint8[i]);
+  }
+  return btoa(binary); // Binary to ASCII
+}
+
+/** Decodes a base 64 encoded Uint8 array using the browser's built-in ASCII to binary function
+ * @param {Uint8Array} base64 - The base 64 encoded Uint8Array to convert
+ * @returns {Uint8Array} The decoded Uint8Array
+ * @since 0.72.9
+ */
+export function base64ToUint8(base64) {
+  const binary = atob(base64); // ASCII to Binary
+  const array = new Uint8Array(binary.length);
+  for (let i = 0; i < binary.length; i++) {
+    array[i] = binary.charCodeAt(i);
+  }
+  return array;
+}
+
+/** Handles reading from the clipboard.
+ * Assume this only returns text.
+ * Assume this requires user input.
+ * @param {ClipboardEvent} [event=undefined] - (Optional) The clipboard event that triggered this to run
+ * @since 0.88.426
+ * @returns {string} The clipboard data as a string
+ */
+export async function getClipboardData(event = undefined) {
+
+  let data = ''; // Data from clipboard
+
+  // Try using the event, if any was provided
+  if (event) {
+    data = event.clipboardData.getData('text/plain');
+  }
+
+  if (data.length != 0) {return data;} // Continue only if data is still empty
+  
+  // Try using the navigator clipboard
+  await navigator.clipboard.readText().then(text => {
+    data = text;
+  }).catch(error => {
+    consoleLog(`Failed to retrieve clipboard data using navigator! Using fallback methods...`);
+  });
+
+  if (data.length != 0) {return data;} // Continue only if data is still empty
+
+  // Try using IE clipboard
+  data = window.clipboardData?.getData('Text');
+
+  return data;
+}
+
+/** Calcualtes the relative luminance of an RGB value
+ * @param {Array<Number, Number, Number>} array - The RGB values as an array
+ * @returns {Number} The relative luminance as a Number
+ * @since 0.88.180
+ */
+export function calculateRelativeLuminance(array) {
+
+  // Convert the 0-255 range to 0-1
+  const srgb = array.map(channel => {
+    channel /= 255;
+    return (channel <= 0.03928) ? (channel / 12.92) : Math.pow((channel + 0.055) / 1.055, 2.4);
+  });
+
+  // https://en.wikipedia.org/wiki/Relative_luminance#Relative_luminance_and_%22gamma_encoded%22_colorspaces
+  return (0.2126 * srgb[0]) + (0.7152 * srgb[1]) + (0.0722 * srgb[2]);
+}
+
+/** Converts an RGB color to hexdecimal color.
+ * Octothorpe not included.
+ * @param {number | Array<number, number, number>} red - The Red channel of the RGB color, or all three channels as an Array
+ * @param {number} [green] - The Green channel of the RGB color
+ * @param {number} [blue] - The Blue channel of the RGB color
+ * @returns {string} Hex color code as string
+ * @since 0.90.31
+ */
+export function rgbToHex(red, green, blue) {
+  if (Array.isArray(red)) {[red, green, blue] = red;} // Deconstruct the Array if an Array was passed in
+  return ((1 << 24) | (red << 16) | (green << 8) | blue).toString(16).slice(1); // Packs it into a 24-bit integer, then converts it to base16.
+}
+
+/** Converts a hexdecimal color to an RGB color.
+ * Alpha channel not supported.
+ * @param {string} hex - Hex color code as string
+ * @returns {Array<number, number, number>} RGB color as an Array
+ * @since 0.90.31
+ */
+export function hexToRGB(hex) {
+  hex = (hex[0] == '#') ? hex.slice(1) : hex; // Removes the octothorpe, if any
+  const packedIntRGB = parseInt(hex, 16); // Converts (base16) into an integer
+  return [(packedIntRGB >> 16 & 255), (packedIntRGB >> 8 & 255), (packedIntRGB & 255)]; // Unpacks the integer into the RGB channels
+}
+
+/** Returns the coordinate input fields
+ * @returns {Element[]} The 4 coordinate Inputs
+ * @since 0.74.0
+ */
+export function selectAllCoordinateInputs(document) {
+  coords = [];
+
+  coords.push(document.querySelector('#bm-input-tx'));
+  coords.push(document.querySelector('#bm-input-ty'));
+  coords.push(document.querySelector('#bm-input-px'));
+  coords.push(document.querySelector('#bm-input-py'));
+
+  return coords;
+}
+
+/** Processes the palette used for Blue Marble.
+ * Each ID is sorted from smallest to largest.
+ * Color ID's are integers, which can be negative.
+ * Custom colors have been added for the Blue Marble purposes.
+ * Wplace palette colors have not been modified.
+ * @since 0.88.6
+ */
+export function colorpaletteForBlueMarble(tolerance) {
+
+  const colorpaletteBM = colorpalette; // Makes a copy
+
+  // Adds the Blue Marble color for "erased" and "other" pixels to the palette list
+  colorpaletteBM.unshift({ "id": -1,  "premium": false, "name": "Erased",      "rgb": [222, 250, 206] });
+  colorpaletteBM.unshift({ "id": -2,  "premium": false, "name": "Other",       "rgb": [  0,   0,   0] });
+
+  const lookupTable = new Map();
+
+  // For each color in Blue Marble's palette...
+  for (const color of colorpaletteBM) {
+    if ((color.id == 0) || (color.id == -2)) continue; // skip Transparent or Other colors
+
+    // Target RGB values. These are exactly correct.
+    const targetRed = color.rgb[0];
+    const targetGreen = color.rgb[1];
+    const targetBlue = color.rgb[2];
+
+    // For each RGB value in the range of RGB values centered on the target RGB value for each channel...
+    for (let deltaRedRange = -tolerance; deltaRedRange <= tolerance; deltaRedRange++) {
+      for (let deltaGreenRange = -tolerance; deltaGreenRange <= tolerance; deltaGreenRange++) {
+        for (let deltaBlueRange = -tolerance; deltaBlueRange <= tolerance; deltaBlueRange++) {
+          // Basically, we are making a "cube" around each target value.
+          // Say the tolerance is 3. The size of the cube will be ((3*2)+1)^3 which is 343 total.
+          // This means 343 colors will be Mapped as associated to the target color ID because 343 colors are in the "cube" surrounding and including the target color
+
+          // This specific deviation from the target RGB color values within the cube
+          const derivativeRed = targetRed + deltaRedRange;
+          const derivativeGreen = targetGreen + deltaGreenRange;
+          const derivativeBlue = targetBlue + deltaBlueRange;
+
+          // If it is impossible for the color to exist, then skip the color
+          if (derivativeRed < 0 || derivativeRed > 255 || derivativeGreen < 0 || derivativeGreen > 255 || derivativeBlue < 0 || derivativeBlue > 255) continue;
+
+          // Packed into 32-bit integer like RGBA = 0xAARRGGBB with the alpha channel forced to be 255
+          // Also, it is forced to be an unsigned 32-bit integer
+          const derivativeColor32 = ((255 << 24) | (derivativeBlue << 16) | (derivativeGreen << 8) | derivativeRed) >>> 0;
+          if (!lookupTable.has(derivativeColor32)) {
+            lookupTable.set(derivativeColor32, color.id);
+          }
+        }
+      }
+    }
+  }
+
+  return {palette: colorpaletteBM, LUT: lookupTable}
+}
+
+/** The color palette used by wplace.live
+ * @since 0.78.0
+ * @examples
+ * import utils from 'src/utils.js';
+ * console.log(utils[5]?.name); // "White"
+ * console.log(utils[5]?.rgb); // [255, 255, 255]
+ */
+export const colorpalette = [
+  { "id": 0,  "premium": false, "name": "Transparent",      "rgb": [  0,   0,   0] },
+  { "id": 1,  "premium": false, "name": "Black",            "rgb": [  0,   0,   0] },
+  { "id": 2,  "premium": false, "name": "Dark Gray",        "rgb": [ 60,  60,  60] },
+  { "id": 3,  "premium": false, "name": "Gray",             "rgb": [120, 120, 120] },
+  { "id": 4,  "premium": false, "name": "Light Gray",       "rgb": [210, 210, 210] },
+  { "id": 5,  "premium": false, "name": "White",            "rgb": [255, 255, 255] },
+  { "id": 6,  "premium": false, "name": "Deep Red",         "rgb": [ 96,   0,  24] },
+  { "id": 7,  "premium": false, "name": "Red",              "rgb": [237,  28,  36] },
+  { "id": 8,  "premium": false, "name": "Orange",           "rgb": [255, 127,  39] },
+  { "id": 9,  "premium": false, "name": "Gold",             "rgb": [246, 170,   9] },
+  { "id": 10, "premium": false, "name": "Yellow",           "rgb": [249, 221,  59] },
+  { "id": 11, "premium": false, "name": "Light Yellow",     "rgb": [255, 250, 188] },
+  { "id": 12, "premium": false, "name": "Dark Green",       "rgb": [ 14, 185, 104] },
+  { "id": 13, "premium": false, "name": "Green",            "rgb": [ 19, 230, 123] },
+  { "id": 14, "premium": false, "name": "Light Green",      "rgb": [135, 255,  94] },
+  { "id": 15, "premium": false, "name": "Dark Teal",        "rgb": [ 12, 129, 110] },
+  { "id": 16, "premium": false, "name": "Teal",             "rgb": [ 16, 174, 166] },
+  { "id": 17, "premium": false, "name": "Light Teal",       "rgb": [ 19, 225, 190] },
+  { "id": 18, "premium": false, "name": "Dark Blue",        "rgb": [ 40,  80, 158] },
+  { "id": 19, "premium": false, "name": "Blue",             "rgb": [ 64, 147, 228] },
+  { "id": 20, "premium": false, "name": "Cyan",             "rgb": [ 96, 247, 242] },
+  { "id": 21, "premium": false, "name": "Indigo",           "rgb": [107,  80, 246] },
+  { "id": 22, "premium": false, "name": "Light Indigo",     "rgb": [153, 177, 251] },
+  { "id": 23, "premium": false, "name": "Dark Purple",      "rgb": [120,  12, 153] },
+  { "id": 24, "premium": false, "name": "Purple",           "rgb": [170,  56, 185] },
+  { "id": 25, "premium": false, "name": "Light Purple",     "rgb": [224, 159, 249] },
+  { "id": 26, "premium": false, "name": "Dark Pink",        "rgb": [203,   0, 122] },
+  { "id": 27, "premium": false, "name": "Pink",             "rgb": [236,  31, 128] },
+  { "id": 28, "premium": false, "name": "Light Pink",       "rgb": [243, 141, 169] },
+  { "id": 29, "premium": false, "name": "Dark Brown",       "rgb": [104,  70,  52] },
+  { "id": 30, "premium": false, "name": "Brown",            "rgb": [149, 104,  42] },
+  { "id": 31, "premium": false, "name": "Beige",            "rgb": [248, 178, 119] },
+  { "id": 32, "premium": true,  "name": "Medium Gray",      "rgb": [170, 170, 170] },
+  { "id": 33, "premium": true,  "name": "Dark Red",         "rgb": [165,  14,  30] },
+  { "id": 34, "premium": true,  "name": "Light Red",        "rgb": [250, 128, 114] },
+  { "id": 35, "premium": true,  "name": "Dark Orange",      "rgb": [228,  92,  26] },
+  { "id": 36, "premium": true,  "name": "Light Tan",        "rgb": [214, 181, 148] },
+  { "id": 37, "premium": true,  "name": "Dark Goldenrod",   "rgb": [156, 132,  49] },
+  { "id": 38, "premium": true,  "name": "Goldenrod",        "rgb": [197, 173,  49] },
+  { "id": 39, "premium": true,  "name": "Light Goldenrod",  "rgb": [232, 212,  95] },
+  { "id": 40, "premium": true,  "name": "Dark Olive",       "rgb": [ 74, 107,  58] },
+  { "id": 41, "premium": true,  "name": "Olive",            "rgb": [ 90, 148,  74] },
+  { "id": 42, "premium": true,  "name": "Light Olive",      "rgb": [132, 197, 115] },
+  { "id": 43, "premium": true,  "name": "Dark Cyan",        "rgb": [ 15, 121, 159] },
+  { "id": 44, "premium": true,  "name": "Light Cyan",       "rgb": [187, 250, 242] },
+  { "id": 45, "premium": true,  "name": "Light Blue",       "rgb": [125, 199, 255] },
+  { "id": 46, "premium": true,  "name": "Dark Indigo",      "rgb": [ 77,  49, 184] },
+  { "id": 47, "premium": true,  "name": "Dark Slate Blue",  "rgb": [ 74,  66, 132] },
+  { "id": 48, "premium": true,  "name": "Slate Blue",       "rgb": [122, 113, 196] },
+  { "id": 49, "premium": true,  "name": "Light Slate Blue", "rgb": [181, 174, 241] },
+  { "id": 50, "premium": true,  "name": "Light Brown",      "rgb": [219, 164,  99] },
+  { "id": 51, "premium": true,  "name": "Dark Beige",       "rgb": [209, 128,  81] },
+  { "id": 52, "premium": true,  "name": "Light Beige",      "rgb": [255, 197, 165] },
+  { "id": 53, "premium": true,  "name": "Dark Peach",       "rgb": [155,  82,  73] },
+  { "id": 54, "premium": true,  "name": "Peach",            "rgb": [209, 128, 120] },
+  { "id": 55, "premium": true,  "name": "Light Peach",      "rgb": [250, 182, 164] },
+  { "id": 56, "premium": true,  "name": "Dark Tan",         "rgb": [123,  99,  82] },
+  { "id": 57, "premium": true,  "name": "Tan",              "rgb": [156, 132, 107] },
+  { "id": 58, "premium": true,  "name": "Dark Slate",       "rgb": [ 51,  57,  65] },
+  { "id": 59, "premium": true,  "name": "Slate",            "rgb": [109, 117, 141] },
+  { "id": 60, "premium": true,  "name": "Light Slate",      "rgb": [179, 185, 209] },
+  { "id": 61, "premium": true,  "name": "Dark Stone",       "rgb": [109, 100,  63] },
+  { "id": 62, "premium": true,  "name": "Stone",            "rgb": [148, 140, 107] },
+  { "id": 63, "premium": true,  "name": "Light Stone",      "rgb": [205, 197, 158] }
+];
+
+
+
+ + + + +
+ +
+ +
+ Generated by JSDoc 4.0.5 on Tue Mar 17 2026 01:31:43 GMT+0000 (Coordinated Universal Time) using the Minami theme. +
+ + + + +