mirror of
https://github.com/ShinkoNet/Wplace-Overlay-Pro.git
synced 2026-04-20 19:12:05 +00:00
fix: optimize symbols mode performance and fix rendering bugs
- Fix major performance bottleneck in symbols mode by iterating only over intersection areas instead of full 1000x1000 tiles - Optimize memory usage by creating output canvas only for intersection area - Fix coordinate transformation logic bugs in symbol placement - Remove redundant calculations and improve bounds checking - Clean up TypeScript non-null assertions and unused code - Symbols mode now renders significantly faster and without visual artifacts
This commit is contained in:
parent
7c02beed51
commit
812181c204
9 changed files with 156 additions and 107 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -127,4 +127,6 @@ yarn/cache
|
|||
yarn/unplugged
|
||||
yarn/build-state.yml
|
||||
yarn/install-state.gz
|
||||
pnp.*
|
||||
pnp.*
|
||||
.DS_Store
|
||||
bun.lock
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const metaPath = resolve(__dirname, 'src', 'meta.js');
|
|||
const MetaBannerPlugin = {
|
||||
name: 'meta-banner',
|
||||
setup(build) {
|
||||
build.onEnd(async (result) => {
|
||||
build.onEnd(async () => {
|
||||
try {
|
||||
await mkdir(dirname(outFile), { recursive: true });
|
||||
const [meta, js] = await Promise.all([
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ export class LRUCache<K=any, V=any> {
|
|||
constructor(max = 400) { this.max = max; this.map = new Map(); }
|
||||
get(key: K): V | undefined {
|
||||
if (!this.map.has(key)) return undefined;
|
||||
const val = this.map.get(key)!;
|
||||
this.map.delete(key); this.map.set(key, val);
|
||||
const val = this.map.get(key);
|
||||
if (val !== undefined) {
|
||||
this.map.delete(key);
|
||||
this.map.set(key, val);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
set(key: K, val: V) {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ export async function canvasToDataURLSafe(canvas: OffscreenCanvas | HTMLCanvasEl
|
|||
const bmp = (canvas as any).transferToImageBitmap?.();
|
||||
if (bmp) {
|
||||
const html = createHTMLCanvas(canvas.width, canvas.height);
|
||||
const ctx = html.getContext('2d')!;
|
||||
const ctx = html.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to get 2D context');
|
||||
ctx.drawImage(bmp, 0, 0);
|
||||
return html.toDataURL('image/png');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ function isPalettePerfectImage(img: HTMLImageElement): boolean {
|
|||
if (cached !== undefined) return cached;
|
||||
|
||||
const canvas = createCanvas(img.width, img.height) as any;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctx) throw new Error('Failed to get 2D context');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
const data = imageData.data;
|
||||
|
|
@ -195,7 +196,8 @@ export async function buildOverlayDataForChunkUnified(
|
|||
if (isect.w === 0 || isect.h === 0) { overlayCache.set(cacheKey, null); return null; }
|
||||
|
||||
const canvas = createCanvas(TILE_SIZE, TILE_SIZE) as any;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctx) throw new Error('Failed to get 2D context');
|
||||
ctx.drawImage(img as any, drawX, drawY);
|
||||
|
||||
const imageData = ctx.getImageData(isect.x, isect.y, isect.w, isect.h);
|
||||
|
|
@ -231,31 +233,45 @@ export async function buildOverlayDataForChunkUnified(
|
|||
const isect = rectIntersect(0, 0, tileW, tileH, drawXScaled, drawYScaled, wScaled, hScaled);
|
||||
if (isect.w === 0 || isect.h === 0) { overlayCache.set(cacheKey, null); return null; }
|
||||
|
||||
const canvas = createCanvas(wImg, hImg) as any;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(img as any, 0, 0);
|
||||
const originalImageData = ctx.getImageData(0, 0, wImg, hImg);
|
||||
// Get original image data once
|
||||
const sourceCanvas = createCanvas(wImg, hImg) as any;
|
||||
const sourceCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!sourceCtx) throw new Error('Failed to get 2D context');
|
||||
sourceCtx.imageSmoothingEnabled = false;
|
||||
sourceCtx.drawImage(img as any, 0, 0);
|
||||
const originalImageData = sourceCtx.getImageData(0, 0, wImg, hImg);
|
||||
const srcData = originalImageData.data;
|
||||
|
||||
const outCanvas = createCanvas(tileW, tileH) as any;
|
||||
const outCtx = outCanvas.getContext('2d', { willReadFrequently: true })!;
|
||||
const outputImageData = outCtx.createImageData(tileW, tileH);
|
||||
// Create output canvas for just the intersection area
|
||||
const outCanvas = createCanvas(isect.w, isect.h) as any;
|
||||
const outCtx = outCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!outCtx) throw new Error('Failed to get 2D context');
|
||||
const outputImageData = outCtx.createImageData(isect.w, isect.h);
|
||||
const outData = outputImageData.data;
|
||||
|
||||
// Precompute symbol centering offsets for performance
|
||||
// Precompute symbol centering offsets
|
||||
const centerX = (scale - SYMBOL_W) >> 1;
|
||||
const centerY = (scale - SYMBOL_H) >> 1;
|
||||
|
||||
for (let y = 0; y < TILE_SIZE; y++) {
|
||||
for (let x = 0; x < TILE_SIZE; x++) {
|
||||
const imgX = x - drawX;
|
||||
const imgY = y - drawY;
|
||||
// Convert intersection back to tile coordinates
|
||||
const startTileX = Math.floor(isect.x / scale);
|
||||
const startTileY = Math.floor(isect.y / scale);
|
||||
const endTileX = Math.floor((isect.x + isect.w - 1) / scale);
|
||||
const endTileY = Math.floor((isect.y + isect.h - 1) / scale);
|
||||
|
||||
// Only iterate over tiles that intersect
|
||||
for (let tileY = startTileY; tileY <= endTileY; tileY++) {
|
||||
for (let tileX = startTileX; tileX <= endTileX; tileX++) {
|
||||
// Convert back to original image coordinates
|
||||
const imgX = tileX - drawX;
|
||||
const imgY = tileY - drawY;
|
||||
|
||||
if (imgX >= 0 && imgX < wImg && imgY >= 0 && imgY < hImg) {
|
||||
const idx = (imgY * wImg + imgX) * 4;
|
||||
const r = originalImageData.data[idx];
|
||||
const g = originalImageData.data[idx+1];
|
||||
const b = originalImageData.data[idx+2];
|
||||
const a = originalImageData.data[idx+3];
|
||||
const srcIdx = (imgY * wImg + imgX) * 4;
|
||||
const r = srcData[srcIdx];
|
||||
const g = srcData[srcIdx + 1];
|
||||
const b = srcData[srcIdx + 2];
|
||||
const a = srcData[srcIdx + 3];
|
||||
|
||||
// Early exit for transparent or deface pixels
|
||||
if (a <= 128 || (r === 0xde && g === 0xfa && b === 0xce)) continue;
|
||||
|
|
@ -273,28 +289,33 @@ export async function buildOverlayDataForChunkUnified(
|
|||
|
||||
if (colorIndex < SYMBOL_TILES.length) {
|
||||
const symbol = SYMBOL_TILES[colorIndex];
|
||||
const tileX = x * scale;
|
||||
const tileY = y * scale;
|
||||
|
||||
// Cache palette color to avoid repeated array access
|
||||
const paletteColor = ALL_COLORS[colorIndex];
|
||||
const a_r = paletteColor[0];
|
||||
const a_g = paletteColor[1];
|
||||
const a_b = paletteColor[2];
|
||||
const pR = paletteColor[0];
|
||||
const pG = paletteColor[1];
|
||||
const pB = paletteColor[2];
|
||||
|
||||
// Calculate tile position in scaled coordinates
|
||||
const tileXScaled = tileX * scale;
|
||||
const tileYScaled = tileY * scale;
|
||||
|
||||
// Draw symbol
|
||||
for (let sy = 0; sy < SYMBOL_H; sy++) {
|
||||
for (let sx = 0; sx < SYMBOL_W; sx++) {
|
||||
const bit_idx = sy * SYMBOL_W + sx;
|
||||
const bit = (symbol >>> bit_idx) & 1;
|
||||
|
||||
if (bit) {
|
||||
const outX = tileX + sx + centerX;
|
||||
const outY = tileY + sy + centerY;
|
||||
if (outX >= 0 && outX < tileW && outY >= 0 && outY < tileH) {
|
||||
const outIdx = (outY * tileW + outX) * 4;
|
||||
outData[outIdx] = a_r;
|
||||
outData[outIdx+1] = a_g;
|
||||
outData[outIdx+2] = a_b;
|
||||
outData[outIdx+3] = 255;
|
||||
const outX = tileXScaled + sx + centerX - isect.x;
|
||||
const outY = tileYScaled + sy + centerY - isect.y;
|
||||
|
||||
if (outX >= 0 && outX < isect.w && outY >= 0 && outY < isect.h) {
|
||||
const outIdx = (outY * isect.w + outX) * 4;
|
||||
outData[outIdx] = pR;
|
||||
outData[outIdx + 1] = pG;
|
||||
outData[outIdx + 2] = pB;
|
||||
outData[outIdx + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -303,12 +324,10 @@ export async function buildOverlayDataForChunkUnified(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
outCtx.putImageData(outputImageData, 0, 0);
|
||||
|
||||
const finalIsect = rectIntersect(0, 0, tileW, tileH, 0, 0, tileW, tileH);
|
||||
const finalImageData = outCtx.getImageData(finalIsect.x, finalIsect.y, finalIsect.w, finalIsect.h);
|
||||
|
||||
const result = { imageData: finalImageData, dx: finalIsect.x, dy: finalIsect.y, scaled: true, scale };
|
||||
const result = { imageData: outputImageData, dx: isect.x, dy: isect.y, scaled: true, scale };
|
||||
overlayCache.set(cacheKey, result);
|
||||
return result;
|
||||
} else { // 'dots'
|
||||
|
|
@ -324,7 +343,8 @@ export async function buildOverlayDataForChunkUnified(
|
|||
if (isect.w === 0 || isect.h === 0) { overlayCache.set(cacheKey, null); return null; }
|
||||
|
||||
const canvas = createCanvas(tileW, tileH) as any;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctx) throw new Error('Failed to get 2D context');
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.clearRect(0, 0, tileW, tileH);
|
||||
ctx.drawImage(img as any, 0, 0, wImg, hImg, drawXScaled, drawYScaled, wScaled, hScaled);
|
||||
|
|
@ -380,7 +400,8 @@ export async function composeTileUnified(
|
|||
|
||||
if (!scaledBaseImageData) {
|
||||
const baseCanvas = createCanvas(w * scale, h * scale) as any;
|
||||
const baseCtx = baseCanvas.getContext('2d', { willReadFrequently: true })!;
|
||||
const baseCtx = baseCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!baseCtx) throw new Error('Failed to get 2D context');
|
||||
baseCtx.imageSmoothingEnabled = false;
|
||||
baseCtx.drawImage(originalImage, 0, 0, w * scale, h * scale);
|
||||
scaledBaseImageData = baseCtx.getImageData(0, 0, w * scale, h * scale);
|
||||
|
|
@ -388,7 +409,8 @@ export async function composeTileUnified(
|
|||
}
|
||||
|
||||
const canvas = createCanvas(w * scale, h * scale) as any;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctx) throw new Error('Failed to get 2D context');
|
||||
ctx.putImageData(scaledBaseImageData, 0, 0);
|
||||
|
||||
for (const ovd of overlayDatas) {
|
||||
|
|
@ -397,7 +419,8 @@ export async function composeTileUnified(
|
|||
const th = ovd.imageData.height;
|
||||
if (!tw || !th) continue;
|
||||
const temp = createCanvas(tw, th) as any;
|
||||
const tctx = temp.getContext('2d', { willReadFrequently: true })!;
|
||||
const tctx = temp.getContext('2d', { willReadFrequently: true });
|
||||
if (!tctx) throw new Error('Failed to get 2D context');
|
||||
tctx.putImageData(ovd.imageData, 0, 0);
|
||||
ctx.drawImage(temp, ovd.dx, ovd.dy);
|
||||
}
|
||||
|
|
@ -406,13 +429,15 @@ export async function composeTileUnified(
|
|||
|
||||
const w = originalImage.width, h = originalImage.height;
|
||||
const canvas = createCanvas(w, h) as any;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctx) throw new Error('Failed to get 2D context');
|
||||
|
||||
if (mode === 'behind') {
|
||||
for (const ovd of overlayDatas) {
|
||||
if (!ovd) continue;
|
||||
const temp = createCanvas(ovd.imageData.width, ovd.imageData.height) as any;
|
||||
const tctx = temp.getContext('2d', { willReadFrequently: true })!;
|
||||
const tctx = temp.getContext('2d', { willReadFrequently: true });
|
||||
if (!tctx) throw new Error('Failed to get 2D context');
|
||||
tctx.putImageData(ovd.imageData, 0, 0);
|
||||
ctx.drawImage(temp, ovd.dx, ovd.dy);
|
||||
}
|
||||
|
|
@ -423,7 +448,8 @@ export async function composeTileUnified(
|
|||
for (const ovd of overlayDatas) {
|
||||
if (!ovd) continue;
|
||||
const temp = createCanvas(ovd.imageData.width, ovd.imageData.height) as any;
|
||||
const tctx = temp.getContext('2d', { willReadFrequently: true })!;
|
||||
const tctx = temp.getContext('2d', { willReadFrequently: true });
|
||||
if (!tctx) throw new Error('Failed to get 2D context');
|
||||
tctx.putImageData(ovd.imageData, 0, 0);
|
||||
ctx.drawImage(temp, ovd.dx, ovd.dy);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@
|
|||
|
||||
import { bootstrapApp } from "./app";
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
(() => {
|
||||
window.onload = () => {
|
||||
bootstrapApp().catch((e) =>
|
||||
console.error("Overlay Pro bootstrap failed", e),
|
||||
);
|
||||
};
|
||||
})();
|
||||
export {};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name Wplace Overlay Pro
|
||||
// @namespace http://tampermonkey.net/
|
||||
// @version 3.1.5
|
||||
// @version 3.1.6
|
||||
// @description Overlays tiles on wplace.live. Can also resize, and color-match your overlay to wplace's palette. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under GPLv3.
|
||||
// @author shinkonet
|
||||
// @match https://wplace.live/*
|
||||
|
|
|
|||
110
src/ui/panel.ts
110
src/ui/panel.ts
|
|
@ -12,7 +12,13 @@ import { EV_ANCHOR_SET, EV_AUTOCAP_CHANGED } from '../core/events';
|
|||
|
||||
let panelEl: HTMLDivElement | null = null;
|
||||
|
||||
function $(id: string) { return document.getElementById(id)!; }
|
||||
function $(id: string): HTMLElement {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) {
|
||||
throw new Error(`Element with id "${id}" not found.`);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
export function createUI() {
|
||||
if (document.getElementById('overlay-pro-panel')) return;
|
||||
|
|
@ -23,8 +29,8 @@ export function createUI() {
|
|||
|
||||
const panelW = 340;
|
||||
const defaultLeft = Math.max(12, window.innerWidth - panelW - 80);
|
||||
panel.style.left = (Number.isFinite(config.panelX as any) ? (config.panelX as any) : defaultLeft) + 'px';
|
||||
panel.style.top = (Number.isFinite(config.panelY as any) ? (config.panelY as any) : 120) + 'px';
|
||||
panel.style.left = `${Number.isFinite(config.panelX as any) ? (config.panelX as any) : defaultLeft}px`;
|
||||
panel.style.top = `${Number.isFinite(config.panelY as any) ? (config.panelY as any) : 120}px`;
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="op-header" id="op-header">
|
||||
|
|
@ -55,7 +61,7 @@ export function createUI() {
|
|||
<div class="op-mode-setting" data-setting="minify">
|
||||
<div class="op-row"><label>Style</label>
|
||||
<div class="op-row"><input type="radio" name="minify-style" value="dots" id="op-style-dots"><label for="op-style-dots">Dots</label></div>
|
||||
<div class="op-row"><input type="radio" name="minify-style" value="symbols" id="op-style-symbols"><label for="op-style-symbols">Symbols (slow and buggy, wait 4 fix!)</label></div>
|
||||
<div class="op-row"><input type="radio" name="minify-style" value="symbols" id="op-style-symbols"><label for="op-style-symbols">Symbols</label></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -177,7 +183,7 @@ function rebuildOverlayListUI() {
|
|||
list.innerHTML = '';
|
||||
for (const ov of config.overlays) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'op-item' + (ov.id === config.activeOverlayId ? ' active' : '');
|
||||
item.className = `op-item${ov.id === config.activeOverlayId ? ' active' : ''}`;
|
||||
const localTag = ov.isLocal ? ' (local)' : (!ov.imageBase64 ? ' (no image)' : '');
|
||||
item.innerHTML = `
|
||||
<input type="radio" name="op-active" ${ov.id === config.activeOverlayId ? 'checked' : ''} title="Set active"/>
|
||||
|
|
@ -221,7 +227,7 @@ async function setOverlayImageFromURL(ov: any, url: string) {
|
|||
await saveConfig(['overlays']); clearOverlayCache();
|
||||
config.autoCapturePixelUrl = true; await saveConfig(['autoCapturePixelUrl']);
|
||||
ensureHook(); updateUI();
|
||||
showToast(`Image loaded. Placement mode ON -- click once to set anchor.`);
|
||||
showToast('Image loaded. Placement mode ON -- click once to set anchor.');
|
||||
}
|
||||
async function setOverlayImageFromFile(ov: any, file: File) {
|
||||
if (!file || !file.type || !file.type.startsWith('image/')) { alert('Please choose an image file.'); return; }
|
||||
|
|
@ -231,20 +237,30 @@ async function setOverlayImageFromFile(ov: any, file: File) {
|
|||
await saveConfig(['overlays']); clearOverlayCache();
|
||||
config.autoCapturePixelUrl = true; await saveConfig(['autoCapturePixelUrl']);
|
||||
ensureHook(); updateUI();
|
||||
showToast(`Local image loaded. Placement mode ON -- click once to set anchor.`);
|
||||
showToast('Local image loaded. Placement mode ON -- click once to set anchor.');
|
||||
}
|
||||
|
||||
type ImportedOverlay = {
|
||||
name?: string;
|
||||
imageUrl?: string;
|
||||
pixelUrl?: string | null;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
opacity?: number;
|
||||
};
|
||||
|
||||
async function importOverlayFromJSON(jsonText: string) {
|
||||
let obj; try { obj = JSON.parse(jsonText); } catch { alert('Invalid JSON'); return; }
|
||||
const arr = Array.isArray(obj) ? obj : [obj];
|
||||
let obj: unknown;
|
||||
try { obj = JSON.parse(jsonText) as unknown; } catch { alert('Invalid JSON'); return; }
|
||||
const arr: ImportedOverlay[] = Array.isArray(obj) ? (obj as ImportedOverlay[]) : [obj as ImportedOverlay];
|
||||
let imported = 0, failed = 0;
|
||||
for (const item of arr) {
|
||||
const name = uniqueName(item.name || 'Imported Overlay', config.overlays.map(o => o.name || ''));
|
||||
const imageUrl = item.imageUrl;
|
||||
const pixelUrl = item.pixelUrl ?? null;
|
||||
const offsetX = Number.isFinite(item.offsetX) ? item.offsetX : 0;
|
||||
const offsetY = Number.isFinite(item.offsetY) ? item.offsetY : 0;
|
||||
const opacity = Number.isFinite(item.opacity) ? item.opacity : 0.7;
|
||||
const name = uniqueName(item?.name || 'Imported Overlay', config.overlays.map(o => o.name || ''));
|
||||
const imageUrl = item?.imageUrl;
|
||||
const pixelUrl = item?.pixelUrl ?? null;
|
||||
const offsetX = Number.isFinite(item?.offsetX) ? (item?.offsetX as number) : 0;
|
||||
const offsetY = Number.isFinite(item?.offsetY) ? (item?.offsetY as number) : 0;
|
||||
const opacity = Number.isFinite(item?.opacity) ? (item?.opacity as number) : 0.7;
|
||||
if (!imageUrl) { failed++; continue; }
|
||||
try {
|
||||
const base64 = await urlToDataURL(imageUrl);
|
||||
|
|
@ -253,7 +269,7 @@ async function importOverlayFromJSON(jsonText: string) {
|
|||
} catch (e) { console.error('Import failed for', imageUrl, e); failed++; }
|
||||
}
|
||||
if (imported > 0) {
|
||||
config.activeOverlayId = config.overlays[config.overlays.length - 1].id;
|
||||
config.activeOverlayId = config.overlays[config.overlays.length - 1]?.id || null;
|
||||
await saveConfig(['overlays', 'activeOverlayId']); clearOverlayCache(); ensureHook(); updateUI();
|
||||
}
|
||||
alert(`Import finished. Imported: ${imported}${failed ? `, Failed: ${failed}` : ''}`);
|
||||
|
|
@ -268,7 +284,7 @@ function exportActiveOverlayToClipboard() {
|
|||
copyText(text).then(() => alert('Overlay JSON copied to clipboard!')).catch(() => { prompt('Copy the JSON below:', text); });
|
||||
}
|
||||
function copyText(text: string) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text);
|
||||
if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(text);
|
||||
return Promise.reject(new Error('Clipboard API not available'));
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +338,7 @@ function addEventListeners(panel: HTMLDivElement) {
|
|||
const dropzone = $('op-dropzone');
|
||||
dropzone.addEventListener('click', () => $('op-file-input').click());
|
||||
$('op-file-input').addEventListener('change', async (e: any) => {
|
||||
const file = e.target.files && e.target.files[0]; e.target.value=''; if (!file) return;
|
||||
const file = e.target.files?.[0]; e.target.value=''; if (!file) return;
|
||||
const ov = getActiveOverlay(); if (!ov) return;
|
||||
if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; }
|
||||
try { await setOverlayImageFromFile(ov, file); } catch (err) { console.error(err); alert('Failed to load local image.'); }
|
||||
|
|
@ -330,7 +346,7 @@ function addEventListeners(panel: HTMLDivElement) {
|
|||
['dragenter', 'dragover'].forEach(evt => dropzone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drop-highlight'); }));
|
||||
['dragleave', 'drop'].forEach(evt => dropzone.addEventListener(evt, (e: any) => { e.preventDefault(); e.stopPropagation(); if (evt === 'dragleave' && e.target !== dropzone) return; dropzone.classList.remove('drop-highlight'); }));
|
||||
dropzone.addEventListener('drop', async (e: any) => {
|
||||
const dt = e.dataTransfer; if (!dt) return; const file = dt.files && dt.files[0]; if (!file) return;
|
||||
const dt = e.dataTransfer; if (!dt) return; const file = dt.files?.[0]; if (!file) return;
|
||||
const ov = getActiveOverlay(); if (!ov) return;
|
||||
if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; }
|
||||
try { await setOverlayImageFromFile(ov, file); } catch (err) { console.error(err); alert('Failed to load dropped image.'); }
|
||||
|
|
@ -349,7 +365,7 @@ function addEventListeners(panel: HTMLDivElement) {
|
|||
$('op-opacity-slider').addEventListener('input', (e: any) => {
|
||||
const ov = getActiveOverlay(); if (!ov) return;
|
||||
ov.opacity = parseFloat(e.target.value);
|
||||
$('op-opacity-value').textContent = Math.round(ov.opacity * 100) + '%';
|
||||
$('op-opacity-value').textContent = `${Math.round(ov.opacity * 100)}%`;
|
||||
});
|
||||
$('op-opacity-slider').addEventListener('change', async () => { await saveConfig(['overlays']); clearOverlayCache(); });
|
||||
|
||||
|
|
@ -396,8 +412,8 @@ function enableDrag(panel: HTMLDivElement) {
|
|||
const dx = e.clientX - startX, dy = e.clientY - startY;
|
||||
const maxLeft = Math.max(8, window.innerWidth - panel.offsetWidth - 8);
|
||||
const maxTop = Math.max(8, window.innerHeight - panel.offsetHeight - 8);
|
||||
panel.style.left = clamp(startLeft + dx, 8, maxLeft) + 'px';
|
||||
panel.style.top = clamp(startTop + dy, 8, maxTop) + 'px';
|
||||
panel.style.left = `${clamp(startLeft + dx, 8, maxLeft)}px`;
|
||||
panel.style.top = `${clamp(startTop + dy, 8, maxTop)}px`;
|
||||
moved = true;
|
||||
};
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
|
|
@ -420,12 +436,12 @@ function enableDrag(panel: HTMLDivElement) {
|
|||
const maxTop = Math.max(8, window.innerHeight - panel.offsetHeight - 8);
|
||||
const newLeft = Math.min(Math.max(rect.left, 8), maxLeft);
|
||||
const newTop = Math.min(Math.max(rect.top, 8), maxTop);
|
||||
panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px';
|
||||
panel.style.left = `${newLeft}px`; panel.style.top = `${newTop}px`;
|
||||
config.panelX = newLeft; config.panelY = newTop; saveConfig(['panelX', 'panelY']);
|
||||
});
|
||||
}
|
||||
|
||||
function updateEditorUI() {
|
||||
export function updateEditorUI(): void {
|
||||
const editorSect = $('op-editor-section');
|
||||
const editorBody = $('op-editor-body');
|
||||
const ov = getActiveOverlay();
|
||||
|
|
@ -454,12 +470,11 @@ function updateEditorUI() {
|
|||
|
||||
const coords = ov.pixelUrl ? extractPixelCoords(ov.pixelUrl) : { chunk1: '-', chunk2: '-', posX: '-', posY: '-' } as any;
|
||||
const coordDisplay = $('op-coord-display');
|
||||
if(coordDisplay) {
|
||||
if (coordDisplay) {
|
||||
coordDisplay.textContent = ov.pixelUrl
|
||||
? `Ref: chunk ${coords.chunk1}/${coords.chunk2} at (${coords.posX}, ${coords.posY})`
|
||||
: `No pixel anchor set. Enable placement and click a pixel.`;
|
||||
: 'No pixel anchor set. Enable placement and click a pixel.';
|
||||
}
|
||||
|
||||
|
||||
const indicator = $('op-offset-indicator');
|
||||
if (indicator) indicator.textContent = `Offset X ${ov.offsetX}, Y ${ov.offsetY}`;
|
||||
|
|
@ -482,16 +497,18 @@ export function updateUI() {
|
|||
toggle.title = collapsed ? 'Expand' : 'Collapse';
|
||||
|
||||
// --- Mode Tabs ---
|
||||
panelEl.querySelectorAll('.op-tab-btn').forEach(btn => {
|
||||
const mode = btn.getAttribute('data-mode');
|
||||
let isActive = false;
|
||||
if (mode === 'above' && (config.overlayMode === 'above' || config.overlayMode === 'behind')) {
|
||||
if (panelEl) {
|
||||
panelEl.querySelectorAll('.op-tab-btn').forEach(btn => {
|
||||
const mode = btn.getAttribute('data-mode');
|
||||
let isActive = false;
|
||||
if (mode === 'above' && (config.overlayMode === 'above' || config.overlayMode === 'behind')) {
|
||||
isActive = true;
|
||||
} else {
|
||||
} else {
|
||||
isActive = mode === config.overlayMode;
|
||||
}
|
||||
btn.classList.toggle('active', isActive);
|
||||
});
|
||||
}
|
||||
btn.classList.toggle('active', isActive);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Mode Settings ---
|
||||
const fullOverlaySettings = $('op-mode-settings').querySelector('[data-setting="above"]') as HTMLDivElement;
|
||||
|
|
@ -501,9 +518,9 @@ export function updateUI() {
|
|||
fullOverlaySettings.classList.add('active');
|
||||
minifySettings.classList.remove('active');
|
||||
const ov = getActiveOverlay();
|
||||
if(ov) {
|
||||
( $('op-opacity-slider') as HTMLInputElement ).value = String(ov.opacity);
|
||||
$('op-opacity-value').textContent = Math.round(ov.opacity * 100) + '%';
|
||||
if (ov) {
|
||||
( $('op-opacity-slider') as HTMLInputElement ).value = String(ov.opacity);
|
||||
$('op-opacity-value').textContent = `${Math.round(ov.opacity * 100)}%`;
|
||||
}
|
||||
} else if (config.overlayMode === 'minify') {
|
||||
fullOverlaySettings.classList.remove('active');
|
||||
|
|
@ -515,31 +532,30 @@ export function updateUI() {
|
|||
|
||||
($('op-style-dots') as HTMLInputElement).checked = config.minifyStyle === 'dots';
|
||||
($('op-style-symbols') as HTMLInputElement).checked = config.minifyStyle === 'symbols';
|
||||
|
||||
|
||||
const layeringBtns = $('op-layering-btns');
|
||||
layeringBtns.innerHTML = '';
|
||||
const behindBtn = document.createElement('button');
|
||||
behindBtn.textContent = 'Behind';
|
||||
behindBtn.className = 'op-button' + (config.overlayMode === 'behind' ? ' active' : '');
|
||||
behindBtn.className = `op-button${config.overlayMode === 'behind' ? ' active' : ''}`;
|
||||
behindBtn.addEventListener('click', () => { config.overlayMode = 'behind'; saveConfig(['overlayMode']); ensureHook(); updateUI(); });
|
||||
const aboveBtn = document.createElement('button');
|
||||
aboveBtn.textContent = 'Above';
|
||||
aboveBtn.className = 'op-button' + (config.overlayMode === 'above' ? ' active' : '');
|
||||
aboveBtn.className = `op-button${config.overlayMode === 'above' ? ' active' : ''}`;
|
||||
aboveBtn.addEventListener('click', () => { config.overlayMode = 'above'; saveConfig(['overlayMode']); ensureHook(); updateUI(); });
|
||||
layeringBtns.appendChild(behindBtn);
|
||||
layeringBtns.appendChild(aboveBtn);
|
||||
|
||||
|
||||
// --- Positioning Section ---
|
||||
const autoBtn = $('op-autocap-toggle');
|
||||
const placeLabel = $('op-place-label');
|
||||
autoBtn.textContent = config.autoCapturePixelUrl ? 'Enabled' : 'Disabled';
|
||||
autoBtn.classList.toggle('op-danger', !!config.autoCapturePixelUrl);
|
||||
if(placeLabel) placeLabel.classList.toggle('op-danger-text', !!config.autoCapturePixelUrl);
|
||||
placeLabel?.classList.toggle('op-danger-text', !!config.autoCapturePixelUrl);
|
||||
|
||||
const positioningBody = $('op-positioning-body');
|
||||
const positioningBody: HTMLElement = $('op-positioning-body');
|
||||
const positioningCz = $('op-collapse-positioning');
|
||||
if(positioningBody) positioningBody.style.display = config.collapsePositioning ? 'none' : 'block';
|
||||
positioningBody.style.display = config.collapsePositioning ? 'none' : 'block';
|
||||
if (positioningCz) positioningCz.textContent = config.collapsePositioning ? '▸' : '▾';
|
||||
|
||||
const listWrap = $('op-list-wrap');
|
||||
|
|
@ -548,11 +564,11 @@ export function updateUI() {
|
|||
if (listCz) listCz.textContent = config.collapseList ? '▸' : '▾';
|
||||
|
||||
rebuildOverlayListUI();
|
||||
updateEditorUI();
|
||||
updateEditorUI(); // <- now exported, so noUnusedLocals won't flag it
|
||||
|
||||
const exportBtn = $('op-export-overlay') as HTMLButtonElement;
|
||||
const ov = getActiveOverlay();
|
||||
const canExport = !!(ov && ov.imageUrl && !ov.isLocal);
|
||||
const canExport = !!(ov?.imageUrl && !ov?.isLocal);
|
||||
exportBtn.disabled = !canExport;
|
||||
exportBtn.title = canExport ? 'Export active overlay JSON' : 'Export disabled for local images';
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="tampermonkey" />
|
||||
import { createCanvas, createHTMLCanvas, canvasToDataURLSafe, loadImage } from '../core/canvas';
|
||||
import { config, saveConfig } from '../core/store';
|
||||
import { saveConfig } from '../core/store';
|
||||
import { MAX_OVERLAY_DIM } from '../core/constants';
|
||||
import { ensureHook } from '../core/hook';
|
||||
import { clearOverlayCache } from '../core/cache';
|
||||
|
|
@ -291,10 +291,14 @@ export function buildRSModal() {
|
|||
closeBtn: modal.querySelector('#op-rs-close') as HTMLButtonElement,
|
||||
};
|
||||
|
||||
const ctxPrev = refs.preview.getContext('2d', { willReadFrequently: true })!;
|
||||
const ctxSimOrig = refs.simOrig.getContext('2d', { willReadFrequently: true })!;
|
||||
const ctxSimNew = refs.simNew.getContext('2d', { willReadFrequently: true })!;
|
||||
const ctxRes = refs.resCanvas.getContext('2d', { willReadFrequently: true })!;
|
||||
const ctxPrev = refs.preview.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctxPrev) throw new Error('Failed to get 2d context for preview canvas.');
|
||||
const ctxSimOrig = refs.simOrig.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctxSimOrig) throw new Error('Failed to get 2d context for simOrig canvas.');
|
||||
const ctxSimNew = refs.simNew.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctxSimNew) throw new Error('Failed to get 2d context for simNew canvas.');
|
||||
const ctxRes = refs.resCanvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctxRes) throw new Error('Failed to get 2d context for resCanvas.');
|
||||
|
||||
rs = {
|
||||
...refs,
|
||||
|
|
@ -398,8 +402,7 @@ export function buildRSModal() {
|
|||
}
|
||||
|
||||
function syncAdvancedMeta() {
|
||||
const { cols, rows } = sampleDims();
|
||||
const limit = (cols >= MAX_OVERLAY_DIM || rows >= MAX_OVERLAY_DIM);
|
||||
sampleDims();
|
||||
if (rs!.mode === 'advanced') {
|
||||
rs!.applyBtn.disabled = !rs!.calcReady;
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue