mirror of
https://github.com/SwingTheVine/Wplace-BlueMarble.git
synced 2026-05-08 01:09:41 +00:00
counts and colors for 81
This commit is contained in:
parent
05893df35c
commit
c53bfed433
6 changed files with 476 additions and 39 deletions
2
dist/BlueMarble.user.js
vendored
2
dist/BlueMarble.user.js
vendored
File diff suppressed because one or more lines are too long
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "wplace-bluemarble",
|
||||
"version": "0.78.0",
|
||||
"version": "0.81.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wplace-bluemarble",
|
||||
"version": "0.78.0",
|
||||
"version": "0.81.0",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"jsdoc": "^4.0.4",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { uint8ToBase64 } from "./utils";
|
||||
import { uint8ToBase64, colorpalette } from "./utils";
|
||||
|
||||
/** An instance of a template.
|
||||
* Handles all mathematics, manipulation, and analysis regarding a single template.
|
||||
|
|
@ -39,6 +39,25 @@ export default class Template {
|
|||
this.chunked = chunked;
|
||||
this.tileSize = tileSize;
|
||||
this.pixelCount = 0; // Total pixel count in template
|
||||
this.requiredPixelCount = 0; // Total number of non-transparent, non-#deface pixels
|
||||
this.defacePixelCount = 0; // Number of #deface pixels (represents Transparent color in-game)
|
||||
this.colorPalette = {}; // key: "r,g,b" -> { count: number, enabled: boolean }
|
||||
this.tilePrefixes = new Set(); // Set of "xxxx,yyyy" tiles this template touches
|
||||
this.storageKey = null; // Key used inside templatesJSON to persist settings
|
||||
|
||||
// Build allowed color set from site palette (exclude special Transparent entry by name)
|
||||
const allowed = Array.isArray(colorpalette) ? colorpalette : [];
|
||||
this.allowedColorsSet = new Set(
|
||||
allowed
|
||||
.filter(c => (c?.name || '').toLowerCase() !== 'transparent' && Array.isArray(c?.rgb))
|
||||
.map(c => `${c.rgb[0]},${c.rgb[1]},${c.rgb[2]}`)
|
||||
);
|
||||
// Map rgb-> {id, premium}
|
||||
this.rgbToMeta = new Map(
|
||||
allowed
|
||||
.filter(c => Array.isArray(c?.rgb))
|
||||
.map(c => [ `${c.rgb[0]},${c.rgb[1]},${c.rgb[2]}`, { id: c.id, premium: !!c.premium, name: c.name } ])
|
||||
);
|
||||
}
|
||||
|
||||
/** Creates chunks of the template for each tile.
|
||||
|
|
@ -62,6 +81,54 @@ export default class Template {
|
|||
// Store pixel count in instance property for access by template manager and UI components
|
||||
this.pixelCount = totalPixels;
|
||||
|
||||
// ==================== REQUIRED/DEFACE PIXEL COUNTING ====================
|
||||
// Build a 1× scale canvas to inspect original pixels and count required vs deface
|
||||
try {
|
||||
const inspectCanvas = new OffscreenCanvas(imageWidth, imageHeight);
|
||||
const inspectCtx = inspectCanvas.getContext('2d', { willReadFrequently: true });
|
||||
inspectCtx.imageSmoothingEnabled = false;
|
||||
inspectCtx.clearRect(0, 0, imageWidth, imageHeight);
|
||||
inspectCtx.drawImage(bitmap, 0, 0);
|
||||
const inspectData = inspectCtx.getImageData(0, 0, imageWidth, imageHeight).data;
|
||||
|
||||
let required = 0;
|
||||
let deface = 0;
|
||||
const paletteMap = new Map();
|
||||
for (let y = 0; y < imageHeight; y++) {
|
||||
for (let x = 0; x < imageWidth; x++) {
|
||||
const idx = (y * imageWidth + x) * 4;
|
||||
const r = inspectData[idx];
|
||||
const g = inspectData[idx + 1];
|
||||
const b = inspectData[idx + 2];
|
||||
const a = inspectData[idx + 3];
|
||||
if (a === 0) { continue; } // Ignored transparent pixel
|
||||
if (r === 222 && g === 250 && b === 206) { // #deface
|
||||
deface++;
|
||||
continue; // Do not include in required count so progress reflects paintable pixels
|
||||
}
|
||||
const key = `${r},${g},${b}`;
|
||||
if (!this.allowedColorsSet.has(key)) { continue; } // Skip non-palette colors
|
||||
required++;
|
||||
paletteMap.set(key, (paletteMap.get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.requiredPixelCount = required;
|
||||
this.defacePixelCount = deface;
|
||||
|
||||
// Persist palette with all colors enabled by default
|
||||
const paletteObj = {};
|
||||
for (const [key, count] of paletteMap.entries()) {
|
||||
paletteObj[key] = { count, enabled: true };
|
||||
}
|
||||
this.colorPalette = paletteObj;
|
||||
} catch (err) {
|
||||
// Fail-safe: if OffscreenCanvas not available or any error, fall back to width×height
|
||||
this.requiredPixelCount = Math.max(0, this.pixelCount);
|
||||
this.defacePixelCount = 0;
|
||||
console.warn('Failed to compute required/deface counts. Falling back to total pixels.', err);
|
||||
}
|
||||
|
||||
const templateTiles = {}; // Holds the template tiles
|
||||
const templateTilesBuffers = {}; // Holds the buffers of the template tiles
|
||||
|
||||
|
|
@ -146,6 +213,14 @@ export default class Template {
|
|||
}
|
||||
} else if (x % shreadSize !== 1 || y % shreadSize !== 1) { // Otherwise only draw the middle pixel
|
||||
imageData.data[pixelIndex + 3] = 0; // Make the pixel transparent on the alpha channel
|
||||
} else {
|
||||
// Center pixel: keep only if in allowed site palette
|
||||
const r = imageData.data[pixelIndex];
|
||||
const g = imageData.data[pixelIndex + 1];
|
||||
const b = imageData.data[pixelIndex + 2];
|
||||
if (!this.allowedColorsSet.has(`${r},${g},${b}`)) {
|
||||
imageData.data[pixelIndex + 3] = 0; // hide non-palette colors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,6 +239,8 @@ export default class Template {
|
|||
.padStart(3, '0')},${(pixelY % 1000).toString().padStart(3, '0')}`;
|
||||
|
||||
templateTiles[templateTileName] = await createImageBitmap(canvas); // Creates the bitmap
|
||||
// Record tile prefix for fast lookup later
|
||||
this.tilePrefixes.add(templateTileName.split(',').slice(0,2).join(','));
|
||||
|
||||
const canvasBlob = await canvas.convertToBlob();
|
||||
const canvasBuffer = await canvasBlob.arrayBuffer();
|
||||
|
|
|
|||
148
src/main.js
148
src/main.js
|
|
@ -242,6 +242,19 @@ function observeBlack() {
|
|||
*/
|
||||
function buildOverlayMain() {
|
||||
let isMinimized = false; // Overlay state tracker (false = maximized, true = minimized)
|
||||
// Load last saved coordinates (if any)
|
||||
let savedCoords = {};
|
||||
try { savedCoords = JSON.parse(GM_getValue('bmCoords', '{}')) || {}; } catch (_) { savedCoords = {}; }
|
||||
const persistCoords = () => {
|
||||
try {
|
||||
const tx = Number(document.querySelector('#bm-input-tx')?.value || '');
|
||||
const ty = Number(document.querySelector('#bm-input-ty')?.value || '');
|
||||
const px = Number(document.querySelector('#bm-input-px')?.value || '');
|
||||
const py = Number(document.querySelector('#bm-input-py')?.value || '');
|
||||
const data = { tx, ty, px, py };
|
||||
GM.setValue('bmCoords', JSON.stringify(data));
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
overlayMain.addDiv({'id': 'bm-overlay', 'style': 'top: 10px; right: 75px;'})
|
||||
.addDiv({'id': 'bm-contain-header'})
|
||||
|
|
@ -296,7 +309,8 @@ function buildOverlayMain() {
|
|||
'#bm-contain-automation > *:not(#bm-contain-coords)', // Automation section excluding coordinates
|
||||
'#bm-input-file-template', // Template file upload interface
|
||||
'#bm-contain-buttons-action', // Action buttons container
|
||||
`#${instance.outputStatusId}` // Status log textarea for user feedback
|
||||
`#${instance.outputStatusId}`, // Status log textarea for user feedback
|
||||
'#bm-contain-colorfilter' // Color filter UI
|
||||
];
|
||||
|
||||
// Apply visibility changes to all toggleable elements
|
||||
|
|
@ -475,13 +489,54 @@ function buildOverlayMain() {
|
|||
instance.updateInnerHTML('bm-input-ty', coords?.[1] || '');
|
||||
instance.updateInnerHTML('bm-input-px', coords?.[2] || '');
|
||||
instance.updateInnerHTML('bm-input-py', coords?.[3] || '');
|
||||
persistCoords();
|
||||
}
|
||||
}
|
||||
).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()
|
||||
.addInput({'type': 'number', 'id': 'bm-input-tx', 'placeholder': 'Tl X', 'min': 0, 'max': 2047, 'step': 1, 'required': true, 'value': (savedCoords.tx ?? '')}, (instance, input) => {
|
||||
const handler = () => persistCoords();
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
const handler = () => persistCoords();
|
||||
input.addEventListener('input', handler);
|
||||
input.addEventListener('change', handler);
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
// Color filter UI
|
||||
.addDiv({'id': 'bm-contain-colorfilter', 'style': 'max-height: 140px; overflow: auto; border: 1px solid rgba(255,255,255,0.1); padding: 4px; border-radius: 4px; display: none;'})
|
||||
.addDiv({'style': 'display: flex; gap: 6px; margin-bottom: 6px;'})
|
||||
.addButton({'id': 'bm-button-colors-enable-all', 'textContent': 'Enable All'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
const t = templateManager.templatesArray[0];
|
||||
if (!t?.colorPalette) { return; }
|
||||
Object.values(t.colorPalette).forEach(v => v.enabled = true);
|
||||
buildColorFilterList();
|
||||
instance.handleDisplayStatus('Enabled all colors');
|
||||
};
|
||||
}).buildElement()
|
||||
.addButton({'id': 'bm-button-colors-disable-all', 'textContent': 'Disable All'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
const t = templateManager.templatesArray[0];
|
||||
if (!t?.colorPalette) { return; }
|
||||
Object.values(t.colorPalette).forEach(v => v.enabled = false);
|
||||
buildColorFilterList();
|
||||
instance.handleDisplayStatus('Disabled all colors');
|
||||
};
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'id': 'bm-colorfilter-list'}).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'})
|
||||
|
|
@ -541,6 +596,89 @@ function buildOverlayMain() {
|
|||
.buildElement()
|
||||
.buildElement()
|
||||
.buildOverlay(document.body);
|
||||
|
||||
// ------- Helper: Build the color filter list -------
|
||||
window.buildColorFilterList = function buildColorFilterList() {
|
||||
const listContainer = document.querySelector('#bm-colorfilter-list');
|
||||
const t = templateManager.templatesArray?.[0];
|
||||
if (!listContainer || !t?.colorPalette) {
|
||||
if (listContainer) { listContainer.innerHTML = '<small>No template colors to display.</small>'; }
|
||||
return;
|
||||
}
|
||||
|
||||
listContainer.innerHTML = '';
|
||||
const entries = Object.entries(t.colorPalette)
|
||||
.sort((a,b) => b[1].count - a[1].count); // sort by frequency desc
|
||||
|
||||
for (const [rgb, meta] of entries) {
|
||||
const [r,g,b] = rgb.split(',').map(Number);
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.style.display = 'flex';
|
||||
row.style.alignItems = 'center';
|
||||
row.style.gap = '8px';
|
||||
row.style.margin = '4px 0';
|
||||
|
||||
const swatch = document.createElement('div');
|
||||
swatch.style.width = '14px';
|
||||
swatch.style.height = '14px';
|
||||
swatch.style.border = '1px solid rgba(255,255,255,0.5)';
|
||||
swatch.style.background = `rgb(${r},${g},${b})`;
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.style.fontSize = '12px';
|
||||
let labelText = `${meta.count.toLocaleString()}`;
|
||||
try {
|
||||
const tMeta = templateManager.templatesArray?.[0]?.rgbToMeta?.get(rgb);
|
||||
if (tMeta && typeof tMeta.id === 'number') {
|
||||
const displayName = tMeta?.name || `rgb(${r},${g},${b})`;
|
||||
const starLeft = tMeta.premium ? '★ ' : '';
|
||||
labelText = `#${tMeta.id} ${starLeft}${displayName} • ${labelText}`;
|
||||
}
|
||||
} catch (_) {}
|
||||
label.textContent = labelText;
|
||||
|
||||
const toggle = document.createElement('input');
|
||||
toggle.type = 'checkbox';
|
||||
toggle.checked = !!meta.enabled;
|
||||
toggle.addEventListener('change', () => {
|
||||
meta.enabled = toggle.checked;
|
||||
overlayMain.handleDisplayStatus(`${toggle.checked ? 'Enabled' : 'Disabled'} ${rgb}`);
|
||||
try {
|
||||
const t = templateManager.templatesArray?.[0];
|
||||
const key = t?.storageKey;
|
||||
if (t && key && templateManager.templatesJSON?.templates?.[key]) {
|
||||
templateManager.templatesJSON.templates[key].palette = t.colorPalette;
|
||||
// persist immediately
|
||||
GM.setValue('bmTemplates', JSON.stringify(templateManager.templatesJSON));
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
row.appendChild(toggle);
|
||||
row.appendChild(swatch);
|
||||
row.appendChild(label);
|
||||
listContainer.appendChild(row);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for template creation/import completion to (re)build palette list
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event?.data?.bmEvent === 'bm-rebuild-color-list') {
|
||||
try { buildColorFilterList(); } catch (_) {}
|
||||
}
|
||||
});
|
||||
|
||||
// If a template was already loaded from storage, show the color UI and build list
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (templateManager.templatesArray?.length > 0) {
|
||||
const colorUI = document.querySelector('#bm-contain-colorfilter');
|
||||
if (colorUI) { colorUI.style.display = ''; }
|
||||
buildColorFilterList();
|
||||
}
|
||||
} catch (_) {}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function buildOverlayTabTemplate() {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ 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.
|
||||
|
|
@ -143,11 +144,14 @@ 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
|
||||
this.templatesJSON.templates[`${template.sortID} ${template.authorID}`] = {
|
||||
const storageKey = `${template.sortID} ${template.authorID}`;
|
||||
template.storageKey = storageKey;
|
||||
this.templatesJSON.templates[storageKey] = {
|
||||
"name": template.displayName, // Display name of template
|
||||
"coords": coords.join(', '), // The coords of the template
|
||||
"enabled": true,
|
||||
"tiles": templateTilesBuffers // Stores the chunked tile buffers
|
||||
"tiles": templateTilesBuffers, // Stores the chunked tile buffers
|
||||
"palette": template.colorPalette // Persist palette and enabled flags
|
||||
};
|
||||
|
||||
this.templatesArray = []; // Remove this to enable multiple templates (2/2)
|
||||
|
|
@ -159,6 +163,14 @@ export default class TemplateManager {
|
|||
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 */ }
|
||||
|
||||
console.log(Object.keys(this.templatesJSON.templates).length);
|
||||
console.log(this.templatesJSON);
|
||||
console.log(this.templatesArray);
|
||||
|
|
@ -223,6 +235,18 @@ 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 => {
|
||||
|
|
@ -253,31 +277,10 @@ export default class TemplateManager {
|
|||
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 || 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.`);
|
||||
}
|
||||
// We'll compute per-tile painted/wrong/required counts when templates exist for this tile
|
||||
let paintedCount = 0;
|
||||
let wrongCount = 0;
|
||||
let requiredCount = 0;
|
||||
|
||||
const tileBitmap = await createImageBitmap(tileBlob);
|
||||
|
||||
|
|
@ -294,13 +297,161 @@ 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);
|
||||
|
||||
// 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);
|
||||
// Compute stats by sampling template center pixels against tile pixels,
|
||||
// honoring color enable/disable from the active template's palette
|
||||
if (tilePixels) {
|
||||
try {
|
||||
const tempW = template.bitmap.width;
|
||||
const tempH = template.bitmap.height;
|
||||
const tempCanvas = new OffscreenCanvas(tempW, tempH);
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
tempCtx.imageSmoothingEnabled = false;
|
||||
tempCtx.clearRect(0, 0, tempW, tempH);
|
||||
tempCtx.drawImage(template.bitmap, 0, 0);
|
||||
const tImg = tempCtx.getImageData(0, 0, tempW, tempH);
|
||||
const tData = tImg.data;
|
||||
|
||||
const offsetX = Number(template.pixelCoords[0]) * this.drawMult;
|
||||
const offsetY = Number(template.pixelCoords[1]) * this.drawMult;
|
||||
|
||||
for (let y = 0; y < tempH; y++) {
|
||||
for (let x = 0; x < tempW; x++) {
|
||||
// Only evaluate the center pixel of each shread block
|
||||
if ((x % this.drawMult) !== 1 || (y % this.drawMult) !== 1) { continue; }
|
||||
const gx = x + offsetX;
|
||||
const gy = y + offsetY;
|
||||
if (gx < 0 || gy < 0 || gx >= drawSize || gy >= drawSize) { continue; }
|
||||
const tIdx = (y * tempW + x) * 4;
|
||||
const tr = tData[tIdx];
|
||||
const tg = tData[tIdx + 1];
|
||||
const tb = tData[tIdx + 2];
|
||||
const ta = tData[tIdx + 3];
|
||||
// Ignore transparent and semi-transparent (deface checkerboard uses alpha 32)
|
||||
if (ta < 64) { continue; }
|
||||
// Ignore #deface explicitly if it sneaks through with higher alpha
|
||||
if (tr === 222 && tg === 250 && tb === 206) { continue; }
|
||||
// Ignore non-palette colors (match against allowed set when available)
|
||||
try {
|
||||
const activeTemplate = this.templatesArray?.[0];
|
||||
if (activeTemplate?.allowedColorsSet && !activeTemplate.allowedColorsSet.has(`${tr},${tg},${tb}`)) {
|
||||
continue;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
requiredCount++;
|
||||
|
||||
// Strict center-pixel matching. Treat transparent tile pixels as unpainted (not wrong)
|
||||
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];
|
||||
|
||||
if (pa < 64) {
|
||||
// Unpainted -> neither painted nor wrong
|
||||
} else if (pr === tr && pg === tg && pb === tb) {
|
||||
paintedCount++;
|
||||
} else {
|
||||
wrongCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to compute per-tile painted/wrong stats:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the template overlay for visual guidance, honoring color filter
|
||||
try {
|
||||
const activeTemplate = this.templatesArray?.[0];
|
||||
const palette = activeTemplate?.colorPalette || {};
|
||||
const hasDisabled = Object.values(palette).some(v => v?.enabled === false);
|
||||
if (!hasDisabled) {
|
||||
context.drawImage(template.bitmap, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult);
|
||||
} else {
|
||||
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;
|
||||
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 (let y = 0; y < tempH; y++) {
|
||||
for (let x = 0; x < tempW; x++) {
|
||||
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; }
|
||||
const key = `${r},${g},${b}`;
|
||||
// Hide if color is not in allowed palette or explicitly disabled
|
||||
const inSitePalette = activeTemplate?.allowedColorsSet ? activeTemplate.allowedColorsSet.has(key) : true;
|
||||
const enabled = palette?.[key]?.enabled !== false;
|
||||
if (!inSitePalette || !enabled) {
|
||||
data[idx + 3] = 0; // hide disabled color center pixel
|
||||
}
|
||||
}
|
||||
}
|
||||
filterCtx.putImageData(img, 0, 0);
|
||||
context.drawImage(filterCanvas, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult);
|
||||
}
|
||||
} catch (_) {
|
||||
// Fallback to drawing raw bitmap if filtering fails
|
||||
context.drawImage(template.bitmap, Number(template.pixelCoords[0]) * this.drawMult, Number(template.pixelCoords[1]) * this.drawMult);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
const paintedStr = new Intl.NumberFormat().format(aggPainted);
|
||||
const requiredStr = new Intl.NumberFormat().format(totalRequired);
|
||||
const wrongStr = new Intl.NumberFormat().format(aggWrong);
|
||||
|
||||
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' });
|
||||
|
|
@ -349,6 +500,8 @@ 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);
|
||||
|
|
@ -359,6 +512,36 @@ 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 = `${r},${g},${b}`;
|
||||
paletteMap.set(key, (paletteMap.get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to count required pixels for imported tile', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -370,11 +553,39 @@ 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 */ }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
13
src/utils.js
13
src/utils.js
|
|
@ -391,4 +391,15 @@ export const colorpalette = [
|
|||
"name": "Light Stone",
|
||||
"rgb": [205, 197, 158]
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
// Annotate palette entries with ID (index) and premium flag.
|
||||
try {
|
||||
for (let i = 0; i < colorpalette.length; i++) {
|
||||
const c = colorpalette[i];
|
||||
if (c && typeof c === 'object') {
|
||||
c.id = i;
|
||||
c.premium = i >= 32; // Premium colors are indices 32–63 (0-based)
|
||||
}
|
||||
}
|
||||
} catch (_) { /* no-op */ }
|
||||
Loading…
Reference in a new issue