Reduced average total pixel calculation time from 6.5s to 5ms

This commit is contained in:
SwingTheVine 2026-02-13 21:28:32 -05:00
parent cbb98e2031
commit bbf38d569a
8 changed files with 78 additions and 120 deletions

File diff suppressed because one or more lines are too long

View file

@ -51,7 +51,7 @@
<a href="https://discord.gg/tpeBPy46hf" target="_blank" rel="noopener noreferrer"><img alt="Contact Me" src="https://img.shields.io/badge/Contact_Me-gray?style=flat&logo=Discord&logoColor=white&logoSize=auto&labelColor=cornflowerblue"></a>
<a href="https://bluemarble.lol/" target="_blank" rel="noopener noreferrer"><img alt="Blue Marble Website" src="https://img.shields.io/badge/Blue_Marble_Website-crqch-blue?style=flat&logo=globe&logoColor=white"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="WakaTime" src="https://img.shields.io/badge/Coding_Time-111hrs_12mins-blue?style=flat&logo=wakatime&logoColor=black&logoSize=auto&labelColor=white"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-523-black?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-531-black?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Lines of Code" src="https://img.shields.io/badge/Lines_Of_Code-498-blue?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Comments" src="https://img.shields.io/badge/Lines_Of_Comments-498-blue?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Compression" src="https://img.shields.io/badge/Compression-70.19%25-blue"></a>

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "wplace-bluemarble",
"version": "0.88.25",
"version": "0.88.33",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
"version": "0.88.25",
"version": "0.88.33",
"devDependencies": {
"esbuild": "^0.25.0",
"jsdoc": "^4.0.5",

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.88.25",
"version": "0.88.33",
"type": "module",
"homepage": "https://bluemarble.lol/",
"repository": {

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.88.25
// @version 0.88.33
// @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

@ -44,11 +44,10 @@ export default class Template {
/** 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 {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 {Object} Collection of template bitmaps & buffers organized by tile coordinates
* @since 0.65.4
*/
async createTemplateTiles(tileSize, paletteBM, paletteTolerance) {
async createTemplateTiles(tileSize, paletteBM) {
console.log('Template coordinates:', this.coords);
const shreadSize = 3; // Scale image factor for pixel art enhancement (must be odd)
@ -57,14 +56,6 @@ export default class Template {
const imageHeight = bitmap.height;
this.tileSize = tileSize; // Tile size predetermined by the templateManager
// Calculate total pixel count using standard width × height formula
// TODO: Use non-transparent pixels instead of basic width times height
const totalPixels = imageWidth * imageHeight;
console.log(`Template pixel analysis - Dimensions: ${imageWidth}×${imageHeight} = ${totalPixels.toLocaleString()} pixels`);
// Store pixel count in instance property for access by template manager and UI components
this.pixelCount = totalPixels;
const templateTiles = {}; // Holds the template tiles
const templateTilesBuffers = {}; // Holds the buffers of the template tiles
@ -80,9 +71,24 @@ export default class Template {
context.drawImage(bitmap, 0, 0); // Draws the template to the canvas
let timer = Date.now();
this.#calculateTotalPixelsFromTemplateData(context.getImageData(0, 0, imageWidth, imageHeight), paletteBM, paletteTolerance); // Calculates total pixels from the template buffer retrieved from the canvas context image data
const totalPixelMap = this.#calculateTotalPixelsFromTemplateData(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
console.log(totalPixelMap);
// 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 = totalPixels; // 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
@ -159,33 +165,6 @@ export default class Template {
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
// TODO: Make Erased pixels calculated when showing the template, not generating it for the first time.
// For every pixel...
// for (let y = 0; y < canvasHeight; y++) {
// for (let x = 0; x < canvasWidth; x++) {
// const pixelIndex = (y * canvasWidth + x) * 4; // Find the pixel index in an array where every 4 indexes are 1 pixel
// // If the pixel is the color #deface, draw a translucent gray checkerboard pattern
// if (
// imageData.data[pixelIndex] === 222 &&
// imageData.data[pixelIndex + 1] === 250 &&
// imageData.data[pixelIndex + 2] === 206
// ) {
// if ((x + y) % 2 === 0) { // Formula for checkerboard pattern
// imageData.data[pixelIndex] = 0;
// imageData.data[pixelIndex + 1] = 0;
// imageData.data[pixelIndex + 2] = 0;
// imageData.data[pixelIndex + 3] = 32; // Translucent black
// } else { // Transparent negative space
// imageData.data[pixelIndex + 3] = 0;
// }
// } else if (x % shreadSize !== 1 || y % shreadSize !== 1) { // Otherwise make all non-middle pixels transparent
// imageData.data[pixelIndex + 3] = 0; // Make the pixel transparent on the alpha channel
// }
// }
// }
console.log(`Shreaded pixels for ${pixelX}, ${pixelY}`, imageData);
@ -225,22 +204,31 @@ export default class Template {
* @param {ImageData} imageData - The pre-shreaded template "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
*/
async #calculateTotalPixelsFromTemplateData(imageData, paletteBM, paletteTolerance) {
#calculateTotalPixelsFromTemplateData(imageData, paletteBM) {
const buffer32Arr = new Uint32Array(imageData.data.buffer); // RGB values as a Uint32Array. Each index represents 1 pixel.
const { palette: 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
paletteBM.palette.forEach(color => _colorpalette.set(color.id, 0));
//paletteBM.palette.forEach(color => _colorpalette.set(color.id, { ...color, amount: 0 }));
palette.forEach(color => _colorpalette.set(color.id, 0));
// For every pixel...
for (let pixelIndex = 0; pixelIndex < buffer32Arr.length; pixelIndex++) {
// Finds the best matching
const bestColorID = this.#findClosestPixelColorID(buffer32Arr[pixelIndex], paletteBM, paletteTolerance);
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;
}
// Adds one to the "amount" value for that pixel in the temporary color palette Map
_colorpalette.set(bestColorID, _colorpalette.get(bestColorID) + 1);
@ -248,58 +236,6 @@ export default class Template {
}
console.log(_colorpalette);
}
/** Takes a 32-bit integer of an RGB value and finds the closest palette color.
* This uses squared Euclidean distance calculations to find the closest color in 3D space.
* @param {Number} pixelColor32 - Pixel to find the color of
* @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 {Number} The ID value of the color that matches.
* @since 0.88.10
*/
#findClosestPixelColorID(pixelColor32, paletteBM, paletteTolerance) {
let bestIndex = Infinity; // Best matching index palette color
let bestDistance = Infinity; // The distance to the best matching index palette color
const { palette: palette, RGB: _, R: paletteR, G: paletteG, B: paletteB } = paletteBM; // Gets the full color palette as Array<Object> as well as each R, G, and B palette as a Uint32Array
const pixelR = (pixelColor32 >> 16) & 0xFF; // Red value for the pixel
const pixelG = (pixelColor32 >> 8) & 0xFF; // Green value for the pixel
const pixelB = pixelColor32 & 0xFF; // Blue value for the pixel
// If the pixel we want to find the palette color of is transparent, then return the transparent index early
if ((pixelColor32 >>> 24) == 0) {return 0;}
// For every palette color...
for (let paletteColorIndex = 0; paletteColorIndex < palette.length; paletteColorIndex++) {
// ...find how close the pixel is in 3D space to each palette color, then return the closest palette color.
// Skip all colors in the pallete where the color ID is 0 (Transparent color) or less than 0 (Blue Marble custom color)
if (palette[paletteColorIndex].id <= 0) {continue;}
// The difference in RGB values between the pixel color and the palette color for each of the 3 channels
const deltaR = paletteR[paletteColorIndex] - pixelR;
const deltaG = paletteG[paletteColorIndex] - pixelG;
const deltaB = paletteB[paletteColorIndex] - pixelB;
// If the palette color is outside of the tolerance, skip this color
if ((Math.abs(deltaR) + Math.abs(deltaG) + Math.abs(deltaB)) > paletteTolerance) {continue;}
// This is is the Manhattan distance. We don't need to do any of the calculations below if this exceeds the tolerance.
// The tolerance check here is the sum of the difference across the RGB channels.
// E.g. "123,45,6" minus "123,44,5" is 2, which is within tolerance. "123,45,6" minus "23,45,6" is 100, which is outside tolerance.
// Squared Euclidean distance in space between palette color and pixel color
const distance = (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB);
// If this palette color is the closest color YET, then update the "best" variables
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = paletteColorIndex;
}
}
// Returns the ID of the best matching color in the palette, or returns the color ID for "Other" (which is -2)
return (bestIndex == Infinity) ? -2 : palette[bestIndex].id;
return _colorpalette;
}
}

View file

@ -1,5 +1,5 @@
import Template from "./Template";
import { base64ToUint8, colorpaletteForBlueMarble, consoleLog, numberToEncoded } from "./utils";
import { base64ToUint8, colorpaletteForBlueMarble, numberToEncoded } from "./utils";
/** Manages the template system.
* This class handles all external requests for template modification, creation, and analysis.
@ -50,8 +50,8 @@ export default class TemplateManager {
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.paletteBM = colorpaletteForBlueMarble(); // Retrieves the color palette BM will use as an Object containing multiple Uint32Arrays
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.canvasTemplate = null; // Our canvas
@ -140,7 +140,7 @@ export default class TemplateManager {
coords: coords
});
//template.chunked = await template.createTemplateTiles(this.tileSize); // Chunks the tiles
const { templateTiles, templateTilesBuffers } = await template.createTemplateTiles(this.tileSize, this.paletteBM, this.paletteTolerance); // Chunks the tiles
const { templateTiles, templateTilesBuffers } = await template.createTemplateTiles(this.tileSize, this.paletteBM); // Chunks the tiles
template.chunked = templateTiles; // Stores the chunked tile bitmaps
// Appends a child into the templates object

View file

@ -149,31 +149,53 @@ export function selectAllCoordinateInputs(document) {
* Wplace palette colors have not been modified.
* @since 0.88.6
*/
export function colorpaletteForBlueMarble() {
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 paletteRGB32 = new Uint32Array(colorpaletteBM.length); // Uint32Array palette of all 3 channels for each color
const paletteR32 = new Uint32Array(colorpaletteBM.length); // Uint32Array palette of just red channel for each color
const paletteG32 = new Uint32Array(colorpaletteBM.length); // Uint32Array palette of just green channel for each color
const paletteB32 = new Uint32Array(colorpaletteBM.length); // Uint32Array palette of just blue channel for each color
const lookupTable = new Map();
// For each color...
for (let color = 0; color < colorpaletteBM.length; color++) {
// 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
const [red, green, blue] = colorpaletteBM[color].rgb; // Retrieves the RGB values of the color
// Target RGB values. These are exactly correct.
const targetRed = color.rgb[0];
const targetGreen = color.rgb[1];
const targetBlue = color.rgb[2];
paletteRGB32[color] = (red) | (green << 8) | (blue << 16); // Takes 3 ints of RGB of color and puts in 32 bits
paletteR32[color] = red; // Red channel of color
paletteG32[color] = green; // Green channel of color
paletteB32[color] = blue; // Blue channel of color
// 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, RGB: paletteRGB32, R: paletteR32, G: paletteG32, B: paletteB32}
return {palette: colorpaletteBM, LUT: lookupTable}
}
/** The color palette used by wplace.live