mirror of
https://github.com/tiennm99/WPlace-AutoBOT.git
synced 2026-05-30 10:22:39 +00:00
837 lines
28 KiB
JavaScript
837 lines
28 KiB
JavaScript
// ==UserScript==
|
|
// @name WPlace Overlay Manager
|
|
// @namespace http://tampermonkey.net/
|
|
// @version 2025-09-16.1
|
|
// @description Overlay management for WPlace AutoBot - handles tile processing, chunking, and rendering
|
|
// @author Wbot
|
|
// @match https://wplace.live/*
|
|
// @grant none
|
|
// ==/UserScript==
|
|
|
|
/**
|
|
* OverlayManager - Handles overlay processing, tile chunking, and canvas operations for WPlace AutoBot
|
|
* Extracted from Auto-Image.js for better modularity and reusabilityc
|
|
*/
|
|
class OverlayManager {
|
|
constructor() {
|
|
this.isEnabled = false;
|
|
this.startCoords = null; // { region: {x, y}, pixel: {x, y} }
|
|
this.imageBitmap = null;
|
|
this.chunkedTiles = new Map(); // Map<"tileX,tileY", ImageBitmap>
|
|
this.originalTiles = new Map(); // Map<"tileX,ttileY", ImageBitmap> store latest original tile bitmaps
|
|
this.originalTilesData = new Map(); // Map<"tileX,tileY", {w,h,data:Uint8ClampedArray}> cache full ImageData for fast pixel reads
|
|
this.tileSize = 1000;
|
|
this.processPromise = null; // Track ongoing processing
|
|
this.lastProcessedHash = null; // Cache invalidation
|
|
this.workerPool = null; // Web worker pool for heavy processing
|
|
this.neededTilesForSave = null; // Set of tiles needed for save file restoration
|
|
}
|
|
|
|
/**
|
|
* Toggle overlay enabled state
|
|
* @returns {boolean} New enabled state
|
|
*/
|
|
toggle() {
|
|
this.isEnabled = !this.isEnabled;
|
|
console.log(`Overlay ${this.isEnabled ? 'enabled' : 'disabled'}.`);
|
|
return this.isEnabled;
|
|
}
|
|
|
|
/**
|
|
* Enable overlay
|
|
*/
|
|
enable() {
|
|
this.isEnabled = true;
|
|
|
|
// Force canvas refresh if script was loaded manually (not on startup)
|
|
// This ensures overlay works even when tiles are already cached
|
|
this._triggerCanvasRefreshIfNeeded();
|
|
|
|
// Also try direct canvas overlay approach as backup
|
|
this._attemptDirectCanvasOverlay();
|
|
}
|
|
|
|
/**
|
|
* Attempt to directly overlay on the existing canvas
|
|
* @private
|
|
*/
|
|
_attemptDirectCanvasOverlay() {
|
|
// Only try this if we have overlay data and it's been more than 5 seconds since page load
|
|
const timeSinceLoad = Date.now() - (window.performance?.timing?.navigationStart || 0);
|
|
|
|
if (timeSinceLoad > 5000 && this.chunkedTiles.size > 0) {
|
|
setTimeout(() => {
|
|
this._drawDirectOverlay();
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw overlay directly on the main canvas
|
|
* @private
|
|
*/
|
|
_drawDirectOverlay() {
|
|
try {
|
|
const canvas = document.querySelector('canvas');
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
// Store original composite operation
|
|
const originalComposite = ctx.globalCompositeOperation;
|
|
const originalAlpha = ctx.globalAlpha;
|
|
|
|
// Set overlay blending
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
ctx.globalAlpha = window.state?.overlayOpacity || 0.6;
|
|
|
|
// Draw overlay chunks directly on canvas
|
|
this.chunkedTiles.forEach((bitmap, tileKey) => {
|
|
const [tileX, tileY] = tileKey.split(',').map(Number);
|
|
|
|
// Convert tile coordinates to canvas coordinates
|
|
// This assumes WPlace uses 1000px tiles
|
|
const canvasX = tileX * 1000;
|
|
const canvasY = tileY * 1000;
|
|
|
|
try {
|
|
ctx.drawImage(bitmap, canvasX, canvasY);
|
|
} catch (error) {
|
|
// Ignore individual tile draw errors
|
|
}
|
|
});
|
|
|
|
// Restore original settings
|
|
ctx.globalCompositeOperation = originalComposite;
|
|
ctx.globalAlpha = originalAlpha;
|
|
|
|
} catch (error) {
|
|
// Ignore direct overlay errors silently
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Trigger canvas refresh for manual script execution
|
|
* @private
|
|
*/
|
|
_triggerCanvasRefreshIfNeeded() {
|
|
// Only trigger refresh if we have overlay data and it's been more than 5 seconds since page load
|
|
// (indicates manual execution rather than startup execution)
|
|
const timeSinceLoad = Date.now() - (window.performance?.timing?.navigationStart || 0);
|
|
|
|
if (timeSinceLoad > 5000 && this.chunkedTiles.size > 0) {
|
|
// Try multiple methods to refresh the canvas tiles
|
|
setTimeout(() => {
|
|
this._attemptCanvasRefresh();
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempt to refresh canvas tiles using various methods
|
|
* @private
|
|
*/
|
|
_attemptCanvasRefresh() {
|
|
try {
|
|
// Method 1: Clear browser cache for tile URLs
|
|
if ('caches' in window) {
|
|
caches.keys().then(cacheNames => {
|
|
cacheNames.forEach(cacheName => {
|
|
caches.delete(cacheName);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Method 2: Force browser to bypass cache by adding cache-busting parameter
|
|
// We need to intercept and modify tile requests to add cache-busting
|
|
const originalFetch = window.fetch;
|
|
let fetchOverrideActive = true;
|
|
|
|
window.fetch = async function(...args) {
|
|
const url = args[0] instanceof Request ? args[0].url : args[0];
|
|
|
|
if (fetchOverrideActive && typeof url === 'string' && url.match(/\/\d+\/\d+\.png/)) {
|
|
// Add cache-busting parameter
|
|
const separator = url.includes('?') ? '&' : '?';
|
|
const cacheBustUrl = `${url}${separator}_cb=${Date.now()}`;
|
|
|
|
if (args[0] instanceof Request) {
|
|
args[0] = new Request(cacheBustUrl, args[0]);
|
|
} else {
|
|
args[0] = cacheBustUrl;
|
|
}
|
|
}
|
|
|
|
return originalFetch.apply(this, args);
|
|
};
|
|
|
|
// Disable cache-busting after 10 seconds
|
|
setTimeout(() => {
|
|
fetchOverrideActive = false;
|
|
}, 10000);
|
|
|
|
// Method 3: Try to trigger a slight zoom change
|
|
const canvas = document.querySelector('canvas');
|
|
if (canvas) {
|
|
// Dispatch a wheel event to trigger tile refresh
|
|
const wheelEvent = new WheelEvent('wheel', {
|
|
deltaY: 1,
|
|
deltaMode: 0,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
view: window
|
|
});
|
|
|
|
canvas.dispatchEvent(wheelEvent);
|
|
|
|
// Immediately counter it
|
|
setTimeout(() => {
|
|
const counterEvent = new WheelEvent('wheel', {
|
|
deltaY: -1,
|
|
deltaMode: 0,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
view: window
|
|
});
|
|
canvas.dispatchEvent(counterEvent);
|
|
}, 50);
|
|
}
|
|
|
|
// Method 4: Try to trigger resize event
|
|
window.dispatchEvent(new Event('resize'));
|
|
|
|
} catch (error) {
|
|
console.warn('⚠️ Could not trigger canvas refresh:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable overlay
|
|
*/
|
|
disable() {
|
|
this.isEnabled = false;
|
|
}
|
|
|
|
/**
|
|
* Clear all overlay data and disable
|
|
*/
|
|
clear() {
|
|
this.disable();
|
|
this.imageBitmap = null;
|
|
this.chunkedTiles.clear();
|
|
this.originalTiles.clear();
|
|
this.originalTilesData.clear();
|
|
this.lastProcessedHash = null;
|
|
if (this.processPromise) {
|
|
this.processPromise = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the image bitmap for overlay processing
|
|
* @param {ImageBitmap} imageBitmap - The image to use for overlay
|
|
*/
|
|
async setImage(imageBitmap) {
|
|
this.imageBitmap = imageBitmap;
|
|
this.lastProcessedHash = null; // Invalidate cache
|
|
if (this.imageBitmap && this.startCoords) {
|
|
await this.processImageIntoChunks();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the position for overlay rendering
|
|
* @param {Object} startPosition - Starting pixel position {x, y}
|
|
* @param {Object} region - Region coordinates {x, y}
|
|
*/
|
|
async setPosition(startPosition, region) {
|
|
if (!startPosition || !region) {
|
|
this.startCoords = null;
|
|
this.chunkedTiles.clear();
|
|
this.lastProcessedHash = null;
|
|
return;
|
|
}
|
|
this.startCoords = { region, pixel: startPosition };
|
|
this.lastProcessedHash = null; // Invalidate cache
|
|
if (this.imageBitmap) {
|
|
await this.processImageIntoChunks();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate hash for cache invalidation
|
|
* @returns {string|null} Hash string or null if no data
|
|
* @private
|
|
*/
|
|
_generateProcessHash() {
|
|
if (!this.imageBitmap || !this.startCoords) return null;
|
|
const { width, height } = this.imageBitmap;
|
|
const { x: px, y: py } = this.startCoords.pixel;
|
|
const { x: rx, y: ry } = this.startCoords.region;
|
|
|
|
// Access global state for overlay settings
|
|
const blueMarbleEnabled = window.state?.blueMarbleEnabled || false;
|
|
const overlayOpacity = window.state?.overlayOpacity || 0.6;
|
|
|
|
return `${width}x${height}_${px},${py}_${rx},${ry}_${blueMarbleEnabled}_${overlayOpacity}`;
|
|
}
|
|
|
|
/**
|
|
* Process image into chunks with caching and batch processing
|
|
*/
|
|
async processImageIntoChunks() {
|
|
if (!this.imageBitmap || !this.startCoords) {
|
|
console.warn('🚫 Cannot process chunks - missing imageBitmap or startCoords');
|
|
return;
|
|
}
|
|
|
|
console.log('🔄 Processing image into chunks...', {
|
|
imageSize: `${this.imageBitmap.width}x${this.imageBitmap.height}`,
|
|
startCoords: this.startCoords
|
|
});
|
|
|
|
// Check if we're already processing to avoid duplicate work
|
|
if (this.processPromise) {
|
|
console.log('⏳ Already processing chunks - waiting...');
|
|
return this.processPromise;
|
|
}
|
|
|
|
// Check cache validity
|
|
const currentHash = this._generateProcessHash();
|
|
if (this.lastProcessedHash === currentHash && this.chunkedTiles.size > 0) {
|
|
// Using cached overlay chunks
|
|
return;
|
|
}
|
|
|
|
// Start processing
|
|
console.log('🚀 Starting new chunk processing...');
|
|
this.processPromise = this._doProcessImageIntoChunks();
|
|
try {
|
|
await this.processPromise;
|
|
this.lastProcessedHash = currentHash;
|
|
console.log(`✅ Chunk processing complete - ${this.chunkedTiles.size} tiles created`);
|
|
} finally {
|
|
this.processPromise = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to process image into chunks
|
|
* @private
|
|
*/
|
|
async _doProcessImageIntoChunks() {
|
|
const startTime = performance.now();
|
|
this.chunkedTiles.clear();
|
|
|
|
const { width: imageWidth, height: imageHeight } = this.imageBitmap;
|
|
const { x: startPixelX, y: startPixelY } = this.startCoords.pixel;
|
|
const { x: startRegionX, y: startRegionY } = this.startCoords.region;
|
|
|
|
const { startTileX, startTileY, endTileX, endTileY } = this.calculateTileRange(
|
|
startRegionX,
|
|
startRegionY,
|
|
startPixelX,
|
|
startPixelY,
|
|
imageWidth,
|
|
imageHeight,
|
|
this.tileSize
|
|
);
|
|
|
|
const totalTiles = (endTileX - startTileX + 1) * (endTileY - startTileY + 1);
|
|
console.log(`🔄 Processing ${totalTiles} overlay tiles...`);
|
|
|
|
// Process tiles in batches to avoid blocking the main thread
|
|
const batchSize = 4; // Process 4 tiles at a time
|
|
const tilesToProcess = [];
|
|
|
|
for (let ty = startTileY; ty <= endTileY; ty++) {
|
|
for (let tx = startTileX; tx <= endTileX; tx++) {
|
|
tilesToProcess.push({ tx, ty });
|
|
}
|
|
}
|
|
|
|
// Process tiles in batches with yielding
|
|
for (let i = 0; i < tilesToProcess.length; i += batchSize) {
|
|
const batch = tilesToProcess.slice(i, i + batchSize);
|
|
|
|
await Promise.all(
|
|
batch.map(async ({ tx, ty }) => {
|
|
const tileKey = `${tx},${ty}`;
|
|
const chunkBitmap = await this._processTile(
|
|
tx,
|
|
ty,
|
|
imageWidth,
|
|
imageHeight,
|
|
startPixelX,
|
|
startPixelY,
|
|
startRegionX,
|
|
startRegionY
|
|
);
|
|
if (chunkBitmap) {
|
|
this.chunkedTiles.set(tileKey, chunkBitmap);
|
|
}
|
|
})
|
|
);
|
|
|
|
// Yield control to prevent blocking
|
|
if (i + batchSize < tilesToProcess.length) {
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
}
|
|
}
|
|
|
|
const processingTime = performance.now() - startTime;
|
|
console.log(
|
|
`✅ Overlay processed ${this.chunkedTiles.size} tiles in ${Math.round(processingTime)}ms`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Process a single tile
|
|
* @param {number} tx - Tile X coordinate
|
|
* @param {number} ty - Tile Y coordinate
|
|
* @param {number} imageWidth - Image width
|
|
* @param {number} imageHeight - Image height
|
|
* @param {number} startPixelX - Starting pixel X
|
|
* @param {number} startPixelY - Starting pixel Y
|
|
* @param {number} startRegionX - Starting region X
|
|
* @param {number} startRegionY - Starting region Y
|
|
* @returns {Promise<ImageBitmap|null>} Processed tile bitmap
|
|
* @private
|
|
*/
|
|
async _processTile(
|
|
tx,
|
|
ty,
|
|
imageWidth,
|
|
imageHeight,
|
|
startPixelX,
|
|
startPixelY,
|
|
startRegionX,
|
|
startRegionY
|
|
) {
|
|
const tileKey = `${tx},${ty}`;
|
|
|
|
// Calculate the portion of the image that overlaps with this tile
|
|
const imgStartX = (tx - startRegionX) * this.tileSize - startPixelX;
|
|
const imgStartY = (ty - startRegionY) * this.tileSize - startPixelY;
|
|
|
|
// Crop coordinates within the source image
|
|
const sX = Math.max(0, imgStartX);
|
|
const sY = Math.max(0, imgStartY);
|
|
const sW = Math.min(imageWidth - sX, this.tileSize - (sX - imgStartX));
|
|
const sH = Math.min(imageHeight - sY, this.tileSize - (sY - imgStartY));
|
|
|
|
if (sW <= 0 || sH <= 0) return null;
|
|
|
|
// Destination coordinates on the new chunk canvas
|
|
const dX = Math.max(0, -imgStartX);
|
|
const dY = Math.max(0, -imgStartY);
|
|
|
|
const chunkCanvas = new OffscreenCanvas(this.tileSize, this.tileSize);
|
|
const chunkCtx = chunkCanvas.getContext('2d');
|
|
chunkCtx.imageSmoothingEnabled = false;
|
|
|
|
chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH);
|
|
|
|
// Blue marble effect with faster pixel manipulation
|
|
const blueMarbleEnabled = window.state?.blueMarbleEnabled || false;
|
|
if (blueMarbleEnabled) {
|
|
const imageData = chunkCtx.getImageData(dX, dY, sW, sH);
|
|
const data = imageData.data;
|
|
|
|
// Faster pixel manipulation using typed arrays
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
const pixelIndex = i / 4;
|
|
const pixelY = Math.floor(pixelIndex / sW);
|
|
const pixelX = pixelIndex % sW;
|
|
|
|
if ((pixelX + pixelY) % 2 === 0 && data[i + 3] > 0) {
|
|
data[i + 3] = 0; // Set alpha to 0
|
|
}
|
|
}
|
|
|
|
chunkCtx.putImageData(imageData, dX, dY);
|
|
}
|
|
|
|
return await chunkCanvas.transferToImageBitmap();
|
|
}
|
|
|
|
/**
|
|
* Process and respond to tile requests with overlay compositing
|
|
* @param {Object} eventData - Event data containing endpoint, blobID, and blobData
|
|
*/
|
|
async processAndRespondToTileRequest(eventData) {
|
|
const { endpoint, blobID, blobData } = eventData;
|
|
|
|
let finalBlob = blobData;
|
|
|
|
if (this.isEnabled && this.chunkedTiles.size > 0) {
|
|
const tileMatch = endpoint.match(/(\d+)\/(\d+)\.png/);
|
|
if (tileMatch) {
|
|
const tileX = parseInt(tileMatch[1], 10);
|
|
const tileY = parseInt(tileMatch[2], 10);
|
|
const tileKey = `${tileX},${tileY}`;
|
|
|
|
const chunkBitmap = this.chunkedTiles.get(tileKey);
|
|
// Also store the original tile bitmap for later pixel color checks
|
|
try {
|
|
const originalBitmap = await createImageBitmap(blobData);
|
|
this.originalTiles.set(tileKey, originalBitmap);
|
|
// Cache full ImageData for fast pixel access (avoid repeated drawImage/getImageData)
|
|
try {
|
|
let canvas, ctx;
|
|
if (typeof OffscreenCanvas !== 'undefined') {
|
|
canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height);
|
|
ctx = canvas.getContext('2d');
|
|
} else {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = originalBitmap.width;
|
|
canvas.height = originalBitmap.height;
|
|
ctx = canvas.getContext('2d');
|
|
}
|
|
ctx.imageSmoothingEnabled = false;
|
|
ctx.drawImage(originalBitmap, 0, 0);
|
|
const imgData = ctx.getImageData(0, 0, originalBitmap.width, originalBitmap.height);
|
|
// Store typed array copy to avoid retaining large canvas
|
|
this.originalTilesData.set(tileKey, {
|
|
w: originalBitmap.width,
|
|
h: originalBitmap.height,
|
|
data: new Uint8ClampedArray(imgData.data),
|
|
});
|
|
} catch (e) {
|
|
// If ImageData extraction fails, still keep the bitmap as fallback
|
|
console.warn('OverlayManager: could not cache ImageData for', tileKey, e);
|
|
}
|
|
} catch (e) {
|
|
console.warn('OverlayManager: could not create original bitmap for', tileKey, e);
|
|
}
|
|
|
|
if (chunkBitmap) {
|
|
try {
|
|
// Use faster compositing for better performance
|
|
finalBlob = await this._compositeTileOptimized(blobData, chunkBitmap);
|
|
} catch (e) {
|
|
console.error('Error compositing overlay:', e);
|
|
// Fallback to original tile on error
|
|
finalBlob = blobData;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send the (possibly modified) blob back to the injected script
|
|
window.postMessage(
|
|
{
|
|
source: 'auto-image-overlay',
|
|
blobID: blobID,
|
|
blobData: finalBlob,
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get pixel color from a tile
|
|
* @param {number} tileX - Tile X coordinate
|
|
* @param {number} tileY - Tile Y coordinate
|
|
* @param {number} pixelX - Pixel X within tile
|
|
* @param {number} pixelY - Pixel Y within tile
|
|
* @returns {Promise<Array|null>} [r,g,b,a] or null
|
|
*/
|
|
async getTilePixelColor(tileX, tileY, pixelX, pixelY) {
|
|
const tileKey = `${tileX},${tileY}`;
|
|
|
|
// Get transparency threshold from global state or config
|
|
const alphaThresh = window.state?.customTransparencyThreshold ||
|
|
window.CONFIG?.TRANSPARENCY_THRESHOLD || 10;
|
|
|
|
// 1. Prefer cached ImageData if available
|
|
const cached = this.originalTilesData.get(tileKey);
|
|
if (cached && cached.data && cached.w > 0 && cached.h > 0) {
|
|
const x = Math.max(0, Math.min(cached.w - 1, pixelX));
|
|
const y = Math.max(0, Math.min(cached.h - 1, pixelY));
|
|
const idx = (y * cached.w + x) * 4;
|
|
const d = cached.data;
|
|
const a = d[idx + 3];
|
|
|
|
const paintTransparentPixels = window.state?.paintTransparentPixels || false;
|
|
if (!paintTransparentPixels && a < alphaThresh) {
|
|
// Treat as transparent / unavailable
|
|
return null;
|
|
}
|
|
return [d[idx], d[idx + 1], d[idx + 2], a];
|
|
}
|
|
|
|
// 2. Fallback: use bitmap, with retry
|
|
const maxRetries = 3;
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
const bitmap = this.originalTiles.get(tileKey);
|
|
if (!bitmap) {
|
|
if (attempt === maxRetries) {
|
|
console.warn('OverlayManager: no bitmap for', tileKey, 'after', maxRetries, 'attempts');
|
|
} else {
|
|
await this._sleep(50 * attempt); // exponential delay
|
|
}
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
let canvas, ctx;
|
|
if (typeof OffscreenCanvas !== 'undefined') {
|
|
canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
ctx = canvas.getContext('2d');
|
|
} else {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = bitmap.width;
|
|
canvas.height = bitmap.height;
|
|
ctx = canvas.getContext('2d');
|
|
}
|
|
ctx.imageSmoothingEnabled = false;
|
|
ctx.drawImage(bitmap, 0, 0);
|
|
|
|
const x = Math.max(0, Math.min(bitmap.width - 1, pixelX));
|
|
const y = Math.max(0, Math.min(bitmap.height - 1, pixelY));
|
|
const data = ctx.getImageData(x, y, 1, 1).data;
|
|
const a = data[3];
|
|
|
|
const paintTransparentPixels = window.state?.paintTransparentPixels || false;
|
|
if (!paintTransparentPixels && a < alphaThresh) {
|
|
return null;
|
|
}
|
|
|
|
return [data[0], data[1], data[2], a];
|
|
} catch (e) {
|
|
console.warn('OverlayManager: failed to read pixel (attempt', attempt, ')', tileKey, e);
|
|
if (attempt < maxRetries) {
|
|
await this._sleep(50 * attempt);
|
|
} else {
|
|
console.error(
|
|
'OverlayManager: failed to read pixel after',
|
|
maxRetries,
|
|
'attempts',
|
|
tileKey
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. If everything fails, return null
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Composite tile with overlay using optimized rendering
|
|
* @param {Blob} originalBlob - Original tile blob
|
|
* @param {ImageBitmap} overlayBitmap - Overlay bitmap
|
|
* @returns {Promise<Blob>} Composited tile blob
|
|
* @private
|
|
*/
|
|
async _compositeTileOptimized(originalBlob, overlayBitmap) {
|
|
const originalBitmap = await createImageBitmap(originalBlob);
|
|
const canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height);
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Disable antialiasing for pixel-perfect rendering
|
|
ctx.imageSmoothingEnabled = false;
|
|
|
|
// Draw original tile first
|
|
ctx.drawImage(originalBitmap, 0, 0);
|
|
|
|
// Set opacity and draw overlay with optimized blend mode
|
|
const overlayOpacity = window.state?.overlayOpacity || 0.6;
|
|
ctx.globalAlpha = overlayOpacity;
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
ctx.drawImage(overlayBitmap, 0, 0);
|
|
|
|
// Use faster blob conversion with compression settings
|
|
return await canvas.convertToBlob({
|
|
type: 'image/png',
|
|
quality: 0.95, // Slight compression for faster processing
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wait until all required tiles are loaded and cached
|
|
* @param {number} startRegionX - Starting region X
|
|
* @param {number} startRegionY - Starting region Y
|
|
* @param {number} pixelWidth - Image width in pixels
|
|
* @param {number} pixelHeight - Image height in pixels
|
|
* @param {number} startPixelX - Starting pixel X offset
|
|
* @param {number} startPixelY - Starting pixel Y offset
|
|
* @param {number} timeoutMs - Timeout in milliseconds
|
|
* @returns {Promise<boolean>} True if tiles are ready
|
|
*/
|
|
async waitForTiles(
|
|
startRegionX,
|
|
startRegionY,
|
|
pixelWidth,
|
|
pixelHeight,
|
|
startPixelX = 0,
|
|
startPixelY = 0,
|
|
timeoutMs = 10000
|
|
) {
|
|
const { startTileX, startTileY, endTileX, endTileY } = this.calculateTileRange(
|
|
startRegionX,
|
|
startRegionY,
|
|
startPixelX,
|
|
startPixelY,
|
|
pixelWidth,
|
|
pixelHeight,
|
|
this.tileSize
|
|
);
|
|
|
|
const requiredTiles = [];
|
|
for (let ty = startTileY; ty <= endTileY; ty++) {
|
|
for (let tx = startTileX; tx <= endTileX; tx++) {
|
|
requiredTiles.push(`${tx},${ty}`);
|
|
}
|
|
}
|
|
|
|
if (requiredTiles.length === 0) return true;
|
|
|
|
const startTime = Date.now();
|
|
|
|
while (Date.now() - startTime < timeoutMs) {
|
|
const stopFlag = window.state?.stopFlag || false;
|
|
if (stopFlag) {
|
|
console.log('waitForTiles: stopped by user');
|
|
return false;
|
|
}
|
|
|
|
const missing = requiredTiles.filter((key) => !this.originalTiles.has(key));
|
|
if (missing.length === 0) {
|
|
console.log(`✅ All ${requiredTiles.length} required tiles are loaded`);
|
|
return true;
|
|
}
|
|
|
|
await this._sleep(100);
|
|
}
|
|
|
|
console.warn(`❌ Timeout waiting for tiles: ${requiredTiles.length} required,
|
|
${requiredTiles.filter((k) => this.originalTiles.has(k)).length} loaded`);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Calculate tile range for given parameters
|
|
* @param {number} startRegionX - Starting region X
|
|
* @param {number} startRegionY - Starting region Y
|
|
* @param {number} startPixelX - Starting pixel X
|
|
* @param {number} startPixelY - Starting pixel Y
|
|
* @param {number} width - Width in pixels
|
|
* @param {number} height - Height in pixels
|
|
* @param {number} tileSize - Size of each tile (default 1000)
|
|
* @returns {{startTileX: number, startTileY: number, endTileX: number, endTileY: number}}
|
|
*/
|
|
calculateTileRange(
|
|
startRegionX,
|
|
startRegionY,
|
|
startPixelX,
|
|
startPixelY,
|
|
width,
|
|
height,
|
|
tileSize = 1000
|
|
) {
|
|
const endPixelX = startPixelX + width;
|
|
const endPixelY = startPixelY + height;
|
|
|
|
return {
|
|
startTileX: startRegionX + Math.floor(startPixelX / tileSize),
|
|
startTileY: startRegionY + Math.floor(startPixelY / tileSize),
|
|
endTileX: startRegionX + Math.floor((endPixelX - 1) / tileSize),
|
|
endTileY: startRegionY + Math.floor((endPixelY - 1) / tileSize),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Preload tiles for save file coordinates to populate originalTiles cache
|
|
* This fixes the "no bitmap" error when loading save files
|
|
* @param {Object} startPosition - The start position from save file
|
|
* @param {Object} region - The region from save file
|
|
* @param {number} width - Image width
|
|
* @param {number} height - Image height
|
|
*/
|
|
/**
|
|
* Preload tiles for save file coordinates to populate originalTiles cache
|
|
* This fixes the "no bitmap" error when loading save files
|
|
* For WPlace, we can't manually fetch tiles like r/place - we need to trigger natural tile loading
|
|
* @param {Object} startPosition - The start position from save file
|
|
* @param {Object} region - The region from save file
|
|
* @param {number} width - Image width
|
|
* @param {number} height - Image height
|
|
*/
|
|
async preloadTilesForSaveFile(startPosition, region, width, height) {
|
|
if (!startPosition || !region || !width || !height) {
|
|
console.warn('OverlayManager: Missing parameters for tile preload');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Calculate tiles that need to be loaded based on image area
|
|
const tileSize = 1000; // WPlace uses 1000px tiles, not 256px like r/place
|
|
const tiles = this.calculateTileRange(
|
|
region.x, region.y,
|
|
startPosition.x, startPosition.y,
|
|
width, height,
|
|
tileSize
|
|
);
|
|
|
|
console.log(`🔄 WPlace: Need to preload ${(tiles.endTileX - tiles.startTileX + 1) * (tiles.endTileY - tiles.startTileY + 1)} tiles for save file...`);
|
|
|
|
// For WPlace, instead of manually fetching tiles, we need to wait for natural tile loading
|
|
// The browser will request tiles as the user navigates, and we intercept them
|
|
// For now, just log the tiles we need and let the overlay work with what's available
|
|
const neededTiles = [];
|
|
for (let tileX = tiles.startTileX; tileX <= tiles.endTileX; tileX++) {
|
|
for (let tileY = tiles.startTileY; tileY <= tiles.endTileY; tileY++) {
|
|
neededTiles.push(`${tileX},${tileY}`);
|
|
}
|
|
}
|
|
|
|
console.log(`📍 WPlace: Will use tiles when available: ${neededTiles.slice(0, 5).join(', ')}${neededTiles.length > 5 ? ` and ${neededTiles.length - 5} more...` : ''}`);
|
|
|
|
// Store the needed tiles list for reference
|
|
this.neededTilesForSave = new Set(neededTiles);
|
|
|
|
console.log('✅ WPlace: Tile preload setup complete - tiles will populate as user navigates');
|
|
} catch (error) {
|
|
console.error('❌ Failed to setup tile preload for save file:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sleep utility
|
|
* @param {number} ms - Milliseconds to sleep
|
|
* @returns {Promise<void>}
|
|
* @private
|
|
*/
|
|
_sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* Clean up resources
|
|
*/
|
|
cleanup() {
|
|
this.clear();
|
|
if (this.workerPool) {
|
|
// Clean up worker pool if implemented
|
|
this.workerPool = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create global instance
|
|
window.WPlaceOverlayManager = OverlayManager;
|
|
|
|
// Create global instance for Auto-Image.js compatibility
|
|
window.globalOverlayManager = new OverlayManager();
|
|
|
|
// Legacy compatibility - expose key methods globally for backward compatibility
|
|
window.OverlayManager = OverlayManager;
|
|
|
|
console.log('✅ WPlace Overlay Manager loaded and ready'); |