counts and colors for 81

This commit is contained in:
vishnuvardhan33 2025-08-12 14:39:36 +05:30 committed by SwingTheVine
parent 05893df35c
commit c53bfed433
6 changed files with 476 additions and 39 deletions

File diff suppressed because one or more lines are too long

4
package-lock.json generated
View file

@ -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",

View file

@ -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();

View file

@ -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() {

View file

@ -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 */ }
}
}

View file

@ -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 3263 (0-based)
}
}
} catch (_) { /* no-op */ }