Did most new template creation system

This commit is contained in:
SwingTheVine 2025-07-31 18:48:44 -04:00
parent d56f952f4b
commit d1d6ebe7cc
9 changed files with 270 additions and 29 deletions

File diff suppressed because one or more lines are too long

View file

@ -35,7 +35,7 @@
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/LICENSE.txt" target="_blank"><img alt="Software License: MPL-2.0" src="https://img.shields.io/badge/Software_License-MPL--2.0-brightgreen?style=flat"></a>
<a href="https://discord.gg/tpeBPy46hf" target="_blank"><img alt="Contact Me" src="https://img.shields.io/badge/Contact_Me-gray?style=flat&logo=Discord&logoColor=white&logoSize=auto&labelColor=cornflowerblue"></a>
<a href="" target="_blank"><img alt="WakaTime" src="https://img.shields.io/badge/Coding_Time-59hrs_0mins-blue?style=flat&logo=wakatime&logoColor=black&logoSize=auto&labelColor=white"></a>
<a href="" target="_blank"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-354-black?style=flat"></a>
<a href="" target="_blank"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-395-black?style=flat"></a>
<a href="" target="_blank"><img alt="Total Lines of Code" src="https://tokei.rs/b1/github/SwingTheVine/Wplace-BlueMarble?category=code"></a>
<a href="" target="_blank"><img alt="Total Comments" src="https://tokei.rs/b1/github/SwingTheVine/Wplace-BlueMarble?category=comments"></a>
<a href="" target="_blank"><img alt="Compression" src="https://img.shields.io/badge/Compression-73.03%25-blue"></a>
@ -44,7 +44,7 @@
<h2>Overview</h2>
<p>
Welcome to Blue Marble! Blue Marble is a userscript for the website <a href="https://wplace.live/" target="_blank">wplace.live</a>.
Welcome to Blue Marble! Blue Marble is a userscript for the website <a href="https://wplace.live/" target="_blank">wplace.live</a>. If you like this userscript, please ⭐ the repository!
<h3>Installation Instructions</h3>
<a href="" target="_blank"><img alt="Supported Browsers" src="https://img.shields.io/badge/Browsers-Chrome%20%7C%20Firefox%20%7C%20Safari%20%7C%20IE-orange?style=flat"></a>

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "wplace-bluemarble",
"version": "0.64.12",
"version": "0.65.41",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
"version": "0.64.12",
"version": "0.65.41",
"devDependencies": {
"esbuild": "^0.25.0",
"terser": "^5.43.1"

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.65.0",
"version": "0.65.41",
"type": "module",
"scripts": {
"build": "node build/build.js",

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.65.0
// @version 0.65.41
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine
// @license MPL-2.0

View file

@ -4,7 +4,7 @@
*/
import TemplateManager from "./templateManager.js";
import { escapeHTML, serverTPtoDisplayTP } from "./utils.js";
import { escapeHTML, numberToEncoded, serverTPtoDisplayTP } from "./utils.js";
export default class ApiManager {
@ -62,6 +62,15 @@ export default class ApiManager {
}
const nextLevelPixels = Math.ceil(Math.pow(Math.floor(dataJSON['level']) * Math.pow(30, 0.65), (1/0.65)) - dataJSON['pixelsPainted']); // Calculates pixels to the next level
console.log(dataJSON['id']);
if (!!dataJSON['id'] || dataJSON['id'] === 0) {
console.log(numberToEncoded(
dataJSON['id'],
'!#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'
));
}
this.templateManager.userID = dataJSON['id'];
overlay.updateInnerHTML('bm-user-name', `Username: <b>${escapeHTML(dataJSON['name'])}</b>`); // Updates the text content of the username field
overlay.updateInnerHTML('bm-user-droplets', `Droplets: <b>${new Intl.NumberFormat().format(dataJSON['droplets'])}</b>`); // Updates the text content of the droplets field

View file

@ -165,7 +165,7 @@ document.head.appendChild(stylesheetLink);
// CONSTRUCTORS
const observers = new Observers(); // Constructs a new Observers object
const overlay = new Overlay(name, version); // Constructs a new Overlay object
const templateManager = new TemplateManager(); // Constructs a new TemplateManager object
const templateManager = new TemplateManager(name, version); // Constructs a new TemplateManager object
const apiManager = new ApiManager(templateManager); // Constructs a new ApiManager object
overlay.setApiManager(apiManager); // Sets the API manager
@ -223,10 +223,10 @@ function buildOverlayMain() {
}
}
).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-tx', 'placeholder': 'Tl X', 'min': 0, 'max': 2047, 'step': 1}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-ty', 'placeholder': 'Tl Y', 'min': 0, 'max': 2047, 'step': 1}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-px', 'placeholder': 'Px X', 'min': 0, 'max': 2047, 'step': 1}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-py', 'placeholder': 'Px Y', 'min': 0, 'max': 2047, 'step': 1}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-tx', 'placeholder': 'Tl X', 'min': 0, 'max': 2047, 'step': 1, 'required': true}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-ty', 'placeholder': 'Tl Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-px', 'placeholder': 'Px X', 'min': 0, 'max': 2047, 'step': 1, 'required': true}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-py', 'placeholder': 'Px Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true}).buildElement()
.buildElement()
.addInputFile({'id': 'bm-input-file-template', 'textContent': 'Upload Template', 'accept': 'image/png, image/jpeg, image/webp, image/bmp, image/gif'}).buildElement()
.addDiv({'id': 'bm-contain-buttons-template'})
@ -234,13 +234,24 @@ function buildOverlayMain() {
button.onclick = () => {
const input = document.querySelector('#bm-input-file-template');
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;}
console.log(`TCoords: ${apiManager.templateCoordsTilePixel}\nCoords: ${apiManager.coordsTilePixel}`);
apiManager.templateCoordsTilePixel = apiManager.coordsTilePixel; // Update template coords
console.log(`TCoords: ${apiManager.templateCoordsTilePixel}\nCoords: ${apiManager.coordsTilePixel}`);
templateManager.setTemplateImage(input.files[0]);
templateManager.createTemplate(input.files[0], input.files[0]?.name.replace(/\.[^/.]+$/, ''), [Number(coordTlX.value), Number(coordTlY.value), Number(coordPxX.value), Number(coordPxY.value)]);
// console.log(`TCoords: ${apiManager.templateCoordsTilePixel}\nCoords: ${apiManager.coordsTilePixel}`);
// apiManager.templateCoordsTilePixel = apiManager.coordsTilePixel; // Update template coords
// console.log(`TCoords: ${apiManager.templateCoordsTilePixel}\nCoords: ${apiManager.coordsTilePixel}`);
// templateManager.setTemplateImage(input.files[0]);
instance.handleDisplayStatus(`Drew to canvas!`);
}
@ -253,6 +264,12 @@ function buildOverlayMain() {
.addButton({'id': 'bm-button-teleport', 'className': 'bm-help', 'textContent': '✈'}).buildElement()
.addButton({'id': 'bm-button-favorite', 'className': 'bm-help', 'innerHTML': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><polygon points="10,2 12,7.5 18,7.5 13.5,11.5 15.5,18 10,14 4.5,18 6.5,11.5 2,7.5 8,7.5" fill="white"></polygon></svg>'}).buildElement()
.addButton({'id': 'bm-button-templates', 'className': 'bm-help', 'innerHTML': '🖌'}).buildElement()
.addButton({'id': 'bm-button-convert', 'className': 'bm-help', 'innerHTML': '🎨'},
(instance, button) => {
button.addEventListener('click', () => {
window.open('https://pepoafonso.github.io/color_converter_wplace/', '_blank', 'noopener noreferrer');
});
}).buildElement()
.buildElement()
.addSmall({'textContent': 'Made by SwingTheVine', 'style': 'margin-top: auto;'}).buildElement()
.buildElement()

View file

@ -1,28 +1,56 @@
import { numberToEncoded } from "./utils";
/** Manages the template system.
* This class handles all external requests for modification to a Template.
* @since 0.55.8
* @example
* // Example of JSON structure for a template
{
"scriptVersion": "1.13.0",
"schemaVersion": "2.1.0",
"templates": {
""
}
}
* // JSON structure for a template
* {
* "whoami": "BlueMarble",
* "scriptVersion": "1.13.0",
* "schemaVersion": "2.1.0",
* "templates": {
* "0 $Z": {
* "name": "My Template",
* "enabled": true,
* "tiles": {
* "1231,0047,183,593": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA",
* "1231,0048,183,000": "data:image/png;AAAFCAYAAACNbyblAAAAHElEQVQI12P4"
* }
* },
* "1 $Z": {
* "name": "My Template",
* "enabled": false,
* "tiles": {
* "375,1846,276,188": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA",
* "376,1846,000,188": "data:image/png;AAAFCAYAAACNbyblAAAAHElEQVQI12P4"
* }
* }
* }
* }
*/
export default class TemplateManager {
/** The constructor for the {@link TemplateManager} class.
* @since 0.55.8
*/
constructor() {
constructor(name, version) {
// Meta
this.name = name; // Name of userscript
this.version = version; // Version of userscript
this.templatesVersion = '1.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
// Template
this.canvasTemplate = null; // Our canvas
this.canvasTemplateZoomed = null; // The template when zoomed out
this.canvasTemplateID = 'bm-canvas'; // Our canvas ID
this.canvasMainID = 'div#map canvas.maplibregl-canvas'; // The selector for the main canvas
this.template = null; // The template image.
this.templateState = ''; // The state of the template ('blob', 'proccessing', 'template', etc.)
this.templates = null; // All templates currently loaded (JSON)
}
/** Retrieves the pixel art canvas.
@ -65,18 +93,177 @@ export default class TemplateManager {
return this.canvasTemplate; // Return the new canvas
}
createTemplate() {
/** 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.templatesVersion, // 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
*/
async createTemplate(blob, name, coords) {
// Creates the JSON object if it does not already exist
if (!this.templates) {this.templates = await this.createJSON();}
const tileSize = 1000; // The size of a tile in pixels
console.log(`Awaiting creation...`);
// 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.templates.templates[`${this.templates.templates.length || 0} ${numberToEncoded(this.userID || 0, this.encodingBase)}`] = {
"name": name, // Display name of template
"tiles": await this.#createTemplateTiles(blob, coords, tileSize)
};
console.log(this.templates);
}
/** Creates chunks of the template for each tile.
* @param {File} blob - The File blob to process
* @param {Array<number, number, number, number>} coords - The coordinates of the top left corner of the template
* @param {number} tileSize - The size of a tile (assumes tiles are square)
* @returns {Object} Collection of template bitmaps in a Object
* @since 0.65.4
*/
async #createTemplateTiles(blob, coords, tileSize) {
console.log(coords);
const shreadSize = 3; // Scale image factor. Must be odd
const bitmap = await createImageBitmap(blob); // Creates a bitmap image
const imageWidth = bitmap.width;
const imageHeight = bitmap.height;
const templateTiles = {}; // Holds the template tiles
const canvas = new OffscreenCanvas(tileSize, tileSize);
const context = canvas.getContext('2d', { willReadFrequently: true });
// For every tile...
for (let pixelY = coords[3]; pixelY < (imageHeight + 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(tileSize - (pixelY % tileSize), imageHeight - ((pixelY - coords[3]) * (pixelY != coords[3])));
console.log(`Math.min(${tileSize} - (${pixelY} % ${tileSize}), ${imageHeight} - (${pixelY - coords[3]} * (${pixelY} != ${coords[3]})))`);
for (let pixelX = coords[2]; pixelX < (imageWidth + 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(tileSize - (pixelX % tileSize), imageWidth - ((pixelX - coords[2]) * (pixelX != coords[2])));
console.log(`Math.min(${tileSize} - (${pixelX} % ${tileSize}), ${imageWidth} - (${pixelX} * (${pixelX} != ${coords[2]})))`);
console.log(`Draw Size X: ${drawSizeX}\nDraw Size Y: ${drawSizeY}`);
console.log(`Draw X: ${drawSizeX}\nDraw Y: ${drawSizeY}\nCanvas Width: ${drawSizeX * shreadSize}\nCanvas Height: ${drawSizeY * shreadSize}`);
// Change the canvas size and wipe the canvas
canvas.width = drawSizeX * shreadSize;
canvas.height = drawSizeY * shreadSize;
console.log(`Getting X ${pixelX}-${pixelX + drawSizeX}\nGetting Y ${pixelY}-${pixelY + drawSizeY}`);
// Draws the template segment on this tile segment
context.clearRect(0, 0, drawSizeX * shreadSize, drawSizeY * shreadSize); // Clear any previous drawing (only runs when canvas size does not change)
context.drawImage(bitmap, pixelX, pixelY, drawSizeX, drawSizeY, 0, 0, drawSizeX * shreadSize, drawSizeY * shreadSize); // Coordinates and size of draw area of source image, then canvas
const imageData = context.getImageData(0, 0, drawSizeX * shreadSize, drawSizeY * shreadSize); // Data of the image on the canvas
for (let y = 0; y < drawSizeY * shreadSize; y++) {
for (let x = 0; x < drawSizeX * shreadSize; x++) {
// For every pixel...
// ... Make it transparent unless it is the "center"
if ((x % shreadSize !== 1) || (y % shreadSize !== 1)) {
const pixelIndex = (y * drawSizeX + x) * 4; // Find the pixel index in an array where every 4 indexes are 1 pixel
imageData.data[pixelIndex + 3] = 0; // Make the pixel transparent on the alpha channel
}
}
}
console.log(`Shreaded pixels for ${pixelX}, ${pixelY}`, imageData);
context.putImageData(imageData, 0, 0);
templateTiles[`${(coords[0] + Math.floor(pixelX / 1000)).toString().padStart(4, '0')},${(coords[1] + Math.floor(pixelY / 1000)).toString().padStart(4, '0')},${(pixelX % 1000).toString().padStart(3, '0')},${(pixelY % 1000).toString().padStart(3, '0')}`] = await canvas.convertToBlob({ type: 'image/png' });
console.log(templateTiles);
pixelX += drawSizeX;
}
pixelY += drawSizeY;
}
console.log('Template Tiles: ', templateTiles);
return templateTiles;
}
/** Creates an image from a blob File
* @param {File} blob - The blob to convert to an Image
* @returns {Image} The image of the blob as an Image
* @since 0.65.4
*/
#loadImageFromBlob(blob) {
return new Promise((resolve, reject) => {
const image = new Image(); // Create a blank image
image.onload = () => resolve(image); // When the blank image loads, populate it with the blob
image.onerror = reject; // Return the error, if any
image.src = URL.createObjectURL(blob);
});
}
/** Generates a {@link Template} class instance from the JSON object template
*
*/
#loadTemplate() {
}
/** Deletes a template from the JSON object.
* Also delete's the corrosponding {@link Template} class instance
*
*/
deleteTemplate() {
}
/** Draws all templates on that tile
*
*/
drawTemplateOnTile() {
}
importJSON() {
}
#parseBlueMarble() {
}
#parseOSU() {
}
/** Sets the template to the image passed in.
* @param {File} file - The file of the template image.
* @since 0.55.8

View file

@ -70,4 +70,32 @@ export function consoleError(...args) {((consoleError) => consoleError(...args))
* @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);}
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
}