Removed color filter (2)

This commit is contained in:
SwingTheVine 2026-02-08 23:08:23 -05:00
parent c40c0589db
commit 083462d0f6
7 changed files with 43 additions and 327 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-498-black?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-501-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.0",
"version": "0.88.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
"version": "0.88.0",
"version": "0.88.3",
"devDependencies": {
"esbuild": "^0.25.0",
"jsdoc": "^4.0.5",

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.88.0",
"version": "0.88.3",
"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.0
// @version 0.88.3
// @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

@ -498,7 +498,7 @@ function buildOverlayMain() {
}
}
).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-tx', 'placeholder': 'Tl X', 'min': 0, 'max': 2047, 'step': 1, 'required': true, 'value': (savedCoords.tx ?? '')}, (instance, input) => {
.addInput({'type': 'number', 'id': 'bm-input-tx', 'placeholder': 'Tl X', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
//if a paste happens on tx, split and format it into other coordinates if possible
input.addEventListener("paste", (event) => {
let splitText = (event.clipboardData || window.clipboardData).getData("text").split(" ").filter(n => n).map(Number).filter(n => !isNaN(n)); //split and filter all Non Numbers
@ -519,17 +519,17 @@ function buildOverlayMain() {
input.addEventListener('input', handler);
input.addEventListener('change', handler);
}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-ty', 'placeholder': 'Tl Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true, 'value': (savedCoords.ty ?? '')}, (instance, input) => {
.addInput({'type': 'number', 'id': 'bm-input-ty', 'placeholder': 'Tl Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
const handler = () => persistCoords();
input.addEventListener('input', handler);
input.addEventListener('change', handler);
}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-px', 'placeholder': 'Px X', 'min': 0, 'max': 2047, 'step': 1, 'required': true, 'value': (savedCoords.px ?? '')}, (instance, input) => {
.addInput({'type': 'number', 'id': 'bm-input-px', 'placeholder': 'Px X', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
const handler = () => persistCoords();
input.addEventListener('input', handler);
input.addEventListener('change', handler);
}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-py', 'placeholder': 'Px Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true, 'value': (savedCoords.py ?? '')}, (instance, input) => {
.addInput({'type': 'number', 'id': 'bm-input-py', 'placeholder': 'Px Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
const handler = () => persistCoords();
input.addEventListener('input', handler);
input.addEventListener('change', handler);

View file

@ -61,7 +61,6 @@ export default class TemplateManager {
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.tileProgress = new Map(); // Tracks per-tile progress stats {painted, required, wrong}
}
/** Retrieves the pixel art canvas.
@ -144,32 +143,17 @@ export default class TemplateManager {
// 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
const storageKey = `${template.sortID} ${template.authorID}`;
template.storageKey = storageKey;
this.templatesJSON.templates[storageKey] = {
this.templatesJSON.templates[`${template.sortID} ${template.authorID}`] = {
"name": template.displayName, // Display name of template
"coords": coords.join(', '), // The coords of the template
"enabled": true,
"tiles": templateTilesBuffers, // Stores the chunked tile buffers
"palette": template.colorPalette // Persist palette and enabled flags
"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
// ==================== PIXEL COUNT DISPLAY SYSTEM ====================
// Display pixel count statistics with internationalized number formatting
// This provides immediate feedback to users about template complexity and size
const pixelCountFormatted = new Intl.NumberFormat().format(template.pixelCount);
this.overlay.handleDisplayStatus(`Template created at ${coords.join(', ')}! Total pixels: ${pixelCountFormatted}`);
// Ensure color filter UI is visible when a template is created
try {
const colorUI = document.querySelector('#bm-contain-colorfilter');
if (colorUI) { colorUI.style.display = ''; }
// Deferred palette list rendering; actual DOM is built in main via helper
window.postMessage({ source: 'blue-marble', bmEvent: 'bm-rebuild-color-list' }, '*');
} catch (_) { /* no-op */ }
this.overlay.handleDisplayStatus(`Template created at ${coords.join(', ')}!`);
console.log(Object.keys(this.templatesJSON.templates).length);
console.log(this.templatesJSON);
@ -235,18 +219,6 @@ export default class TemplateManager {
console.log(templateArray);
// Early exit if none of the active templates touch this tile
const anyTouches = templateArray.some(t => {
if (!t?.chunked) { return false; }
// Fast path via recorded tile prefixes if available
if (t.tilePrefixes && t.tilePrefixes.size > 0) {
return t.tilePrefixes.has(tileCoords);
}
// Fallback: scan chunked keys
return Object.keys(t.chunked).some(k => k.startsWith(tileCoords));
});
if (!anyTouches) { return tileBlob; }
// Retrieves the relavent template tile blobs
const templatesToDraw = templateArray
.map(template => {
@ -277,10 +249,31 @@ export default class TemplateManager {
const templateCount = templatesToDraw?.length || 0; // Number of templates to draw on this tile
console.log(`templateCount = ${templateCount}`);
// We'll compute per-tile painted/wrong/required counts when templates exist for this tile
let paintedCount = 0;
let wrongCount = 0;
let requiredCount = 0;
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 || 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 = new Intl.NumberFormat().format(totalPixels);
// Display status information about the templates being rendered
this.overlay.handleDisplayStatus(
`Displaying ${templateCount} template${templateCount == 1 ? '' : 's'}.\nTotal pixels: ${pixelCountFormatted}`
);
} else {
this.overlay.handleDisplayStatus(`Displaying ${templateCount} templates.`);
}
const tileBitmap = await createImageBitmap(tileBlob);
@ -297,234 +290,16 @@ export default class TemplateManager {
context.clearRect(0, 0, drawSize, drawSize); // Draws transparent background
context.drawImage(tileBitmap, 0, 0, drawSize, drawSize);
// Grab a snapshot of the tile pixels BEFORE we draw any template overlays
let tilePixels = null;
try {
tilePixels = context.getImageData(0, 0, drawSize, drawSize).data;
} catch (_) {
// If reading fails for any reason, we will skip stats
}
// For each template in this tile, draw them.
for (const template of templatesToDraw) {
console.log(`Template:`);
console.log(template);
// Compute stats by sampling template center pixels against tile pixels,
// honoring color enable/disable from the active template's palette
if (tilePixels) {
try {
const tempWidth = template.bitmap.width;
const tempHeight = template.bitmap.height;
const tempCanvas = new OffscreenCanvas(tempWidth, tempHeight);
const tempContext = tempCanvas.getContext('2d', { willReadFrequently: true });
tempContext.imageSmoothingEnabled = false;
tempContext.clearRect(0, 0, tempWidth, tempHeight);
tempContext.drawImage(template.bitmap, 0, 0);
const tImg = tempContext.getImageData(0, 0, tempWidth, tempHeight);
const tData = tImg.data; // Tile Data, Template Data, or Temp Data????
// Draws the each template on the tile based on it's relative position
context.drawImage(template.bitmap, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult);
const offsetX = Number(template.pixelCoords[0]) * this.drawMult;
const offsetY = Number(template.pixelCoords[1]) * this.drawMult;
// Loops over all pixels in the template
// Assigns each pixel a color (if center pixel)
for (let y = 0; y < tempHeight; y++) {
for (let x = 0; x < tempWidth; x++) {
// Purpose: Count which pixels are painted correctly???
// Only evaluate the center pixel of each shread block
// Skip if not the center pixel of the shread block
if ((x % this.drawMult) !== 1 || (y % this.drawMult) !== 1) { continue; }
const gx = x + offsetX;
const gy = y + offsetY;
// IF the pixel is out of bounds of the template, OR if the pixel is outside of the tile, then skip the pixel
if (gx < 0 || gy < 0 || gx >= drawSize || gy >= drawSize) { continue; }
const templatePixelCenter = (y * tempWidth + x) * 4; // Shread block center pixel
const templatePixelCenterRed = tData[templatePixelCenter]; // Shread block's center pixel's RED value
const templatePixelCenterGreen = tData[templatePixelCenter + 1]; // Shread block's center pixel's GREEN value
const templatePixelCenterBlue = tData[templatePixelCenter + 2]; // Shread block's center pixel's BLUE value
const templatePixelCenterAlpha = tData[templatePixelCenter + 3]; // Shread block's center pixel's ALPHA value
// Possibly needs to be removed
// Handle template transparent pixel (alpha < 64): wrong if board has any site palette color here
// If the alpha of the center pixel is less than 64...
if (templatePixelCenterAlpha < 64) {
try {
const activeTemplate = this.templatesArray?.[0];
const tileIdx = (gy * drawSize + gx) * 4;
const pr = tilePixels[tileIdx];
const pg = tilePixels[tileIdx + 1];
const pb = tilePixels[tileIdx + 2];
const pa = tilePixels[tileIdx + 3];
const key = activeTemplate.allowedColorsSet.has(`${pr},${pg},${pb}`) ? `${pr},${pg},${pb}` : 'other';
const isSiteColor = activeTemplate?.allowedColorsSet ? activeTemplate.allowedColorsSet.has(key) : false;
// IF the alpha of the center pixel that is placed on the canvas is greater than or equal to 64, AND the pixel is a Wplace palette color, then it is incorrect.
if (pa >= 64 && isSiteColor) {
wrongCount++;
}
} catch (ignored) {}
continue; // Continue to the next pixel
}
// Treat #deface as Transparent palette color (required and paintable)
// Ignore non-palette colors (match against allowed set when available) for counting required template pixels
// try {
// const activeTemplate = this.templatesArray?.[0]; // Get the first template
// // IF the stored palette data exists, AND the pixel is not in the allowed palette
// if (activeTemplate?.allowedColorsSet && !activeTemplate.allowedColorsSet.has(`${templatePixelCenterRed},${templatePixelCenterGreen},${templatePixelCenterBlue}`)) {
// continue; // Skip this pixel if it is not in the allowed palette
// }
// } catch (ignored) {}
requiredCount++;
// Strict center-pixel matching. Treat transparent tile pixels as unpainted (not wrong)
const realPixelCenter = (gy * drawSize + gx) * 4;
const realPixelRed = tilePixels[realPixelCenter];
const realPixelCenterGreen = tilePixels[realPixelCenter + 1];
const realPixelCenterBlue = tilePixels[realPixelCenter + 2];
const realPixelCenterAlpha = tilePixels[realPixelCenter + 3];
// IF the alpha of the pixel is less than 64...
if (realPixelCenterAlpha < 64) {
// Unpainted -> neither painted nor wrong
// ELSE IF the pixel matches the template center pixel color
} else if (realPixelRed === templatePixelCenterRed && realPixelCenterGreen === templatePixelCenterGreen && realPixelCenterBlue === templatePixelCenterBlue) {
paintedCount++; // ...the pixel is painted correctly
} else {
wrongCount++; // ...the pixel is NOT painted correctly
}
}
}
} catch (exception) {
console.warn('Failed to compute per-tile painted/wrong stats:', exception);
}
}
// Draw the template overlay for visual guidance, honoring color filter
try {
const activeTemplate = this.templatesArray?.[0]; // Get the first template
const palette = activeTemplate?.colorPalette || {}; // Obtain the color palette of the template
const hasDisabled = Object.values(palette).some(v => v?.enabled === false); // Check if any color is disabled
// If none of the template colors are disabled, then draw the image normally
if (!hasDisabled) {
context.drawImage(template.bitmap, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult);
} else {
// ELSE we need to apply the color filter
console.log('Applying color filter...');
const tempW = template.bitmap.width;
const tempH = template.bitmap.height;
const filterCanvas = new OffscreenCanvas(tempW, tempH);
const filterCtx = filterCanvas.getContext('2d', { willReadFrequently: true });
filterCtx.imageSmoothingEnabled = false; // Nearest neighbor
filterCtx.clearRect(0, 0, tempW, tempH);
filterCtx.drawImage(template.bitmap, 0, 0);
const img = filterCtx.getImageData(0, 0, tempW, tempH);
const data = img.data;
// For every pixel...
for (let y = 0; y < tempH; y++) {
for (let x = 0; x < tempW; x++) {
// If this pixel is NOT the center pixel, then skip the pixel
if ((x % this.drawMult) !== 1 || (y % this.drawMult) !== 1) { continue; }
const idx = (y * tempW + x) * 4;
const r = data[idx];
const g = data[idx + 1];
const b = data[idx + 2];
const a = data[idx + 3];
if (a < 1) { continue; }
let key = activeTemplate.allowedColorsSet.has(`${r},${g},${b}`) ? `${r},${g},${b}` : 'other';
// Hide if color is not in allowed palette or explicitly disabled
const inWplacePalette = activeTemplate?.allowedColorsSet ? activeTemplate.allowedColorsSet.has(key) : true;
// if (inWplacePalette) {
// key = 'other'; // Map all non-palette colors to "other"
// console.log('Added color to other');
// }
const isPaletteColorEnabled = palette?.[key]?.enabled !== false;
if (!inWplacePalette || !isPaletteColorEnabled) {
data[idx + 3] = 0; // hide disabled color center pixel
}
}
}
// Draws the template with somes colors disabled
filterCtx.putImageData(img, 0, 0);
context.drawImage(filterCanvas, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult);
}
} catch (exception) {
// If filtering fails, we can log the error or handle it accordingly
console.warn('Failed to apply color filter:', exception);
// Fallback to drawing raw bitmap if filtering fails
context.drawImage(template.bitmap, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult);
}
return await canvas.convertToBlob({ type: 'image/png' });
}
// Save per-tile stats and compute global aggregates across all processed tiles
if (templateCount > 0) {
const tileKey = tileCoords; // already padded string "xxxx,yyyy"
this.tileProgress.set(tileKey, {
painted: paintedCount,
required: requiredCount,
wrong: wrongCount,
});
// Aggregate painted/wrong across tiles we've processed
let aggPainted = 0;
let aggRequiredTiles = 0;
let aggWrong = 0;
for (const stats of this.tileProgress.values()) {
aggPainted += stats.painted || 0;
aggRequiredTiles += stats.required || 0;
aggWrong += stats.wrong || 0;
}
// Determine total required across all templates
// Prefer precomputed per-template required counts; fall back to sum of processed tiles
const totalRequiredTemplates = this.templatesArray.reduce((sum, t) =>
sum + (t.requiredPixelCount || t.pixelCount || 0), 0);
const totalRequired = totalRequiredTemplates > 0 ? totalRequiredTemplates : aggRequiredTiles;
// Turns numbers into formatted number strings. E.g., 1234 -> 1,234 OR 1.234 based on location of user
const paintedStr = new Intl.NumberFormat().format(aggPainted);
const requiredStr = new Intl.NumberFormat().format(totalRequired);
const wrongStr = new Intl.NumberFormat().format(totalRequired - aggPainted); // Used to be aggWrong, but that is bugged
this.overlay.handleDisplayStatus(
`Displaying ${templateCount} template${templateCount == 1 ? '' : 's'}.\nPainted ${paintedStr} / ${requiredStr} • Wrong ${wrongStr}`
);
} else {
this.overlay.handleDisplayStatus(`Displaying ${templateCount} templates.`);
}
return await canvas.convertToBlob({ type: 'image/png' });
}
/** Imports the JSON object, and appends it to any JSON object already loaded
@ -570,8 +345,6 @@ export default class TemplateManager {
//const coords = templateValue?.coords?.split(',').map(Number); // "1,2,3,4" -> [1, 2, 3, 4]
const tilesbase64 = templateValue.tiles;
const templateTiles = {}; // Stores the template bitmap tiles for each tile.
let requiredPixelCount = 0; // Global required pixel count for this imported template
const paletteMap = new Map(); // Accumulates color counts across tiles (center pixels only)
for (const tile in tilesbase64) {
console.log(tile);
@ -582,36 +355,6 @@ export default class TemplateManager {
const templateBlob = new Blob([templateUint8Array], { type: "image/png" }); // Uint8Array -> Blob
const templateBitmap = await createImageBitmap(templateBlob) // Blob -> Bitmap
templateTiles[tile] = templateBitmap;
// Count required pixels in this bitmap (center pixels with alpha >= 64 and not #deface)
try {
const w = templateBitmap.width;
const h = templateBitmap.height;
const c = new OffscreenCanvas(w, h);
const cx = c.getContext('2d', { willReadFrequently: true });
cx.imageSmoothingEnabled = false;
cx.clearRect(0, 0, w, h);
cx.drawImage(templateBitmap, 0, 0);
const data = cx.getImageData(0, 0, w, h).data;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
// Only count center pixels of 3x blocks
if ((x % this.drawMult) !== 1 || (y % this.drawMult) !== 1) { continue; }
const idx = (y * w + x) * 4;
const r = data[idx];
const g = data[idx + 1];
const b = data[idx + 2];
const a = data[idx + 3];
if (a < 64) { continue; }
if (r === 222 && g === 250 && b === 206) { continue; }
requiredPixelCount++;
const key = activeTemplate.allowedColorsSet.has(`${r},${g},${b}`) ? `${r},${g},${b}` : 'other';
paletteMap.set(key, (paletteMap.get(key) || 0) + 1);
}
}
} catch (e) {
console.warn('Failed to count required pixels for imported tile', e);
}
}
}
@ -623,39 +366,12 @@ export default class TemplateManager {
//coords: coords
});
template.chunked = templateTiles;
template.requiredPixelCount = requiredPixelCount;
// Construct colorPalette from paletteMap
const paletteObj = {};
for (const [key, count] of paletteMap.entries()) { paletteObj[key] = { count, enabled: true }; }
template.colorPalette = paletteObj;
// Populate tilePrefixes for fast-scoping
try { Object.keys(templateTiles).forEach(k => { template.tilePrefixes?.add(k.split(',').slice(0,2).join(',')); }); } catch (_) {}
// Merge persisted palette (enabled/disabled) if present
try {
const persisted = templates?.[templateKey]?.palette;
if (persisted) {
for (const [rgb, meta] of Object.entries(persisted)) {
if (!template.colorPalette[rgb]) {
template.colorPalette[rgb] = { count: meta?.count || 0, enabled: !!meta?.enabled };
} else {
template.colorPalette[rgb].enabled = !!meta?.enabled;
}
}
}
} catch (_) {}
// Store storageKey for later writes
template.storageKey = templateKey;
this.templatesArray.push(template);
console.log(this.templatesArray);
console.log(`^^^ This ^^^`);
}
}
// After importing templates from storage, reveal color UI and request palette list build
try {
const colorUI = document.querySelector('#bm-contain-colorfilter');
if (colorUI) { colorUI.style.display = ''; }
window.postMessage({ source: 'blue-marble', bmEvent: 'bm-rebuild-color-list' }, '*');
} catch (_) { /* no-op */ }
}
}