Wplace-BlueMarble/src/templateManager.js
Nicholas 82bf8a3c3e Add pixel counting and minimize/maximize overlay features
Introduces a comprehensive pixel counting system to the Template class and integrates pixel statistics into the template creation and rendering process. Adds minimize/maximize functionality to the overlay UI, including responsive element visibility, fixed minimized dimensions, and improved user feedback. Updates documentation and comments to reflect new features and version history.
2025-08-04 22:08:12 -03:00

345 lines
14 KiB
JavaScript

import Template from "./Template";
import { numberToEncoded } from "./utils";
/** Manages the comprehensive template system with integrated pixel counting and statistics.
*
* This class handles all external requests for template modification, creation, and statistical analysis.
* It serves as the central coordinator between template instances and the user interface, providing
* real-time feedback on template statistics including pixel counts and rendering status.
*
* ENHANCED FEATURES (v1.1.0):
* - Real-time pixel counting and statistics display
* - Intelligent template filtering based on active tiles
* - Internationalized number formatting for large pixel counts
* - Comprehensive status reporting with detailed template information
* - Enhanced user feedback during template creation and rendering
*
* PIXEL COUNTING SYSTEM:
* The template manager integrates with the Template class pixel counting system to provide:
* - Individual template pixel counts during creation
* - Aggregate pixel counts for multiple templates during rendering
* - Smart filtering to count only actively displayed templates
* - Formatted display of pixel statistics in user interface
*
* STATISTICAL INTEGRATION POINTS:
* 1. Template Creation: Displays pixel count when new templates are processed
* 2. Template Rendering: Shows aggregate pixel count for templates being displayed
* 3. Tile Filtering: Counts pixels only for templates active in current viewport
* 4. User Interface: Provides formatted statistics for status messages
*
* @since 0.55.8
* @version 1.1.0 - Added comprehensive pixel counting system and enhanced statistical reporting
* @example
* // JSON structure for a template
* {
* "whoami": "BlueMarble",
* "scriptVersion": "1.13.0",
* "schemaVersion": "2.1.0",
* "templates": {
* "0 $Z": {
* "name": "My Template",
* "enabled": true,
* "tiles": {
* "1231,0047,183,593": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA",
* "1231,0048,183,000": "data:image/png;AAAFCAYAAACNbyblAAAAHElEQVQI12P4"
* }
* },
* "1 $Z": {
* "name": "My Template",
* "URL": "https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/dist/assets/Favicon.png",
* "URLType": "template",
* "enabled": false,
* "tiles": {
* "375,1846,276,188": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA",
* "376,1846,000,188": "data:image/png;AAAFCAYAAACNbyblAAAAHElEQVQI12P4"
* }
* }
* }
* }
*/
export default class TemplateManager {
/** The constructor for the {@link TemplateManager} class.
* @since 0.55.8
*/
constructor(name, version, overlay) {
// Meta
this.name = name; // Name of userscript
this.version = version; // Version of userscript
this.overlay = overlay; // The main instance of the Overlay class
this.templatesVersion = '1.0.0'; // Version of JSON schema
this.userID = null; // The ID of the current user
this.encodingBase = '!#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'; // Characters to use for encoding/decoding
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
// Template
this.canvasTemplate = null; // Our canvas
this.canvasTemplateZoomed = null; // The template when zoomed out
this.canvasTemplateID = 'bm-canvas'; // Our canvas ID
this.canvasMainID = 'div#map canvas.maplibregl-canvas'; // The selector for the main canvas
this.template = null; // The template image.
this.templateState = ''; // The state of the template ('blob', 'proccessing', 'template', etc.)
this.templatesArray = []; // All Template instnaces currently loaded (Template)
this.templatesJSON = null; // All templates currently loaded (JSON)
}
/** Retrieves the pixel art canvas.
* If the canvas has been updated/replaced, it retrieves the new one.
* @param {string} selector - The CSS selector to use to find the canvas.
* @returns {HTMLCanvasElement|null} The canvas as an HTML Canvas Element, or null if the canvas does not exist
* @since 0.58.3
* @deprecated Not in use since 0.63.25
*/
/* @__PURE__ */getCanvas() {
// If the stored canvas is "fresh", return the stored canvas
if (document.body.contains(this.canvasTemplate)) {return this.canvasTemplate;}
// Else, the stored canvas is "stale", get the canvas again
// Attempt to find and destroy the "stale" canvas
document.getElementById(this.canvasTemplateID)?.remove();
const canvasMain = document.querySelector(this.canvasMainID);
const canvasTemplateNew = document.createElement('canvas');
canvasTemplateNew.id = this.canvasTemplateID;
canvasTemplateNew.className = 'maplibregl-canvas';
canvasTemplateNew.style.position = 'absolute';
canvasTemplateNew.style.top = '0';
canvasTemplateNew.style.left = '0';
canvasTemplateNew.style.height = `${canvasMain?.clientHeight * (window.devicePixelRatio || 1)}px`;
canvasTemplateNew.style.width = `${canvasMain?.clientWidth * (window.devicePixelRatio || 1)}px`;
canvasTemplateNew.height = canvasMain?.clientHeight * (window.devicePixelRatio || 1);
canvasTemplateNew.width = canvasMain?.clientWidth * (window.devicePixelRatio || 1);
canvasTemplateNew.style.zIndex = '8999';
canvasTemplateNew.style.pointerEvents = 'none';
canvasMain?.parentElement?.appendChild(canvasTemplateNew); // Append the newCanvas as a child of the parent of the main canvas
this.canvasTemplate = canvasTemplateNew; // Store the new canvas
window.addEventListener('move', this.onMove);
window.addEventListener('zoom', this.onZoom);
window.addEventListener('resize', this.onResize);
return this.canvasTemplate; // Return the new canvas
}
/** Creates the JSON object to store templates in
* @returns {{ whoami: string, scriptVersion: string, schemaVersion: string, templates: Object }} The JSON object
* @since 0.65.4
*/
async createJSON() {
return {
"whoami": this.name.replace(' ', ''), // Name of userscript without spaces
"scriptVersion": this.version, // Version of userscript
"schemaVersion": this.templatesVersion, // Version of JSON schema
"templates": {} // The templates
};
}
/** Creates the template from the inputed file blob
* @param {File} blob - The file blob to create a template from
* @param {string} name - The display name of the template
* @param {Array<number, number, number, number>} coords - The coordinates of the top left corner of the template
* @since 0.65.77
*/
async createTemplate(blob, name, coords) {
// Creates the JSON object if it does not already exist
if (!this.templatesJSON) {this.templatesJSON = await this.createJSON(); console.log(`Creating JSON...`);}
this.overlay.handleDisplayStatus(`Creating template at ${coords.join(', ')}...`);
// Creates a new template instance
const template = new Template({
displayName: name,
sortID: 0, // Object.keys(this.templatesJSON.templates).length || 0, // Uncomment this to enable multiple templates (1/2)
authorID: numberToEncoded(this.userID || 0, this.encodingBase),
file: blob,
coords: coords
});
template.chunked = await template.createTemplateTiles(this.tileSize); // Chunks the tiles
// 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}`] = {
"name": template.displayName, // Display name of template
"enabled": true,
"tiles": template.chunked
};
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}`);
console.log(Object.keys(this.templatesJSON.templates).length);
console.log(this.templatesJSON);
console.log(this.templatesArray);
}
/** Generates a {@link Template} class instance from the JSON object template
*/
#loadTemplate() {
}
/** Deletes a template from the JSON object.
* Also delete's the corrosponding {@link Template} class instance
*/
deleteTemplate() {
}
/** Disables the template from view
*/
async disableTemplate() {
// Creates the JSON object if it does not already exist
if (!this.templatesJSON) {this.templatesJSON = await this.createJSON(); console.log(`Creating JSON...`);}
}
/** Draws all templates on the specified tile with intelligent pixel count reporting.
*
* This method handles the rendering of template overlays on individual tiles and provides
* comprehensive statistics about the templates being displayed. It integrates with the
* pixel counting system to give users real-time feedback about template complexity.
*
* PIXEL COUNTING INTEGRATION:
* The method implements intelligent pixel counting that:
* - Identifies templates that have content in the current tile
* - Sums pixel counts only for templates actually being rendered
* - Formats large numbers with locale-appropriate separators
* - Provides detailed status messages with template and pixel statistics
*
* PERFORMANCE OPTIMIZATIONS:
* - Filters templates by tile coordinates before processing
* - Counts pixels only for active templates to avoid unnecessary calculations
* - Uses efficient array operations for template filtering and aggregation
* - Caches formatted numbers to avoid repeated formatting operations
*
* USER EXPERIENCE ENHANCEMENTS:
* - Shows both template count and total pixel count in status messages
* - Uses internationalized number formatting for better readability
* - Provides immediate feedback when templates are being displayed
* - Handles singular/plural forms correctly for template count
*
* @param {File} tileBlob - The pixels that are placed on a tile
* @param {[number, number]} tileCoords - The tile coordinates [x, y]
* @since 0.65.77
* @version 1.1.0 - Added intelligent pixel counting and enhanced status reporting
*/
async drawTemplateOnTile(tileBlob, tileCoords) {
const drawSize = this.tileSize * this.drawMult; // Calculate draw multiplier for scaling
// Format tile coordinates with proper padding for consistent lookup
tileCoords = tileCoords[0].toString().padStart(4, '0') + ',' + tileCoords[1].toString().padStart(4, '0');
console.log(`Searching for templates in tile: "${tileCoords}"`);
const templateArray = this.templatesArray; // Stores a copy for sorting
// Sorts the array of Template class instances. 0 = first = lowest draw priority
templateArray.sort((a, b) => {
return a.sortID - b.sortID;
});
console.log(templateArray);
// Retrieves the relavent template tile blobs
const templateBlobs = templateArray
.map(template => {
const matchingTiles = Object.keys(template.chunked).filter(tile =>
tile.startsWith(tileCoords)
);
if (matchingTiles.length === 0) {return null;} // Return nothing when nothing is found
// Retrieves the blobs of the templates for this tile
const matchingTileBlobs = matchingTiles.map(tile => template.chunked[tile]);
return matchingTileBlobs?.[0];
})
.filter(Boolean);
console.log(templateBlobs);
if (templateBlobs.length > 0) {
// ==================== INTELLIGENT PIXEL COUNTING SYSTEM ====================
// Calculate total pixel count for templates actively being displayed in this tile
// This provides accurate statistics by counting only templates with content in the current viewport
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 comprehensive status information including both template count and pixel statistics
// This gives users immediate feedback about the complexity and scope of what's being rendered
this.overlay.handleDisplayStatus(
`Displaying ${templateBlobs.length} template${templateBlobs.length == 1 ? '' : 's'}. ` +
`Total pixels: ${pixelCountFormatted}`
);
}
const tileBitmap = await createImageBitmap(tileBlob);
const canvas = new OffscreenCanvas(drawSize, drawSize);
const context = canvas.getContext('2d');
context.imageSmoothingEnabled = false; // Nearest neighbor
// Tells the canvas to ignore anything outside of this area
context.beginPath();
context.rect(0, 0, drawSize, drawSize);
context.clip();
context.clearRect(0, 0, drawSize, drawSize); // Draws transparent background
context.drawImage(tileBitmap, 0, 0, drawSize, drawSize);
// For each template in this tile, draw them.
for (const templateBitmap of templateBlobs) {
console.log(`Template Blob is ${typeof templateBitmap}`);
console.log(templateBitmap);
context.drawImage(templateBitmap, 0, 0);
}
return await canvas.convertToBlob({ type: 'image/png' });
}
/** Imports the JSON object, and appends it to any JSON object already loaded
*/
importJSON() {
}
/** Parses the Blue Marble JSON object
*/
#parseBlueMarble() {
}
/** Parses the OSU! Place JSON object
*/
#parseOSU() {
}
}