mirror of
https://github.com/ShinkoNet/Wplace-Overlay-Pro.git
synced 2026-05-17 01:42:21 +00:00
Merge c7a7028bc9 into 7c02beed51
This commit is contained in:
commit
83828b69a2
15 changed files with 1053 additions and 720 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -128,3 +128,5 @@ yarn/unplugged
|
||||||
yarn/build-state.yml
|
yarn/build-state.yml
|
||||||
yarn/install-state.gz
|
yarn/install-state.gz
|
||||||
pnp.*
|
pnp.*
|
||||||
|
.DS_Store
|
||||||
|
bun.lock
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ License: [`LICENSE.md`](LICENSE.md:1)
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick links
|
## Quick links
|
||||||
- Prebuilt userscript (install in Tampermonkey / Greasemonkey): [`dist/Wplace\ Overlay\ Pro.user.js`](dist/Wplace\ Overlay\ Pro.user.js:1)
|
- Prebuilt userscript (install in Tampermonkey / Greasemonkey): [Releases](https://github.com/Decryptu/Wplace-Overlay-Pro/releases)
|
||||||
- Build script (from source): see the `build` script in [`package.json`](package.json:7)
|
- Build script (from source): see the `build` script in [`package.json`](package.json:7)
|
||||||
- Development watch script: see `watch` in [`package.json`](package.json:8)
|
- Development watch script: see `watch` in [`package.json`](package.json:8)
|
||||||
- Important constants (limits): [`src/core/constants.ts`](src/core/constants.ts:1)
|
- Important constants (limits): [`src/core/constants.ts`](src/core/constants.ts:1)
|
||||||
|
|
@ -40,8 +40,8 @@ License: [`LICENSE.md`](LICENSE.md:1)
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Install a userscript manager such as Tampermonkey or Violentmonkey in your browser.
|
1. Install a userscript manager such as Tampermonkey or Violentmonkey in your browser.
|
||||||
2. Install the prebuilt userscript by opening the file produced by the build step and letting Tampermonkey import it:
|
2. Install the userscript in the [Releases](https://github.com/Decryptu/Wplace-Overlay-Pro/releases) page and letting Tampermonkey import it:
|
||||||
3. Build from source:
|
3. Or you can build from source:
|
||||||
- Install dependencies (npm or pnpm).
|
- Install dependencies (npm or pnpm).
|
||||||
- Run the build script defined in [`package.json`](package.json:7).
|
- Run the build script defined in [`package.json`](package.json:7).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const metaPath = resolve(__dirname, 'src', 'meta.js');
|
||||||
const MetaBannerPlugin = {
|
const MetaBannerPlugin = {
|
||||||
name: 'meta-banner',
|
name: 'meta-banner',
|
||||||
setup(build) {
|
setup(build) {
|
||||||
build.onEnd(async (result) => {
|
build.onEnd(async () => {
|
||||||
try {
|
try {
|
||||||
await mkdir(dirname(outFile), { recursive: true });
|
await mkdir(dirname(outFile), { recursive: true });
|
||||||
const [meta, js] = await Promise.all([
|
const [meta, js] = await Promise.all([
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { createUI, updateUI } from './ui/panel';
|
||||||
import { displayImageFromData } from './core/overlay';
|
import { displayImageFromData } from './core/overlay';
|
||||||
import { showToast } from './core/toast';
|
import { showToast } from './core/toast';
|
||||||
import { urlToDataURL } from './core/gm';
|
import { urlToDataURL } from './core/gm';
|
||||||
|
import { enablePaletteSymbols } from './core/palette-inject';
|
||||||
|
|
||||||
async function applyTemplateFromUrl() {
|
async function applyTemplateFromUrl() {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
@ -64,6 +65,7 @@ export async function bootstrapApp() {
|
||||||
createUI();
|
createUI();
|
||||||
setUpdateUI(() => updateUI());
|
setUpdateUI(() => updateUI());
|
||||||
ensureHook();
|
ensureHook();
|
||||||
|
enablePaletteSymbols();
|
||||||
await applyTemplateFromUrl();
|
await applyTemplateFromUrl();
|
||||||
console.log('Overlay Pro UI ready.');
|
console.log('Overlay Pro UI ready.');
|
||||||
}
|
}
|
||||||
|
|
@ -4,8 +4,11 @@ export class LRUCache<K=any, V=any> {
|
||||||
constructor(max = 400) { this.max = max; this.map = new Map(); }
|
constructor(max = 400) { this.max = max; this.map = new Map(); }
|
||||||
get(key: K): V | undefined {
|
get(key: K): V | undefined {
|
||||||
if (!this.map.has(key)) return undefined;
|
if (!this.map.has(key)) return undefined;
|
||||||
const val = this.map.get(key)!;
|
const val = this.map.get(key);
|
||||||
this.map.delete(key); this.map.set(key, val);
|
if (val !== undefined) {
|
||||||
|
this.map.delete(key);
|
||||||
|
this.map.set(key, val);
|
||||||
|
}
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
set(key: K, val: V) {
|
set(key: K, val: V) {
|
||||||
|
|
@ -25,11 +28,13 @@ export const imageDecodeCache = new LRUCache<string, HTMLImageElement>(64);
|
||||||
export const paletteDetectionCache = new LRUCache<string, boolean>(200);
|
export const paletteDetectionCache = new LRUCache<string, boolean>(200);
|
||||||
export const baseMinifyCache = new LRUCache<string, ImageData>(100);
|
export const baseMinifyCache = new LRUCache<string, ImageData>(100);
|
||||||
export const tooLargeOverlays = new Set<string>();
|
export const tooLargeOverlays = new Set<string>();
|
||||||
|
export const overlayImageDataCache = new LRUCache<string, ImageData>(100);
|
||||||
|
|
||||||
export function clearOverlayCache() {
|
export function clearOverlayCache() {
|
||||||
overlayCache.clear();
|
overlayCache.clear();
|
||||||
imageDecodeCache.clear();
|
imageDecodeCache.clear();
|
||||||
paletteDetectionCache.clear();
|
paletteDetectionCache.clear();
|
||||||
baseMinifyCache.clear();
|
baseMinifyCache.clear();
|
||||||
|
overlayImageDataCache.clear();
|
||||||
tooLargeOverlays.clear();
|
tooLargeOverlays.clear();
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +29,8 @@ export async function canvasToDataURLSafe(canvas: OffscreenCanvas | HTMLCanvasEl
|
||||||
const bmp = (canvas as any).transferToImageBitmap?.();
|
const bmp = (canvas as any).transferToImageBitmap?.();
|
||||||
if (bmp) {
|
if (bmp) {
|
||||||
const html = createHTMLCanvas(canvas.width, canvas.height);
|
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);
|
ctx.drawImage(bmp, 0, 0);
|
||||||
return html.toDataURL('image/png');
|
return html.toDataURL('image/png');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ export const EV_AUTOCAP_CHANGED = 'op-autocap-changed';
|
||||||
export function emit(name: string, detail?: any) {
|
export function emit(name: string, detail?: any) {
|
||||||
try {
|
try {
|
||||||
document.dispatchEvent(new CustomEvent(name, { detail }));
|
document.dispatchEvent(new CustomEvent(name, { detail }));
|
||||||
} catch (e) {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32,6 +32,16 @@ export function attachHook() {
|
||||||
const hookedFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
const hookedFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const urlStr = typeof input === 'string' ? input : ((input as Request).url) || '';
|
const urlStr = typeof input === 'string' ? input : ((input as Request).url) || '';
|
||||||
|
|
||||||
|
// EARLY FILTERING: Quick hostname check to skip non-wplace requests
|
||||||
|
if (!urlStr.includes('backend.wplace.live')) {
|
||||||
|
return originalFetch(input as any, init as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// EARLY FILTERING: Quick path check to skip non-tile/non-pixel requests
|
||||||
|
if (!urlStr.includes('/files/') && !urlStr.includes('/s0/pixel/')) {
|
||||||
|
return originalFetch(input as any, init as any);
|
||||||
|
}
|
||||||
|
|
||||||
// Anchor auto-capture: watch pixel endpoint, then store/normalize
|
// Anchor auto-capture: watch pixel endpoint, then store/normalize
|
||||||
if (config.autoCapturePixelUrl && config.activeOverlayId) {
|
if (config.autoCapturePixelUrl && config.activeOverlayId) {
|
||||||
const pixelMatch = matchPixelUrl(urlStr);
|
const pixelMatch = matchPixelUrl(urlStr);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,26 @@
|
||||||
import { createCanvas, canvasToBlob, blobToImage, loadImage } from './canvas';
|
import { createCanvas, canvasToBlob, blobToImage, loadImage } from './canvas';
|
||||||
import { MINIFY_SCALE, MINIFY_SCALE_SYMBOL, TILE_SIZE, MAX_OVERLAY_DIM } from './constants';
|
import { MINIFY_SCALE, MINIFY_SCALE_SYMBOL, TILE_SIZE, MAX_OVERLAY_DIM } from './constants';
|
||||||
import { imageDecodeCache, overlayCache, tooLargeOverlays, paletteDetectionCache, baseMinifyCache, clearOverlayCache } from './cache';
|
import { imageDecodeCache, overlayCache, tooLargeOverlays, paletteDetectionCache, baseMinifyCache, clearOverlayCache, overlayImageDataCache, LRUCache } from './cache';
|
||||||
import { showToast } from './toast';
|
import { showToast } from './toast';
|
||||||
import { config, saveConfig, type OverlayItem } from './store';
|
import { config, saveConfig, type OverlayItem } from './store';
|
||||||
import { WPLACE_FREE, WPLACE_PAID, SYMBOL_TILES, SYMBOL_W, SYMBOL_H } from './palette';
|
import { WPLACE_FREE, WPLACE_PAID, SYMBOL_TILES, SYMBOL_W, SYMBOL_H } from './palette';
|
||||||
import { getUpdateUI, ensureHook } from './hook';
|
import { getUpdateUI, ensureHook } from './hook';
|
||||||
|
|
||||||
|
// --- RLE Optimization: Types and Cache ---
|
||||||
|
type Run = { colorIndex: number; length: number };
|
||||||
|
type RLEData = Run[][];
|
||||||
|
const overlayRLECache = new LRUCache<string, RLEData>(50); // Cache for run-length encoded overlay data
|
||||||
|
|
||||||
const ALL_COLORS = [...WPLACE_FREE, ...WPLACE_PAID];
|
const ALL_COLORS = [...WPLACE_FREE, ...WPLACE_PAID];
|
||||||
const colorIndexMap = new Map<string, number>();
|
const colorIndexMap = new Map<string, number>();
|
||||||
ALL_COLORS.forEach((c, i) => colorIndexMap.set(c.join(','), i));
|
ALL_COLORS.forEach((c, i) => colorIndexMap.set(c.join(','), i));
|
||||||
|
|
||||||
const LUT_SIZE = 32; // 32x32x32 = 32KB
|
const LUT_SIZE = 32;
|
||||||
const LUT_SHIFT = 8 - Math.log2(LUT_SIZE); // 3 for 32x32x32
|
const LUT_SHIFT = 8 - Math.log2(LUT_SIZE);
|
||||||
const colorLUT = new Uint8Array(LUT_SIZE * LUT_SIZE * LUT_SIZE);
|
const colorLUT = new Uint8Array(LUT_SIZE * LUT_SIZE * LUT_SIZE);
|
||||||
|
const symbolCache = new Map<number, OffscreenCanvas | HTMLCanvasElement>();
|
||||||
|
|
||||||
|
// (buildColorLUT, findColorIndexLUT, findClosestColorIndex, getPreRenderedSymbol functions are unchanged)
|
||||||
function buildColorLUT() {
|
function buildColorLUT() {
|
||||||
for (let r = 0; r < LUT_SIZE; r++) {
|
for (let r = 0; r < LUT_SIZE; r++) {
|
||||||
for (let g = 0; g < LUT_SIZE; g++) {
|
for (let g = 0; g < LUT_SIZE; g++) {
|
||||||
|
|
@ -27,25 +34,21 @@ function buildColorLUT() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findColorIndexLUT(r: number, g: number, b: number): number {
|
function findColorIndexLUT(r: number, g: number, b: number): number {
|
||||||
const lutR = r >> LUT_SHIFT;
|
const lutR = r >> LUT_SHIFT;
|
||||||
const lutG = g >> LUT_SHIFT;
|
const lutG = g >> LUT_SHIFT;
|
||||||
const lutB = b >> LUT_SHIFT;
|
const lutB = b >> LUT_SHIFT;
|
||||||
return colorLUT[lutR * LUT_SIZE * LUT_SIZE + lutG * LUT_SIZE + lutB];
|
return colorLUT[lutR * LUT_SIZE * LUT_SIZE + lutG * LUT_SIZE + lutB];
|
||||||
}
|
}
|
||||||
|
|
||||||
buildColorLUT();
|
|
||||||
|
|
||||||
function findClosestColorIndex(r: number, g: number, b: number) {
|
function findClosestColorIndex(r: number, g: number, b: number) {
|
||||||
let minDistance = Infinity;
|
let minDistance = Infinity;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
for (let i = 0; i < ALL_COLORS.length; i++) {
|
for (let i = 0; i < ALL_COLORS.length; i++) {
|
||||||
const color = ALL_COLORS[i];
|
const color = ALL_COLORS[i];
|
||||||
const distance = Math.sqrt(
|
const distance = Math.sqrt(
|
||||||
Math.pow(r - color[0], 2) +
|
(r - color[0]) ** 2 +
|
||||||
Math.pow(g - color[1], 2) +
|
(g - color[1]) ** 2 +
|
||||||
Math.pow(b - color[2], 2)
|
(b - color[2]) ** 2
|
||||||
);
|
);
|
||||||
if (distance < minDistance) {
|
if (distance < minDistance) {
|
||||||
minDistance = distance;
|
minDistance = distance;
|
||||||
|
|
@ -54,6 +57,67 @@ function findClosestColorIndex(r: number, g: number, b: number) {
|
||||||
}
|
}
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
function getPreRenderedSymbol(colorIndex: number): OffscreenCanvas | HTMLCanvasElement | null {
|
||||||
|
const cachedSymbol = symbolCache.get(colorIndex);
|
||||||
|
if (cachedSymbol) {
|
||||||
|
return cachedSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorIndex >= SYMBOL_TILES.length) return null;
|
||||||
|
const scale = MINIFY_SCALE_SYMBOL;
|
||||||
|
const canvas = createCanvas(scale, scale);
|
||||||
|
const ctx = canvas.getContext('2d', { willReadFrequently: true }) as CanvasRenderingContext2D;
|
||||||
|
if (!ctx) return null;
|
||||||
|
const symbol = SYMBOL_TILES[colorIndex];
|
||||||
|
const paletteColor = ALL_COLORS[colorIndex];
|
||||||
|
ctx.fillStyle = `rgb(${paletteColor[0]}, ${paletteColor[1]}, ${paletteColor[2]})`;
|
||||||
|
const centerX = (scale - SYMBOL_W) >> 1;
|
||||||
|
const centerY = (scale - SYMBOL_H) >> 1;
|
||||||
|
for (let sy = 0; sy < SYMBOL_H; sy++) {
|
||||||
|
for (let sx = 0; sx < SYMBOL_W; sx++) {
|
||||||
|
const bit_idx = sy * SYMBOL_W + sx;
|
||||||
|
if ((symbol >>> bit_idx) & 1) {
|
||||||
|
ctx.fillRect(sx + centerX, sy + centerY, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
symbolCache.set(colorIndex, canvas);
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildColorLUT();
|
||||||
|
|
||||||
|
// --- RLE Optimization: Function to generate RLE data from ImageData ---
|
||||||
|
function generateRLEData(imageData: ImageData, isPalettePerfect: boolean): RLEData {
|
||||||
|
const { width, height, data } = imageData;
|
||||||
|
const rleData: RLEData = [];
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
const row: Run[] = [];
|
||||||
|
let currentRun: Run | null = null;
|
||||||
|
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const i = (y * width + x) * 4;
|
||||||
|
const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
|
||||||
|
|
||||||
|
// Treat transparent or #deface as "no color" (index -1)
|
||||||
|
const colorIndex = (a <= 128 || (r === 0xde && g === 0xfa && b === 0xce))
|
||||||
|
? -1
|
||||||
|
: (isPalettePerfect ? (colorIndexMap.get(`${r},${g},${b}`) ?? -1) : findColorIndexLUT(r, g, b));
|
||||||
|
|
||||||
|
if (currentRun && currentRun.colorIndex === colorIndex) {
|
||||||
|
currentRun.length++;
|
||||||
|
} else {
|
||||||
|
if (currentRun) row.push(currentRun);
|
||||||
|
currentRun = { colorIndex, length: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentRun) row.push(currentRun);
|
||||||
|
rleData.push(row);
|
||||||
|
}
|
||||||
|
return rleData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function extractPixelCoords(pixelUrl: string) {
|
export function extractPixelCoords(pixelUrl: string) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -70,7 +134,6 @@ export function extractPixelCoords(pixelUrl: string) {
|
||||||
return { chunk1: 0, chunk2: 0, posX: 0, posY: 0 };
|
return { chunk1: 0, chunk2: 0, posX: 0, posY: 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchTileUrl(urlStr: string) {
|
export function matchTileUrl(urlStr: string) {
|
||||||
try {
|
try {
|
||||||
const u = new URL(urlStr, location.href);
|
const u = new URL(urlStr, location.href);
|
||||||
|
|
@ -80,7 +143,6 @@ export function matchTileUrl(urlStr: string) {
|
||||||
return { chunk1: parseInt(m[1], 10), chunk2: parseInt(m[2], 10) };
|
return { chunk1: parseInt(m[1], 10), chunk2: parseInt(m[2], 10) };
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchPixelUrl(urlStr: string) {
|
export function matchPixelUrl(urlStr: string) {
|
||||||
try {
|
try {
|
||||||
const u = new URL(urlStr, location.href);
|
const u = new URL(urlStr, location.href);
|
||||||
|
|
@ -91,44 +153,35 @@ export function matchPixelUrl(urlStr: string) {
|
||||||
return { normalized: `https://backend.wplace.live/s0/pixel/${m[1]}/${m[2]}?x=${sp.get('x')||0}&y=${sp.get('y')||0}` };
|
return { normalized: `https://backend.wplace.live/s0/pixel/${m[1]}/${m[2]}?x=${sp.get('x')||0}&y=${sp.get('y')||0}` };
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rectIntersect(ax: number, ay: number, aw: number, ah: number, bx: number, by: number, bw: number, bh: number) {
|
export function rectIntersect(ax: number, ay: number, aw: number, ah: number, bx: number, by: number, bw: number, bh: number) {
|
||||||
const x = Math.max(ax, bx), y = Math.max(ay, by);
|
const x = Math.max(ax, bx), y = Math.max(ay, by);
|
||||||
const r = Math.min(ax + aw, bx + bw), b = Math.min(ay + ah, by + bh);
|
const r = Math.min(ax + aw, bx + bw), b = Math.min(ay + ah, by + bh);
|
||||||
const w = Math.max(0, r - x), h = Math.max(0, b - y);
|
const w = Math.max(0, r - x), h = Math.max(0, b - y);
|
||||||
return { x, y, w, h };
|
return { x, y, w, h };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPalettePerfectImage(img: HTMLImageElement): boolean {
|
function isPalettePerfectImage(img: HTMLImageElement): boolean {
|
||||||
const key = img.src;
|
const key = img.src;
|
||||||
const cached = paletteDetectionCache.get(key);
|
const cached = paletteDetectionCache.get(key);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
|
|
||||||
const canvas = createCanvas(img.width, img.height) as any;
|
const canvas = createCanvas(img.width, img.height) as any;
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
const ctx = canvas.getContext('2d', { willReadFrequently: true }) as CanvasRenderingContext2D;
|
||||||
|
if (!ctx) throw new Error('Failed to get 2D context');
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
|
const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
|
||||||
|
|
||||||
if (a === 0) continue;
|
if (a === 0) continue;
|
||||||
|
|
||||||
// Skip #deface transparency
|
|
||||||
if (r === 0xde && g === 0xfa && b === 0xce) continue;
|
if (r === 0xde && g === 0xfa && b === 0xce) continue;
|
||||||
|
|
||||||
const colorKey = `${r},${g},${b}`;
|
const colorKey = `${r},${g},${b}`;
|
||||||
if (!colorIndexMap.has(colorKey)) {
|
if (!colorIndexMap.has(colorKey)) {
|
||||||
paletteDetectionCache.set(key, false);
|
paletteDetectionCache.set(key, false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
paletteDetectionCache.set(key, true);
|
paletteDetectionCache.set(key, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decodeOverlayImage(imageBase64: string | null) {
|
export async function decodeOverlayImage(imageBase64: string | null) {
|
||||||
if (!imageBase64) return null;
|
if (!imageBase64) return null;
|
||||||
const key = imageBase64;
|
const key = imageBase64;
|
||||||
|
|
@ -138,7 +191,6 @@ export async function decodeOverlayImage(imageBase64: string | null) {
|
||||||
imageDecodeCache.set(key, img);
|
imageDecodeCache.set(key, img);
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function overlaySignature(ov: {
|
export function overlaySignature(ov: {
|
||||||
imageBase64: string | null,
|
imageBase64: string | null,
|
||||||
pixelUrl: string | null,
|
pixelUrl: string | null,
|
||||||
|
|
@ -146,10 +198,23 @@ export function overlaySignature(ov: {
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
opacity: number,
|
opacity: number,
|
||||||
}, isPalettePerfect?: boolean) {
|
}, isPalettePerfect?: boolean) {
|
||||||
const imgKey = ov.imageBase64 ? ov.imageBase64.slice(0, 64) + ':' + ov.imageBase64.length : 'none';
|
const imgKey = ov.imageBase64 ? `${ov.imageBase64.slice(0, 64)}:${ov.imageBase64.length}` : 'none';
|
||||||
const perfectFlag = isPalettePerfect !== undefined ? (isPalettePerfect ? 'P' : 'I') : 'U';
|
const perfectFlag = isPalettePerfect !== undefined ? (isPalettePerfect ? 'P' : 'I') : 'U';
|
||||||
return [imgKey, ov.pixelUrl || 'null', ov.offsetX, ov.offsetY, ov.opacity, perfectFlag].join('|');
|
return [imgKey, ov.pixelUrl || 'null', ov.offsetX, ov.offsetY, ov.opacity, perfectFlag].join('|');
|
||||||
}
|
}
|
||||||
|
async function getCachedOverlayImageData(img: HTMLImageElement, overlayId: string): Promise<ImageData> {
|
||||||
|
const cacheKey = `${overlayId}:${img.width}x${img.height}`;
|
||||||
|
const cached = overlayImageDataCache.get(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
const canvas = createCanvas(img.width, img.height) as any;
|
||||||
|
const ctx = canvas.getContext('2d', { willReadFrequently: true }) as CanvasRenderingContext2D;
|
||||||
|
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);
|
||||||
|
overlayImageDataCache.set(cacheKey, imageData);
|
||||||
|
return imageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function buildOverlayDataForChunkUnified(
|
export async function buildOverlayDataForChunkUnified(
|
||||||
ov: {
|
ov: {
|
||||||
|
|
@ -180,185 +245,113 @@ export async function buildOverlayDataForChunkUnified(
|
||||||
const drawX = (base.chunk1 * TILE_SIZE + base.posX + ov.offsetX) - (targetChunk1 * TILE_SIZE);
|
const drawX = (base.chunk1 * TILE_SIZE + base.posX + ov.offsetX) - (targetChunk1 * TILE_SIZE);
|
||||||
const drawY = (base.chunk2 * TILE_SIZE + base.posY + ov.offsetY) - (targetChunk2 * TILE_SIZE);
|
const drawY = (base.chunk2 * TILE_SIZE + base.posY + ov.offsetY) - (targetChunk2 * TILE_SIZE);
|
||||||
|
|
||||||
// Check if image is palette-perfect for optimization
|
|
||||||
const isPalettePerfect = isPalettePerfectImage(img);
|
const isPalettePerfect = isPalettePerfectImage(img);
|
||||||
const sig = overlaySignature(ov, isPalettePerfect);
|
const sig = overlaySignature(ov, isPalettePerfect);
|
||||||
const cacheKey = `ov:${ov.id}|sig:${sig}|tile:${targetChunk1},${targetChunk2}|mode:${mode}`;
|
const cacheKey = `ov:${ov.id}|sig:${sig}|tile:${targetChunk1},${targetChunk2}|mode:${mode}:${config.minifyStyle}`;
|
||||||
const cached = overlayCache.get(cacheKey);
|
const cached = overlayCache.get(cacheKey);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
|
|
||||||
const colorStrength = (mode === 'minify') ? 1.0 : ov.opacity;
|
|
||||||
const whiteStrength = 1 - colorStrength;
|
|
||||||
|
|
||||||
if (mode !== 'minify') {
|
if (mode !== 'minify') {
|
||||||
|
// This part for 'behind' and 'above' modes is unchanged
|
||||||
const isect = rectIntersect(0, 0, TILE_SIZE, TILE_SIZE, drawX, drawY, wImg, hImg);
|
const isect = rectIntersect(0, 0, TILE_SIZE, TILE_SIZE, drawX, drawY, wImg, hImg);
|
||||||
if (isect.w === 0 || isect.h === 0) { overlayCache.set(cacheKey, null); return null; }
|
if (isect.w === 0 || isect.h === 0) { overlayCache.set(cacheKey, null); return null; }
|
||||||
|
const canvas = createCanvas(isect.w, isect.h) as any;
|
||||||
const canvas = createCanvas(TILE_SIZE, TILE_SIZE) as any;
|
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
if (!ctx) throw new Error('Failed to get 2D context');
|
||||||
ctx.drawImage(img as any, drawX, drawY);
|
ctx.drawImage(img as any, isect.x - drawX, isect.y - drawY, isect.w, isect.h, 0, 0, isect.w, isect.h);
|
||||||
|
ctx.globalCompositeOperation = 'source-in';
|
||||||
const imageData = ctx.getImageData(isect.x, isect.y, isect.w, isect.h);
|
ctx.fillStyle = `rgba(255, 255, 255, ${1 - ov.opacity})`;
|
||||||
const data = imageData.data;
|
ctx.fillRect(0, 0, isect.w, isect.h);
|
||||||
|
ctx.globalCompositeOperation = 'destination-over';
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
ctx.fillStyle = `rgba(255, 255, 255, 1)`;
|
||||||
const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
|
ctx.fillRect(0, 0, isect.w, isect.h);
|
||||||
// Special case for #deface color
|
const imageData = ctx.getImageData(0, 0, isect.w, isect.h);
|
||||||
if (r === 0xde && g === 0xfa && b === 0xce) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (a > 0) {
|
|
||||||
data[i] = Math.round(r * colorStrength + 255 * whiteStrength);
|
|
||||||
data[i + 1] = Math.round(g * colorStrength + 255 * whiteStrength);
|
|
||||||
data[i + 2] = Math.round(b * colorStrength + 255 * whiteStrength);
|
|
||||||
data[i + 3] = 255;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = { imageData, dx: isect.x, dy: isect.y, scaled: false };
|
const result = { imageData, dx: isect.x, dy: isect.y, scaled: false };
|
||||||
overlayCache.set(cacheKey, result);
|
overlayCache.set(cacheKey, result);
|
||||||
return result;
|
return result;
|
||||||
} else {
|
|
||||||
if (config.minifyStyle === 'symbols') {
|
|
||||||
const scale = MINIFY_SCALE_SYMBOL;
|
|
||||||
const tileW = TILE_SIZE * scale;
|
|
||||||
const tileH = TILE_SIZE * scale;
|
|
||||||
const drawXScaled = Math.round(drawX * scale);
|
|
||||||
const drawYScaled = Math.round(drawY * scale);
|
|
||||||
const wScaled = wImg * scale;
|
|
||||||
const hScaled = hImg * scale;
|
|
||||||
|
|
||||||
const isect = rectIntersect(0, 0, tileW, tileH, drawXScaled, drawYScaled, wScaled, hScaled);
|
} else { // --- RLE Optimized Minify Rendering ---
|
||||||
if (isect.w === 0 || isect.h === 0) { overlayCache.set(cacheKey, null); return null; }
|
const scale = config.minifyStyle === 'symbols' ? MINIFY_SCALE_SYMBOL : MINIFY_SCALE;
|
||||||
|
const tileScaledW = TILE_SIZE * scale;
|
||||||
|
const tileScaledH = TILE_SIZE * scale;
|
||||||
|
const drawXScaled = Math.round(drawX * scale);
|
||||||
|
const drawYScaled = Math.round(drawY * scale);
|
||||||
|
const wImgScaled = wImg * scale;
|
||||||
|
const hImgScaled = hImg * scale;
|
||||||
|
|
||||||
const canvas = createCanvas(wImg, hImg) as any;
|
const isect = rectIntersect(0, 0, tileScaledW, tileScaledH, drawXScaled, drawYScaled, wImgScaled, hImgScaled);
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
if (isect.w === 0 || isect.h === 0) { overlayCache.set(cacheKey, null); return null; }
|
||||||
ctx.imageSmoothingEnabled = false;
|
|
||||||
ctx.drawImage(img as any, 0, 0);
|
|
||||||
const originalImageData = ctx.getImageData(0, 0, wImg, hImg);
|
|
||||||
|
|
||||||
const outCanvas = createCanvas(tileW, tileH) as any;
|
// Get or generate RLE data for the overlay
|
||||||
const outCtx = outCanvas.getContext('2d', { willReadFrequently: true })!;
|
const rleCacheKey = `${ov.id}:${sig}`;
|
||||||
const outputImageData = outCtx.createImageData(tileW, tileH);
|
let rleData = overlayRLECache.get(rleCacheKey);
|
||||||
const outData = outputImageData.data;
|
if (!rleData) {
|
||||||
|
const originalImageData = await getCachedOverlayImageData(img, ov.id);
|
||||||
// Precompute symbol centering offsets for performance
|
rleData = generateRLEData(originalImageData, isPalettePerfect);
|
||||||
const centerX = (scale - SYMBOL_W) >> 1;
|
overlayRLECache.set(rleCacheKey, rleData);
|
||||||
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;
|
|
||||||
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];
|
|
||||||
|
|
||||||
// Early exit for transparent or deface pixels
|
|
||||||
if (a <= 128 || (r === 0xde && g === 0xfa && b === 0xce)) continue;
|
|
||||||
|
|
||||||
let colorIndex: number;
|
|
||||||
|
|
||||||
// Fast path for palette-perfect images
|
|
||||||
if (isPalettePerfect) {
|
|
||||||
const colorKey = `${r},${g},${b}`;
|
|
||||||
colorIndex = colorIndexMap.get(colorKey) ?? 0;
|
|
||||||
} else {
|
|
||||||
// Use LUT for fast color matching
|
|
||||||
colorIndex = findColorIndexLUT(r, g, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 };
|
|
||||||
overlayCache.set(cacheKey, result);
|
|
||||||
return result;
|
|
||||||
} else { // 'dots'
|
|
||||||
const scale = MINIFY_SCALE;
|
|
||||||
const tileW = TILE_SIZE * scale;
|
|
||||||
const tileH = TILE_SIZE * scale;
|
|
||||||
const drawXScaled = Math.round(drawX * scale);
|
|
||||||
const drawYScaled = Math.round(drawY * scale);
|
|
||||||
const wScaled = wImg * scale;
|
|
||||||
const hScaled = hImg * scale;
|
|
||||||
|
|
||||||
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(tileW, tileH) as any;
|
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
|
||||||
ctx.imageSmoothingEnabled = false;
|
|
||||||
ctx.clearRect(0, 0, tileW, tileH);
|
|
||||||
ctx.drawImage(img as any, 0, 0, wImg, hImg, drawXScaled, drawYScaled, wScaled, hScaled);
|
|
||||||
|
|
||||||
const imageData = ctx.getImageData(isect.x, isect.y, isect.w, isect.h);
|
|
||||||
const data = imageData.data;
|
|
||||||
const center = Math.floor(scale / 2);
|
|
||||||
const width = isect.w;
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
|
||||||
const a = data[i + 3];
|
|
||||||
if (a === 0) continue;
|
|
||||||
|
|
||||||
const px = (i / 4) % width;
|
|
||||||
const py = Math.floor((i / 4) / width);
|
|
||||||
const absX = isect.x + px;
|
|
||||||
const absY = isect.y + py;
|
|
||||||
|
|
||||||
if ((absX % scale) === center && (absY % scale) === center) {
|
|
||||||
data[i] = Math.round(data[i] * colorStrength + 255 * whiteStrength);
|
|
||||||
data[i + 1] = Math.round(data[i + 1] * colorStrength + 255 * whiteStrength);
|
|
||||||
data[i + 2] = Math.round(data[i + 2] * colorStrength + 255 * whiteStrength);
|
|
||||||
data[i + 3] = 255;
|
|
||||||
} else {
|
|
||||||
data[i] = 0; data[i + 1] = 0; data[i + 2] = 0; data[i + 3] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const result = { imageData, dx: isect.x, dy: isect.y, scaled: true, scale };
|
|
||||||
overlayCache.set(cacheKey, result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const outCanvas = createCanvas(isect.w, isect.h) as any;
|
||||||
|
const outCtx = outCanvas.getContext('2d') as CanvasRenderingContext2D;
|
||||||
|
if (!outCtx) throw new Error('Failed to get 2D context');
|
||||||
|
|
||||||
|
// Calculate the range of source image rows to iterate over
|
||||||
|
const startY = Math.max(0, Math.floor((isect.y - drawYScaled) / scale));
|
||||||
|
const endY = Math.min(hImg, Math.ceil((isect.y + isect.h - drawYScaled) / scale));
|
||||||
|
|
||||||
|
for (let y = startY; y < endY; y++) {
|
||||||
|
const rowRLE = rleData[y];
|
||||||
|
let currentX = 0;
|
||||||
|
const outY = drawYScaled + y * scale - isect.y;
|
||||||
|
|
||||||
|
for (const run of rowRLE) {
|
||||||
|
if (run.colorIndex !== -1) {
|
||||||
|
const runStartImgX = currentX;
|
||||||
|
const runEndImgX = currentX + run.length;
|
||||||
|
|
||||||
|
// Find intersection of this run with the visible tile area
|
||||||
|
const visibleStartImgX = Math.max(runStartImgX, Math.floor((isect.x - drawXScaled) / scale));
|
||||||
|
const visibleEndImgX = Math.min(runEndImgX, Math.ceil((isect.x + isect.w - drawXScaled) / scale));
|
||||||
|
|
||||||
|
if (visibleEndImgX > visibleStartImgX) {
|
||||||
|
const visibleLength = visibleEndImgX - visibleStartImgX;
|
||||||
|
const outX = drawXScaled + visibleStartImgX * scale - isect.x;
|
||||||
|
|
||||||
|
if (config.minifyStyle === 'symbols') {
|
||||||
|
const symbolCanvas = getPreRenderedSymbol(run.colorIndex);
|
||||||
|
if (symbolCanvas) {
|
||||||
|
const pattern = outCtx.createPattern(symbolCanvas, 'repeat');
|
||||||
|
if (pattern) {
|
||||||
|
outCtx.fillStyle = pattern;
|
||||||
|
// We need to translate the context so the pattern aligns with the grid
|
||||||
|
outCtx.save();
|
||||||
|
outCtx.translate(outX, outY);
|
||||||
|
outCtx.fillRect(0, 0, visibleLength * scale, scale);
|
||||||
|
outCtx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // 'dots'
|
||||||
|
const color = ALL_COLORS[run.colorIndex];
|
||||||
|
outCtx.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
|
||||||
|
const center = Math.floor(scale / 2);
|
||||||
|
for (let i = 0; i < visibleLength; i++) {
|
||||||
|
outCtx.fillRect(outX + i * scale + center, outY + center, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentX += run.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageData = outCtx.getImageData(0, 0, isect.w, isect.h);
|
||||||
|
const result = { imageData, dx: isect.x, dy: isect.y, scaled: true, scale };
|
||||||
|
overlayCache.set(cacheKey, result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// composeTileUnified is unchanged
|
||||||
export async function composeTileUnified(
|
export async function composeTileUnified(
|
||||||
originalBlob: Blob,
|
originalBlob: Blob,
|
||||||
overlayDatas: Array<{ imageData: ImageData, dx: number, dy: number, scaled?: boolean } | null>,
|
overlayDatas: Array<{ imageData: ImageData, dx: number, dy: number, scaled?: boolean } | null>,
|
||||||
|
|
@ -366,69 +359,59 @@ export async function composeTileUnified(
|
||||||
) {
|
) {
|
||||||
if (!overlayDatas || overlayDatas.length === 0) return originalBlob;
|
if (!overlayDatas || overlayDatas.length === 0) return originalBlob;
|
||||||
const originalImage = await blobToImage(originalBlob) as any;
|
const originalImage = await blobToImage(originalBlob) as any;
|
||||||
|
|
||||||
if (mode === 'minify') {
|
if (mode === 'minify') {
|
||||||
const scale = config.minifyStyle === 'symbols' ? MINIFY_SCALE_SYMBOL : MINIFY_SCALE;
|
const scale = config.minifyStyle === 'symbols' ? MINIFY_SCALE_SYMBOL : MINIFY_SCALE;
|
||||||
const w = originalImage.width, h = originalImage.height;
|
const w = originalImage.width, h = originalImage.height;
|
||||||
|
|
||||||
const arrayBuffer = await originalBlob.arrayBuffer();
|
const arrayBuffer = await originalBlob.arrayBuffer();
|
||||||
const view = new DataView(arrayBuffer);
|
const view = new DataView(arrayBuffer);
|
||||||
const hash = view.getUint32(0, true) ^ view.getUint32(view.byteLength - 4, true);
|
const hash = view.getUint32(0, true) ^ view.getUint32(view.byteLength - 4, true);
|
||||||
|
|
||||||
const baseCacheKey = `base:${originalBlob.size}:${hash}:${w}x${h}:${scale}:${config.minifyStyle}`;
|
const baseCacheKey = `base:${originalBlob.size}:${hash}:${w}x${h}:${scale}:${config.minifyStyle}`;
|
||||||
let scaledBaseImageData = baseMinifyCache.get(baseCacheKey);
|
let scaledBaseImageData = baseMinifyCache.get(baseCacheKey);
|
||||||
|
|
||||||
if (!scaledBaseImageData) {
|
if (!scaledBaseImageData) {
|
||||||
const baseCanvas = createCanvas(w * scale, h * scale) as any;
|
const baseCanvas = createCanvas(w * scale, h * scale) as any;
|
||||||
const baseCtx = baseCanvas.getContext('2d', { willReadFrequently: true })!;
|
const baseCtx = baseCanvas.getContext('2d', { willReadFrequently: true }) as CanvasRenderingContext2D;
|
||||||
|
if (!baseCtx) throw new Error('Failed to get 2D context');
|
||||||
baseCtx.imageSmoothingEnabled = false;
|
baseCtx.imageSmoothingEnabled = false;
|
||||||
baseCtx.drawImage(originalImage, 0, 0, w * scale, h * scale);
|
baseCtx.drawImage(originalImage, 0, 0, w * scale, h * scale);
|
||||||
scaledBaseImageData = baseCtx.getImageData(0, 0, w * scale, h * scale);
|
scaledBaseImageData = baseCtx.getImageData(0, 0, w * scale, h * scale);
|
||||||
baseMinifyCache.set(baseCacheKey, scaledBaseImageData);
|
baseMinifyCache.set(baseCacheKey, scaledBaseImageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas = createCanvas(w * scale, h * scale) as any;
|
const canvas = createCanvas(w * scale, h * scale) as any;
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
const ctx = canvas.getContext('2d', { willReadFrequently: true }) as CanvasRenderingContext2D;
|
||||||
|
if (!ctx) throw new Error('Failed to get 2D context');
|
||||||
ctx.putImageData(scaledBaseImageData, 0, 0);
|
ctx.putImageData(scaledBaseImageData, 0, 0);
|
||||||
|
|
||||||
for (const ovd of overlayDatas) {
|
for (const ovd of overlayDatas) {
|
||||||
if (!ovd) continue;
|
if (!ovd) continue;
|
||||||
const tw = ovd.imageData.width;
|
const temp = createCanvas(ovd.imageData.width, ovd.imageData.height) as any;
|
||||||
const th = ovd.imageData.height;
|
const tctx = temp.getContext('2d') as CanvasRenderingContext2D;
|
||||||
if (!tw || !th) continue;
|
if (!tctx) continue;
|
||||||
const temp = createCanvas(tw, th) as any;
|
|
||||||
const tctx = temp.getContext('2d', { willReadFrequently: true })!;
|
|
||||||
tctx.putImageData(ovd.imageData, 0, 0);
|
tctx.putImageData(ovd.imageData, 0, 0);
|
||||||
ctx.drawImage(temp, ovd.dx, ovd.dy);
|
ctx.drawImage(temp, ovd.dx, ovd.dy);
|
||||||
}
|
}
|
||||||
return await canvasToBlob(canvas);
|
return await canvasToBlob(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = originalImage.width, h = originalImage.height;
|
const w = originalImage.width, h = originalImage.height;
|
||||||
const canvas = createCanvas(w, h) as any;
|
const canvas = createCanvas(w, h) as any;
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||||
|
if (!ctx) throw new Error('Failed to get 2D context');
|
||||||
|
const drawOverlays = () => {
|
||||||
|
for (const ovd of overlayDatas) {
|
||||||
|
if (!ovd) continue;
|
||||||
|
const temp = createCanvas(ovd.imageData.width, ovd.imageData.height) as any;
|
||||||
|
const tctx = temp.getContext('2d') as CanvasRenderingContext2D;
|
||||||
|
if (!tctx) continue;
|
||||||
|
tctx.putImageData(ovd.imageData, 0, 0);
|
||||||
|
ctx.drawImage(temp, ovd.dx, ovd.dy);
|
||||||
|
}
|
||||||
|
};
|
||||||
if (mode === 'behind') {
|
if (mode === 'behind') {
|
||||||
for (const ovd of overlayDatas) {
|
drawOverlays();
|
||||||
if (!ovd) continue;
|
|
||||||
const temp = createCanvas(ovd.imageData.width, ovd.imageData.height) as any;
|
|
||||||
const tctx = temp.getContext('2d', { willReadFrequently: true })!;
|
|
||||||
tctx.putImageData(ovd.imageData, 0, 0);
|
|
||||||
ctx.drawImage(temp, ovd.dx, ovd.dy);
|
|
||||||
}
|
|
||||||
ctx.drawImage(originalImage, 0, 0);
|
ctx.drawImage(originalImage, 0, 0);
|
||||||
return await canvasToBlob(canvas);
|
} else { // 'above'
|
||||||
} else {
|
|
||||||
ctx.drawImage(originalImage, 0, 0);
|
ctx.drawImage(originalImage, 0, 0);
|
||||||
for (const ovd of overlayDatas) {
|
drawOverlays();
|
||||||
if (!ovd) continue;
|
|
||||||
const temp = createCanvas(ovd.imageData.width, ovd.imageData.height) as any;
|
|
||||||
const tctx = temp.getContext('2d', { willReadFrequently: true })!;
|
|
||||||
tctx.putImageData(ovd.imageData, 0, 0);
|
|
||||||
ctx.drawImage(temp, ovd.dx, ovd.dy);
|
|
||||||
}
|
|
||||||
return await canvasToBlob(canvas);
|
|
||||||
}
|
}
|
||||||
|
return await canvasToBlob(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function displayImageFromData(newOverlay: OverlayItem) {
|
export async function displayImageFromData(newOverlay: OverlayItem) {
|
||||||
|
|
@ -439,6 +422,8 @@ export async function displayImageFromData(newOverlay: OverlayItem) {
|
||||||
await saveConfig();
|
await saveConfig();
|
||||||
|
|
||||||
clearOverlayCache();
|
clearOverlayCache();
|
||||||
|
overlayRLECache.clear();
|
||||||
|
symbolCache.clear();
|
||||||
ensureHook();
|
ensureHook();
|
||||||
|
|
||||||
const updateUI = getUpdateUI();
|
const updateUI = getUpdateUI();
|
||||||
|
|
|
||||||
195
src/core/palette-inject.ts
Normal file
195
src/core/palette-inject.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
import { WPLACE_FREE, WPLACE_PAID, SYMBOL_TILES, SYMBOL_W, SYMBOL_H } from './palette';
|
||||||
|
import { config } from './store';
|
||||||
|
|
||||||
|
const ALL_COLORS = [...WPLACE_FREE, ...WPLACE_PAID];
|
||||||
|
let paletteObserver: MutationObserver | null = null;
|
||||||
|
let isInjected = false;
|
||||||
|
|
||||||
|
function createSymbolCanvas(colorIndex: number, _bgColor: string): HTMLCanvasElement {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 20;
|
||||||
|
canvas.height = 20;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) throw new Error('Failed to get 2D context');
|
||||||
|
|
||||||
|
// Clear with transparency
|
||||||
|
ctx.clearRect(0, 0, 20, 20);
|
||||||
|
|
||||||
|
if (colorIndex < SYMBOL_TILES.length) {
|
||||||
|
const symbol = SYMBOL_TILES[colorIndex];
|
||||||
|
const scale = 3;
|
||||||
|
const offsetX = Math.floor((20 - SYMBOL_W * scale) / 2);
|
||||||
|
const offsetY = Math.floor((20 - SYMBOL_H * scale) / 2);
|
||||||
|
|
||||||
|
// Draw white outline first (slightly larger)
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
for (let y = 0; y < SYMBOL_H; y++) {
|
||||||
|
for (let x = 0; x < SYMBOL_W; x++) {
|
||||||
|
const bitIndex = y * SYMBOL_W + x;
|
||||||
|
const bit = (symbol >>> bitIndex) & 1;
|
||||||
|
|
||||||
|
if (bit) {
|
||||||
|
ctx.fillRect(
|
||||||
|
offsetX + x * scale - 1,
|
||||||
|
offsetY + y * scale - 1,
|
||||||
|
scale + 2,
|
||||||
|
scale + 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw black symbol on top
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
for (let y = 0; y < SYMBOL_H; y++) {
|
||||||
|
for (let x = 0; x < SYMBOL_W; x++) {
|
||||||
|
const bitIndex = y * SYMBOL_W + x;
|
||||||
|
const bit = (symbol >>> bitIndex) & 1;
|
||||||
|
|
||||||
|
if (bit) {
|
||||||
|
ctx.fillRect(
|
||||||
|
offsetX + x * scale,
|
||||||
|
offsetY + y * scale,
|
||||||
|
scale,
|
||||||
|
scale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorFromButton(button: HTMLElement): string | null {
|
||||||
|
const style = button.getAttribute('style');
|
||||||
|
if (!style) return null;
|
||||||
|
|
||||||
|
const match = style.match(/background:\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
return `${match[1]},${match[2]},${match[3]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findColorIndex(colorKey: string): number {
|
||||||
|
return ALL_COLORS.findIndex(([r, g, b]) => `${r},${g},${b}` === colorKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectSymbolsIntoPalette() {
|
||||||
|
if (isInjected) return;
|
||||||
|
|
||||||
|
// Find all color buttons in the palette
|
||||||
|
const colorButtons = document.querySelectorAll('button[id^="color-"]');
|
||||||
|
|
||||||
|
if (colorButtons.length === 0) return;
|
||||||
|
|
||||||
|
colorButtons.forEach((button) => {
|
||||||
|
const htmlButton = button as HTMLElement;
|
||||||
|
const colorKey = getColorFromButton(htmlButton);
|
||||||
|
|
||||||
|
if (!colorKey) return;
|
||||||
|
|
||||||
|
const colorIndex = findColorIndex(colorKey);
|
||||||
|
if (colorIndex === -1) return;
|
||||||
|
|
||||||
|
// Check if symbol already exists
|
||||||
|
if (htmlButton.querySelector('.symbol-overlay')) return;
|
||||||
|
|
||||||
|
// Create symbol overlay
|
||||||
|
const symbolCanvas = createSymbolCanvas(colorIndex, colorKey);
|
||||||
|
symbolCanvas.className = 'symbol-overlay';
|
||||||
|
symbolCanvas.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 0.9;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Make button position relative if it isn't already
|
||||||
|
const computedStyle = window.getComputedStyle(htmlButton);
|
||||||
|
if (computedStyle.position === 'static') {
|
||||||
|
htmlButton.style.position = 'relative';
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlButton.appendChild(symbolCanvas);
|
||||||
|
});
|
||||||
|
|
||||||
|
isInjected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupSymbols() {
|
||||||
|
const symbols = document.querySelectorAll('.symbol-overlay');
|
||||||
|
symbols.forEach(symbol => symbol.remove());
|
||||||
|
isInjected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPaletteWatcher() {
|
||||||
|
if (paletteObserver) return;
|
||||||
|
|
||||||
|
paletteObserver = new MutationObserver((mutations) => {
|
||||||
|
let shouldCheck = false;
|
||||||
|
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.type === 'childList') {
|
||||||
|
// Check if palette was added or removed
|
||||||
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const element = node as Element;
|
||||||
|
if (element.querySelector('button[id^="color-"]') ||
|
||||||
|
element.matches('button[id^="color-"]')) {
|
||||||
|
shouldCheck = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mutation.removedNodes.forEach((node) => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const element = node as Element;
|
||||||
|
if (element.querySelector('.symbol-overlay') ||
|
||||||
|
element.matches('.symbol-overlay')) {
|
||||||
|
isInjected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldCheck && config.overlayMode === 'minify' && config.minifyStyle === 'symbols') {
|
||||||
|
// Small delay to ensure DOM is fully updated
|
||||||
|
setTimeout(injectSymbolsIntoPalette, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paletteObserver.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enablePaletteSymbols() {
|
||||||
|
startPaletteWatcher();
|
||||||
|
|
||||||
|
// Try to inject immediately if palette is already open
|
||||||
|
if (config.overlayMode === 'minify' && config.minifyStyle === 'symbols') {
|
||||||
|
setTimeout(injectSymbolsIntoPalette, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disablePaletteSymbols() {
|
||||||
|
if (paletteObserver) {
|
||||||
|
paletteObserver.disconnect();
|
||||||
|
paletteObserver = null;
|
||||||
|
}
|
||||||
|
cleanupSymbols();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePaletteSymbols() {
|
||||||
|
cleanupSymbols();
|
||||||
|
|
||||||
|
if (config.overlayMode === 'minify' && config.minifyStyle === 'symbols') {
|
||||||
|
setTimeout(injectSymbolsIntoPalette, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,13 +3,11 @@
|
||||||
|
|
||||||
import { bootstrapApp } from "./app";
|
import { bootstrapApp } from "./app";
|
||||||
|
|
||||||
(function () {
|
(() => {
|
||||||
"use strict";
|
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
bootstrapApp().catch((e) =>
|
bootstrapApp().catch((e) =>
|
||||||
console.error("Overlay Pro bootstrap failed", e),
|
console.error("Overlay Pro bootstrap failed", e),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
export {};
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Wplace Overlay Pro
|
// @name Wplace Overlay Pro
|
||||||
// @namespace http://tampermonkey.net/
|
// @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.
|
// @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
|
// @author shinkonet
|
||||||
// @match https://wplace.live/*
|
// @match https://wplace.live/*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/// <reference types="tampermonkey" />
|
/// <reference types="tampermonkey" />
|
||||||
import { WPLACE_FREE, WPLACE_PAID, WPLACE_NAMES, DEFAULT_FREE_KEYS } from '../core/palette';
|
import { WPLACE_FREE, WPLACE_PAID, WPLACE_NAMES, DEFAULT_FREE_KEYS } from '../core/palette';
|
||||||
import { createCanvas } from '../core/canvas';
|
|
||||||
import { config, saveConfig } from '../core/store';
|
import { config, saveConfig } from '../core/store';
|
||||||
import { MAX_OVERLAY_DIM } from '../core/constants';
|
import { MAX_OVERLAY_DIM } from '../core/constants';
|
||||||
import { ensureHook } from '../core/hook';
|
import { ensureHook } from '../core/hook';
|
||||||
|
|
@ -107,7 +106,8 @@ export function buildCCModal() {
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
const previewCanvas = modal.querySelector('#op-cc-preview') as HTMLCanvasElement;
|
const previewCanvas = modal.querySelector('#op-cc-preview') as HTMLCanvasElement;
|
||||||
const previewCtx = previewCanvas.getContext('2d', { willReadFrequently: true })!;
|
const previewCtx = previewCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
if (!previewCtx) return;
|
||||||
|
|
||||||
cc = {
|
cc = {
|
||||||
backdrop,
|
backdrop,
|
||||||
|
|
@ -142,40 +142,46 @@ export function buildCCModal() {
|
||||||
isStale: false
|
isStale: false
|
||||||
};
|
};
|
||||||
|
|
||||||
modal.querySelector('#op-cc-close')!.addEventListener('click', closeCCModal);
|
const closeBtn = modal.querySelector('#op-cc-close') as HTMLButtonElement | null;
|
||||||
|
closeBtn?.addEventListener('click', closeCCModal);
|
||||||
backdrop.addEventListener('click', closeCCModal);
|
backdrop.addEventListener('click', closeCCModal);
|
||||||
modal.querySelector('#op-cc-cancel')!.addEventListener('click', closeCCModal);
|
const cancelBtn = modal.querySelector('#op-cc-cancel') as HTMLButtonElement | null;
|
||||||
|
cancelBtn?.addEventListener('click', closeCCModal);
|
||||||
|
|
||||||
const zoomIn = async () => {
|
const zoomIn = async () => {
|
||||||
cc!.zoom = Math.min(8, (cc!.zoom || 1) * 1.25);
|
if (!cc) return;
|
||||||
config.ccZoom = cc!.zoom; await saveConfig(['ccZoom']);
|
cc.zoom = Math.min(8, (cc.zoom || 1) * 1.25);
|
||||||
|
config.ccZoom = cc.zoom; await saveConfig(['ccZoom']);
|
||||||
applyPreview(); updateMeta();
|
applyPreview(); updateMeta();
|
||||||
};
|
};
|
||||||
const zoomOut = async () => {
|
const zoomOut = async () => {
|
||||||
cc!.zoom = Math.max(0.1, (cc!.zoom || 1) / 1.25);
|
if (!cc) return;
|
||||||
config.ccZoom = cc!.zoom; await saveConfig(['ccZoom']);
|
cc.zoom = Math.max(0.1, (cc.zoom || 1) / 1.25);
|
||||||
|
config.ccZoom = cc.zoom; await saveConfig(['ccZoom']);
|
||||||
applyPreview(); updateMeta();
|
applyPreview(); updateMeta();
|
||||||
};
|
};
|
||||||
modal.querySelector('#op-cc-zoom-in')!.addEventListener('click', zoomIn);
|
(modal.querySelector('#op-cc-zoom-in') as HTMLButtonElement | null)?.addEventListener('click', zoomIn);
|
||||||
modal.querySelector('#op-cc-zoom-out')!.addEventListener('click', zoomOut);
|
(modal.querySelector('#op-cc-zoom-out') as HTMLButtonElement | null)?.addEventListener('click', zoomOut);
|
||||||
|
|
||||||
cc.realtimeBtn.addEventListener('click', async () => {
|
cc.realtimeBtn?.addEventListener('click', async () => {
|
||||||
cc!.realtime = !cc!.realtime;
|
if (!cc) return;
|
||||||
cc!.realtimeBtn.textContent = `Realtime: ${cc!.realtime ? 'ON' : 'OFF'}`;
|
cc.realtime = !cc.realtime;
|
||||||
cc!.realtimeBtn.classList.toggle('op-danger', cc!.realtime);
|
cc.realtimeBtn.textContent = `Realtime: ${cc.realtime ? 'ON' : 'OFF'}`;
|
||||||
config.ccRealtime = cc!.realtime; await saveConfig(['ccRealtime']);
|
cc.realtimeBtn.classList.toggle('op-danger', cc.realtime);
|
||||||
if (cc!.realtime && cc!.isStale) recalcNow();
|
config.ccRealtime = cc.realtime; await saveConfig(['ccRealtime']);
|
||||||
|
if (cc.realtime && cc.isStale) recalcNow();
|
||||||
});
|
});
|
||||||
|
|
||||||
cc.recalcBtn.addEventListener('click', () => { recalcNow(); });
|
cc.recalcBtn?.addEventListener('click', () => { recalcNow(); });
|
||||||
|
|
||||||
cc.applyBtn.addEventListener('click', async () => {
|
cc.applyBtn?.addEventListener('click', async () => {
|
||||||
const ov = cc!.overlay; if (!ov || !cc!.processedCanvas) return;
|
if (!cc) return;
|
||||||
if (cc!.processedCanvas.width >= MAX_OVERLAY_DIM || cc!.processedCanvas.height >= MAX_OVERLAY_DIM) {
|
const ov = cc.overlay; if (!ov || !cc.processedCanvas) return;
|
||||||
|
if (cc.processedCanvas.width >= MAX_OVERLAY_DIM || cc.processedCanvas.height >= MAX_OVERLAY_DIM) {
|
||||||
showToast(`Image too large to apply (must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM}).`);
|
showToast(`Image too large to apply (must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM}).`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dataUrl = cc!.processedCanvas.toDataURL('image/png');
|
const dataUrl = cc.processedCanvas.toDataURL('image/png');
|
||||||
ov.imageBase64 = dataUrl; ov.imageUrl = null; ov.isLocal = true;
|
ov.imageBase64 = dataUrl; ov.imageUrl = null; ov.isLocal = true;
|
||||||
|
|
||||||
// Mark the processed image as palette-perfect for optimization
|
// Mark the processed image as palette-perfect for optimization
|
||||||
|
|
@ -183,8 +189,8 @@ export function buildCCModal() {
|
||||||
|
|
||||||
await saveConfig(['overlays']); clearOverlayCache(); ensureHook();
|
await saveConfig(['overlays']); clearOverlayCache(); ensureHook();
|
||||||
emitOverlayChanged();
|
emitOverlayChanged();
|
||||||
const uniqueColors = Object.keys(cc!.lastColorCounts).length;
|
const uniqueColors = Object.keys(cc.lastColorCounts).length;
|
||||||
showToast(`Overlay updated (${cc!.processedCanvas.width}×${cc!.processedCanvas.height}, ${uniqueColors} colors).`);
|
showToast(`Overlay updated (${cc.processedCanvas.width}×${cc.processedCanvas.height}, ${uniqueColors} colors).`);
|
||||||
closeCCModal();
|
closeCCModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -204,22 +210,31 @@ export function openCCModal(overlay: any) {
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
if (!cc!.sourceCanvas) { cc!.sourceCanvas = document.createElement('canvas'); cc!.sourceCtx = cc!.sourceCanvas.getContext('2d', { willReadFrequently: true })!; }
|
if (!cc) return;
|
||||||
cc!.sourceCanvas.width = img.width; cc!.sourceCanvas.height = img.height;
|
|
||||||
cc!.sourceCtx!.clearRect(0,0,img.width,img.height);
|
|
||||||
cc!.sourceCtx!.drawImage(img, 0, 0);
|
|
||||||
|
|
||||||
cc!.sourceImageData = cc!.sourceCtx!.getImageData(0,0,img.width,img.height);
|
if (!cc.sourceCanvas) {
|
||||||
|
cc.sourceCanvas = document.createElement('canvas');
|
||||||
|
cc.sourceCtx = cc.sourceCanvas.getContext('2d', { willReadFrequently: true }) || null;
|
||||||
|
}
|
||||||
|
if (!cc.sourceCanvas) return;
|
||||||
|
|
||||||
if (!cc!.processedCanvas) { cc!.processedCanvas = document.createElement('canvas'); cc!.processedCtx = cc!.processedCanvas.getContext('2d')!; }
|
cc.sourceCanvas.width = img.width; cc.sourceCanvas.height = img.height;
|
||||||
|
cc.sourceCtx?.clearRect(0, 0, img.width, img.height);
|
||||||
|
cc.sourceCtx?.drawImage(img, 0, 0);
|
||||||
|
cc.sourceImageData = cc.sourceCtx ? cc.sourceCtx.getImageData(0, 0, img.width, img.height) : null;
|
||||||
|
|
||||||
|
if (!cc.processedCanvas) {
|
||||||
|
cc.processedCanvas = document.createElement('canvas');
|
||||||
|
cc.processedCtx = cc.processedCanvas.getContext('2d') || null;
|
||||||
|
}
|
||||||
|
|
||||||
processImage();
|
processImage();
|
||||||
cc!.isStale = false;
|
cc.isStale = false;
|
||||||
applyPreview();
|
applyPreview();
|
||||||
updateMeta();
|
updateMeta();
|
||||||
|
|
||||||
cc!.backdrop.classList.add('show');
|
cc.backdrop.classList.add('show');
|
||||||
cc!.modal.style.display = 'flex';
|
cc.modal.style.display = 'flex';
|
||||||
};
|
};
|
||||||
img.src = overlay.imageBase64;
|
img.src = overlay.imageBase64;
|
||||||
}
|
}
|
||||||
|
|
@ -240,20 +255,20 @@ function weightedNearest(r: number, g: number, b: number, palette: number[][]) {
|
||||||
const rdiff = pr - r;
|
const rdiff = pr - r;
|
||||||
const gdiff = pg - g;
|
const gdiff = pg - g;
|
||||||
const bdiff = pb - b;
|
const bdiff = pb - b;
|
||||||
const x = (512 + rmean) * rdiff * rdiff >> 8;
|
const x = ((512 + rmean) * rdiff * rdiff) >> 8;
|
||||||
const y = 4 * gdiff * gdiff;
|
const y = 4 * gdiff * gdiff;
|
||||||
const z = (767 - rmean) * bdiff * bdiff >> 8;
|
const z = ((767 - rmean) * bdiff * bdiff) >> 8;
|
||||||
const dist = Math.sqrt(x + y + z);
|
const dist = Math.sqrt(x + y + z);
|
||||||
if (dist < bestDist) { bestDist = dist; best = [pr, pg, pb]; }
|
if (dist < bestDist) { bestDist = dist; best = [pr, pg, pb]; }
|
||||||
}
|
}
|
||||||
return best || [0,0,0];
|
return best || [0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActivePalette(): number[][] {
|
function getActivePalette(): number[][] {
|
||||||
if (!cc) return [];
|
if (!cc) return [];
|
||||||
const arr: number[][] = [];
|
const arr: number[][] = [];
|
||||||
cc.selectedFree.forEach(k => { const [r,g,b] = k.split(',').map(n => parseInt(n,10)); if (Number.isFinite(r)) arr.push([r,g,b]); });
|
cc.selectedFree.forEach(k => { const [r, g, b] = k.split(',').map(n => parseInt(n, 10)); if (Number.isFinite(r)) arr.push([r, g, b]); });
|
||||||
cc.selectedPaid.forEach(k => { const [r,g,b] = k.split(',').map(n => parseInt(n,10)); if (Number.isFinite(r)) arr.push([r,g,b]); });
|
cc.selectedPaid.forEach(k => { const [r, g, b] = k.split(',').map(n => parseInt(n, 10)); if (Number.isFinite(r)) arr.push([r, g, b]); });
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,19 +283,24 @@ function processImage() {
|
||||||
const counts: Record<string, number> = {};
|
const counts: Record<string, number> = {};
|
||||||
|
|
||||||
for (let i = 0; i < src.length; i += 4) {
|
for (let i = 0; i < src.length; i += 4) {
|
||||||
const r = src[i], g = src[i+1], b = src[i+2], a = src[i+3];
|
const r = src[i], g = src[i + 1], b = src[i + 2], a = src[i + 3];
|
||||||
if (a === 0) { out[i]=0; out[i+1]=0; out[i+2]=0; out[i+3]=0; continue; }
|
if (a === 0) { out[i] = 0; out[i + 1] = 0; out[i + 2] = 0; out[i + 3] = 0; continue; }
|
||||||
const [nr, ng, nb] = palette.length ? weightedNearest(r,g,b,palette) : [r,g,b];
|
const [nr, ng, nb] = palette.length ? weightedNearest(r, g, b, palette) : [r, g, b];
|
||||||
out[i]=nr; out[i+1]=ng; out[i+2]=nb; out[i+3]=255;
|
out[i] = nr; out[i + 1] = ng; out[i + 2] = nb; out[i + 3] = 255;
|
||||||
const key = `${nr},${ng},${nb}`;
|
const key = `${nr},${ng},${nb}`;
|
||||||
counts[key] = (counts[key] || 0) + 1;
|
counts[key] = (counts[key] || 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cc.processedCanvas) { cc.processedCanvas = document.createElement('canvas'); cc.processedCtx = cc.processedCanvas.getContext('2d')!; }
|
if (!cc.processedCanvas) {
|
||||||
|
cc.processedCanvas = document.createElement('canvas');
|
||||||
|
cc.processedCtx = cc.processedCanvas.getContext('2d') || null;
|
||||||
|
}
|
||||||
|
if (!cc.processedCanvas) return;
|
||||||
|
|
||||||
cc.processedCanvas.width = w; cc.processedCanvas.height = h;
|
cc.processedCanvas.width = w; cc.processedCanvas.height = h;
|
||||||
|
|
||||||
const outImg = new ImageData(out, w, h);
|
const outImg = new ImageData(out, w, h);
|
||||||
cc.processedCtx!.putImageData(outImg, 0, 0);
|
cc.processedCtx?.putImageData(outImg, 0, 0);
|
||||||
cc.lastColorCounts = counts;
|
cc.lastColorCounts = counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,16 +316,16 @@ function applyPreview() {
|
||||||
cc.previewCanvas.height = ph;
|
cc.previewCanvas.height = ph;
|
||||||
|
|
||||||
const ctx = cc.previewCtx;
|
const ctx = cc.previewCtx;
|
||||||
ctx.clearRect(0,0,pw,ph);
|
ctx.clearRect(0, 0, pw, ph);
|
||||||
ctx.imageSmoothingEnabled = false;
|
(ctx as any).imageSmoothingEnabled = false;
|
||||||
ctx.drawImage(srcCanvas, 0,0, srcCanvas.width, srcCanvas.height, 0,0, pw, ph);
|
ctx.drawImage(srcCanvas, 0, 0, srcCanvas.width, srcCanvas.height, 0, 0, pw, ph);
|
||||||
ctx.imageSmoothingEnabled = true;
|
(ctx as any).imageSmoothingEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMeta() {
|
function updateMeta() {
|
||||||
if (!cc || !cc.sourceImageData) { if (cc) cc.meta.textContent = ''; return; }
|
if (!cc || !cc.sourceImageData) { if (cc) cc.meta.textContent = ''; return; }
|
||||||
const w = cc.sourceImageData.width, h = cc.sourceImageData.height;
|
const w = cc.sourceImageData.width, h = cc.sourceImageData.height;
|
||||||
const colorsUsed = Object.keys(cc.lastColorCounts||{}).length;
|
const colorsUsed = Object.keys(cc.lastColorCounts || {}).length;
|
||||||
const status = cc.isStale ? 'pending recalculation' : 'up to date';
|
const status = cc.isStale ? 'pending recalculation' : 'up to date';
|
||||||
cc.meta.textContent = `Size: ${w}×${h} | Zoom: ${cc.zoom.toFixed(2)}× | Colors: ${colorsUsed} | Status: ${status}`;
|
cc.meta.textContent = `Size: ${w}×${h} | Zoom: ${cc.zoom.toFixed(2)}× | Colors: ${colorsUsed} | Status: ${status}`;
|
||||||
}
|
}
|
||||||
|
|
@ -315,92 +335,105 @@ function renderPaletteGrid() {
|
||||||
cc.freeGrid.innerHTML = '';
|
cc.freeGrid.innerHTML = '';
|
||||||
cc.paidGrid.innerHTML = '';
|
cc.paidGrid.innerHTML = '';
|
||||||
|
|
||||||
for (const [r,g,b] of WPLACE_FREE) {
|
for (const [r, g, b] of WPLACE_FREE) {
|
||||||
const key = `${r},${g},${b}`;
|
const key = `${r},${g},${b}`;
|
||||||
const cell = document.createElement('div');
|
const cell = document.createElement('div');
|
||||||
cell.className = 'op-cc-cell';
|
cell.className = 'op-cc-cell';
|
||||||
cell.style.background = `rgb(${r},${g},${b})`;
|
cell.style.background = `rgb(${r},${g},${b})`;
|
||||||
cell.title = WPLACE_NAMES[key] || key;
|
cell.title = WPLACE_NAMES[key] || key;
|
||||||
cell.dataset.key = key;
|
(cell as any).dataset.key = key;
|
||||||
cell.dataset.type = 'free';
|
(cell as any).dataset.type = 'free';
|
||||||
if (cc.selectedFree.has(key)) cell.classList.add('active');
|
if (cc.selectedFree.has(key)) cell.classList.add('active');
|
||||||
cell.addEventListener('click', async () => {
|
cell.addEventListener('click', async () => {
|
||||||
if (cc!.selectedFree.has(key)) cc!.selectedFree.delete(key); else cc!.selectedFree.add(key);
|
if (!cc) return;
|
||||||
cell.classList.toggle('active', cc!.selectedFree.has(key));
|
if (cc.selectedFree.has(key)) cc.selectedFree.delete(key); else cc.selectedFree.add(key);
|
||||||
config.ccFreeKeys = Array.from(cc!.selectedFree); await saveConfig(['ccFreeKeys']);
|
cell.classList.toggle('active', cc.selectedFree.has(key));
|
||||||
if (cc!.realtime) processImage(); else { cc!.isStale = true; }
|
config.ccFreeKeys = Array.from(cc.selectedFree); await saveConfig(['ccFreeKeys']);
|
||||||
|
if (cc.realtime) processImage(); else { cc.isStale = true; }
|
||||||
applyPreview(); updateMeta(); updateMasterButtons();
|
applyPreview(); updateMeta(); updateMasterButtons();
|
||||||
});
|
});
|
||||||
cc.freeGrid.appendChild(cell);
|
cc.freeGrid.appendChild(cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [r,g,b] of WPLACE_PAID) {
|
for (const [r, g, b] of WPLACE_PAID) {
|
||||||
const key = `${r},${g},${b}`;
|
const key = `${r},${g},${b}`;
|
||||||
const cell = document.createElement('div');
|
const cell = document.createElement('div');
|
||||||
cell.className = 'op-cc-cell';
|
cell.className = 'op-cc-cell';
|
||||||
cell.style.background = `rgb(${r},${g},${b})`;
|
cell.style.background = `rgb(${r},${g},${b})`;
|
||||||
cell.title = WPLACE_NAMES[key] || key;
|
cell.title = WPLACE_NAMES[key] || key;
|
||||||
cell.dataset.key = key;
|
(cell as any).dataset.key = key;
|
||||||
cell.dataset.type = 'paid';
|
(cell as any).dataset.type = 'paid';
|
||||||
if (cc.selectedPaid.has(key)) cell.classList.add('active');
|
if (cc.selectedPaid.has(key)) cell.classList.add('active');
|
||||||
cell.addEventListener('click', async () => {
|
cell.addEventListener('click', async () => {
|
||||||
if (cc!.selectedPaid.has(key)) cc!.selectedPaid.delete(key); else cc!.selectedPaid.add(key);
|
if (!cc) return;
|
||||||
cell.classList.toggle('active', cc!.selectedPaid.has(key));
|
if (cc.selectedPaid.has(key)) cc.selectedPaid.delete(key); else cc.selectedPaid.add(key);
|
||||||
config.ccPaidKeys = Array.from(cc!.selectedPaid); await saveConfig(['ccPaidKeys']);
|
cell.classList.toggle('active', cc.selectedPaid.has(key));
|
||||||
if (cc!.realtime) processImage(); else { cc!.isStale = true; }
|
config.ccPaidKeys = Array.from(cc.selectedPaid); await saveConfig(['ccPaidKeys']);
|
||||||
|
if (cc.realtime) processImage(); else { cc.isStale = true; }
|
||||||
applyPreview(); updateMeta(); updateMasterButtons();
|
applyPreview(); updateMeta(); updateMasterButtons();
|
||||||
});
|
});
|
||||||
cc.paidGrid.appendChild(cell);
|
cc.paidGrid.appendChild(cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
cc.freeToggle.addEventListener('click', async () => {
|
cc.freeToggle.addEventListener('click', async () => {
|
||||||
|
if (!cc) return;
|
||||||
const allActive = isAllFreeActive();
|
const allActive = isAllFreeActive();
|
||||||
setAllActive('free', !allActive);
|
setAllActive('free', !allActive);
|
||||||
config.ccFreeKeys = Array.from(cc!.selectedFree);
|
config.ccFreeKeys = Array.from(cc.selectedFree);
|
||||||
await saveConfig(['ccFreeKeys']);
|
await saveConfig(['ccFreeKeys']);
|
||||||
if (cc!.realtime) recalcNow(); else markStale();
|
if (cc.realtime) recalcNow(); else markStale();
|
||||||
applyPreview(); updateMeta(); updateMasterButtons();
|
applyPreview(); updateMeta(); updateMasterButtons();
|
||||||
});
|
});
|
||||||
cc.paidToggle.addEventListener('click', async () => {
|
cc.paidToggle.addEventListener('click', async () => {
|
||||||
|
if (!cc) return;
|
||||||
const allActive = isAllPaidActive();
|
const allActive = isAllPaidActive();
|
||||||
setAllActive('paid', !allActive);
|
setAllActive('paid', !allActive);
|
||||||
config.ccPaidKeys = Array.from(cc!.selectedPaid);
|
config.ccPaidKeys = Array.from(cc.selectedPaid);
|
||||||
await saveConfig(['ccPaidKeys']);
|
await saveConfig(['ccPaidKeys']);
|
||||||
if (cc!.realtime) recalcNow(); else markStale();
|
if (cc.realtime) recalcNow(); else markStale();
|
||||||
applyPreview(); updateMeta(); updateMasterButtons();
|
applyPreview(); updateMeta(); updateMasterButtons();
|
||||||
});
|
});
|
||||||
|
|
||||||
updateMasterButtons();
|
updateMasterButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAllFreeActive() { return DEFAULT_FREE_KEYS.every(k => cc!.selectedFree.has(k)); }
|
function isAllFreeActive(): boolean {
|
||||||
function isAllPaidActive() {
|
if (!cc) return false;
|
||||||
const allPaidKeys = WPLACE_PAID.map(([r,g,b]) => `${r},${g},${b}`);
|
return DEFAULT_FREE_KEYS.every(k => cc.selectedFree.has(k));
|
||||||
return allPaidKeys.every(k => cc!.selectedPaid.has(k)) && allPaidKeys.length > 0;
|
|
||||||
}
|
}
|
||||||
function setAllActive(type: 'free'|'paid', active: boolean) {
|
function isAllPaidActive(): boolean {
|
||||||
|
if (!cc) return false;
|
||||||
|
const allPaidKeys = WPLACE_PAID.map(([r, g, b]) => `${r},${g},${b}`);
|
||||||
|
return allPaidKeys.every(k => cc.selectedPaid.has(k)) && allPaidKeys.length > 0;
|
||||||
|
}
|
||||||
|
function setAllActive(type: 'free' | 'paid', active: boolean) {
|
||||||
|
if (!cc) return;
|
||||||
if (type === 'free') {
|
if (type === 'free') {
|
||||||
const keys = DEFAULT_FREE_KEYS;
|
const keys = DEFAULT_FREE_KEYS;
|
||||||
if (active) keys.forEach(k => cc!.selectedFree.add(k)); else cc!.selectedFree.clear();
|
if (active) keys.forEach(k => cc.selectedFree.add(k)); else cc.selectedFree.clear();
|
||||||
cc!.freeGrid.querySelectorAll('.op-cc-cell').forEach(cell => cell.classList.toggle('active', active));
|
cc.freeGrid.querySelectorAll('.op-cc-cell').forEach(cell => cell.classList.toggle('active', active));
|
||||||
} else {
|
} else {
|
||||||
const keys = WPLACE_PAID.map(([r,g,b]) => `${r},${g},${b}`);
|
const keys = WPLACE_PAID.map(([r, g, b]) => `${r},${g},${b}`);
|
||||||
if (active) keys.forEach(k => cc!.selectedPaid.add(k)); else cc!.selectedPaid.clear();
|
if (active) keys.forEach(k => cc.selectedPaid.add(k)); else cc.selectedPaid.clear();
|
||||||
cc!.paidGrid.querySelectorAll('.op-cc-cell').forEach(cell => cell.classList.toggle('active', active));
|
cc.paidGrid.querySelectorAll('.op-cc-cell').forEach(cell => cell.classList.toggle('active', active));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function updateMasterButtons() {
|
function updateMasterButtons() {
|
||||||
cc!.freeToggle.textContent = isAllFreeActive() ? 'Unselect All' : 'Select All';
|
if (!cc) return;
|
||||||
cc!.paidToggle.textContent = isAllPaidActive() ? 'Unselect All' : 'Select All';
|
cc.freeToggle.textContent = isAllFreeActive() ? 'Unselect All' : 'Select All';
|
||||||
|
cc.paidToggle.textContent = isAllPaidActive() ? 'Unselect All' : 'Select All';
|
||||||
}
|
}
|
||||||
|
|
||||||
function recalcNow() {
|
function recalcNow() {
|
||||||
|
if (!cc) return;
|
||||||
processImage();
|
processImage();
|
||||||
cc!.isStale = false;
|
cc.isStale = false;
|
||||||
applyPreview();
|
applyPreview();
|
||||||
updateMeta();
|
updateMeta();
|
||||||
}
|
}
|
||||||
function markStale() {
|
function markStale() {
|
||||||
cc!.isStale = true;
|
if (!cc) return;
|
||||||
cc!.meta.textContent = cc!.meta.textContent.replace(/ \| Status: .+$/, '') + ' | Status: pending recalculation';
|
cc.isStale = true;
|
||||||
|
const base = (cc.meta.textContent || '').replace(/ \| Status: .+$/, '');
|
||||||
|
cc.meta.textContent = `${base} | Status: pending recalculation`;
|
||||||
}
|
}
|
||||||
320
src/ui/panel.ts
320
src/ui/panel.ts
|
|
@ -9,10 +9,11 @@ import { extractPixelCoords } from '../core/overlay';
|
||||||
import { buildCCModal, openCCModal } from './ccModal';
|
import { buildCCModal, openCCModal } from './ccModal';
|
||||||
import { buildRSModal, openRSModal } from './rsModal';
|
import { buildRSModal, openRSModal } from './rsModal';
|
||||||
import { EV_ANCHOR_SET, EV_AUTOCAP_CHANGED } from '../core/events';
|
import { EV_ANCHOR_SET, EV_AUTOCAP_CHANGED } from '../core/events';
|
||||||
|
import { updatePaletteSymbols } from '../core/palette-inject';
|
||||||
|
|
||||||
let panelEl: HTMLDivElement | null = null;
|
let panelEl: HTMLDivElement | null = null;
|
||||||
|
|
||||||
function $(id: string) { return document.getElementById(id)!; }
|
function $(id: string): HTMLElement | null { return document.getElementById(id); }
|
||||||
|
|
||||||
export function createUI() {
|
export function createUI() {
|
||||||
if (document.getElementById('overlay-pro-panel')) return;
|
if (document.getElementById('overlay-pro-panel')) return;
|
||||||
|
|
@ -23,8 +24,10 @@ export function createUI() {
|
||||||
|
|
||||||
const panelW = 340;
|
const panelW = 340;
|
||||||
const defaultLeft = Math.max(12, window.innerWidth - panelW - 80);
|
const defaultLeft = Math.max(12, window.innerWidth - panelW - 80);
|
||||||
panel.style.left = (Number.isFinite(config.panelX as any) ? (config.panelX as any) : defaultLeft) + 'px';
|
const initLeft = (typeof config.panelX === 'number' && Number.isFinite(config.panelX)) ? config.panelX : defaultLeft;
|
||||||
panel.style.top = (Number.isFinite(config.panelY as any) ? (config.panelY as any) : 120) + 'px';
|
const initTop = (typeof config.panelY === 'number' && Number.isFinite(config.panelY)) ? config.panelY : 120;
|
||||||
|
panel.style.left = `${initLeft}px`;
|
||||||
|
panel.style.top = `${initTop}px`;
|
||||||
|
|
||||||
panel.innerHTML = `
|
panel.innerHTML = `
|
||||||
<div class="op-header" id="op-header">
|
<div class="op-header" id="op-header">
|
||||||
|
|
@ -55,7 +58,7 @@ export function createUI() {
|
||||||
<div class="op-mode-setting" data-setting="minify">
|
<div class="op-mode-setting" data-setting="minify">
|
||||||
<div class="op-row"><label>Style</label>
|
<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="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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -173,11 +176,12 @@ export function createUI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildOverlayListUI() {
|
function rebuildOverlayListUI() {
|
||||||
const list = $('op-overlay-list');
|
const list = $('op-overlay-list') as HTMLDivElement | null;
|
||||||
|
if (!list) return;
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
for (const ov of config.overlays) {
|
for (const ov of config.overlays) {
|
||||||
const item = document.createElement('div');
|
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)' : '');
|
const localTag = ov.isLocal ? ' (local)' : (!ov.imageBase64 ? ' (no image)' : '');
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<input type="radio" name="op-active" ${ov.id === config.activeOverlayId ? 'checked' : ''} title="Set active"/>
|
<input type="radio" name="op-active" ${ov.id === config.activeOverlayId ? 'checked' : ''} title="Set active"/>
|
||||||
|
|
@ -207,7 +211,7 @@ function rebuildOverlayListUI() {
|
||||||
|
|
||||||
async function addBlankOverlay() {
|
async function addBlankOverlay() {
|
||||||
const name = uniqueName('Overlay', config.overlays.map(o => o.name || ''));
|
const name = uniqueName('Overlay', config.overlays.map(o => o.name || ''));
|
||||||
const ov = { id: uid(), name, enabled: true, imageUrl: null, imageBase64: null, isLocal: false, pixelUrl: null, offsetX: 0, offsetY: 0, opacity: 0.7 };
|
const ov = { id: uid(), name, enabled: true, imageUrl: null as string | null, imageBase64: null as string | null, isLocal: false, pixelUrl: null as string | null, offsetX: 0, offsetY: 0, opacity: 0.7 };
|
||||||
config.overlays.push(ov);
|
config.overlays.push(ov);
|
||||||
config.activeOverlayId = ov.id;
|
config.activeOverlayId = ov.id;
|
||||||
await saveConfig(['overlays', 'activeOverlayId']);
|
await saveConfig(['overlays', 'activeOverlayId']);
|
||||||
|
|
@ -221,7 +225,7 @@ async function setOverlayImageFromURL(ov: any, url: string) {
|
||||||
await saveConfig(['overlays']); clearOverlayCache();
|
await saveConfig(['overlays']); clearOverlayCache();
|
||||||
config.autoCapturePixelUrl = true; await saveConfig(['autoCapturePixelUrl']);
|
config.autoCapturePixelUrl = true; await saveConfig(['autoCapturePixelUrl']);
|
||||||
ensureHook(); updateUI();
|
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) {
|
async function setOverlayImageFromFile(ov: any, file: File) {
|
||||||
if (!file || !file.type || !file.type.startsWith('image/')) { alert('Please choose an image file.'); return; }
|
if (!file || !file.type || !file.type.startsWith('image/')) { alert('Please choose an image file.'); return; }
|
||||||
|
|
@ -231,20 +235,43 @@ async function setOverlayImageFromFile(ov: any, file: File) {
|
||||||
await saveConfig(['overlays']); clearOverlayCache();
|
await saveConfig(['overlays']); clearOverlayCache();
|
||||||
config.autoCapturePixelUrl = true; await saveConfig(['autoCapturePixelUrl']);
|
config.autoCapturePixelUrl = true; await saveConfig(['autoCapturePixelUrl']);
|
||||||
ensureHook(); updateUI();
|
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 ImportOverlay = {
|
||||||
|
name?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
pixelUrl?: string | null;
|
||||||
|
offsetX?: number;
|
||||||
|
offsetY?: number;
|
||||||
|
opacity?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isImportOverlay(val: unknown): val is ImportOverlay {
|
||||||
|
return typeof val === 'object' && val !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importOverlayFromJSON(jsonText: string) {
|
async function importOverlayFromJSON(jsonText: string) {
|
||||||
let obj; try { obj = JSON.parse(jsonText); } catch { alert('Invalid JSON'); return; }
|
let parsed: unknown;
|
||||||
const arr = Array.isArray(obj) ? obj : [obj];
|
try {
|
||||||
|
parsed = JSON.parse(jsonText) as unknown;
|
||||||
|
} catch {
|
||||||
|
alert('Invalid JSON');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr: ImportOverlay[] = Array.isArray(parsed)
|
||||||
|
? parsed.filter(isImportOverlay)
|
||||||
|
: (isImportOverlay(parsed) ? [parsed] : []);
|
||||||
|
|
||||||
let imported = 0, failed = 0;
|
let imported = 0, failed = 0;
|
||||||
for (const item of arr) {
|
for (const item of arr) {
|
||||||
const name = uniqueName(item.name || 'Imported Overlay', config.overlays.map(o => o.name || ''));
|
const name = uniqueName(item?.name || 'Imported Overlay', config.overlays.map(o => o.name || ''));
|
||||||
const imageUrl = item.imageUrl;
|
const imageUrl = item?.imageUrl;
|
||||||
const pixelUrl = item.pixelUrl ?? null;
|
const pixelUrl = (item?.pixelUrl ?? null) as string | null;
|
||||||
const offsetX = Number.isFinite(item.offsetX) ? item.offsetX : 0;
|
const offsetX = (typeof item?.offsetX === 'number' && Number.isFinite(item.offsetX)) ? item.offsetX : 0;
|
||||||
const offsetY = Number.isFinite(item.offsetY) ? item.offsetY : 0;
|
const offsetY = (typeof item?.offsetY === 'number' && Number.isFinite(item.offsetY)) ? item.offsetY : 0;
|
||||||
const opacity = Number.isFinite(item.opacity) ? item.opacity : 0.7;
|
const opacity = (typeof item?.opacity === 'number' && Number.isFinite(item.opacity)) ? item.opacity : 0.7;
|
||||||
if (!imageUrl) { failed++; continue; }
|
if (!imageUrl) { failed++; continue; }
|
||||||
try {
|
try {
|
||||||
const base64 = await urlToDataURL(imageUrl);
|
const base64 = await urlToDataURL(imageUrl);
|
||||||
|
|
@ -268,14 +295,14 @@ function exportActiveOverlayToClipboard() {
|
||||||
copyText(text).then(() => alert('Overlay JSON copied to clipboard!')).catch(() => { prompt('Copy the JSON below:', text); });
|
copyText(text).then(() => alert('Overlay JSON copied to clipboard!')).catch(() => { prompt('Copy the JSON below:', text); });
|
||||||
}
|
}
|
||||||
function copyText(text: string) {
|
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'));
|
return Promise.reject(new Error('Clipboard API not available'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEventListeners(panel: HTMLDivElement) {
|
function addEventListeners(panel: HTMLDivElement) {
|
||||||
$('op-theme-toggle').addEventListener('click', async (e) => { e.stopPropagation(); config.theme = config.theme === 'light' ? 'dark' : 'light'; await saveConfig(['theme']); applyTheme(); });
|
$('op-theme-toggle')?.addEventListener('click', async (e) => { e.stopPropagation(); config.theme = config.theme === 'light' ? 'dark' : 'light'; await saveConfig(['theme']); applyTheme(); });
|
||||||
$('op-refresh-btn').addEventListener('click', (e) => { e.stopPropagation(); location.reload(); });
|
$('op-refresh-btn')?.addEventListener('click', (e) => { e.stopPropagation(); location.reload(); });
|
||||||
$('op-panel-toggle').addEventListener('click', (e) => { e.stopPropagation(); config.isPanelCollapsed = !config.isPanelCollapsed; saveConfig(['isPanelCollapsed']); updateUI(); });
|
$('op-panel-toggle')?.addEventListener('click', (e) => { e.stopPropagation(); config.isPanelCollapsed = !config.isPanelCollapsed; saveConfig(['isPanelCollapsed']); updateUI(); });
|
||||||
|
|
||||||
panel.querySelectorAll('.op-tab-btn').forEach(btn => {
|
panel.querySelectorAll('.op-tab-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
|
@ -288,23 +315,46 @@ function addEventListeners(panel: HTMLDivElement) {
|
||||||
saveConfig(['overlayMode']);
|
saveConfig(['overlayMode']);
|
||||||
ensureHook();
|
ensureHook();
|
||||||
updateUI();
|
updateUI();
|
||||||
|
updatePaletteSymbols();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
$('op-style-dots').addEventListener('change', () => { if (($('op-style-dots') as HTMLInputElement).checked) { config.minifyStyle = 'dots'; saveConfig(['minifyStyle']); clearOverlayCache(); ensureHook(); }});
|
|
||||||
$('op-style-symbols').addEventListener('change', () => { if (($('op-style-symbols') as HTMLInputElement).checked) { config.minifyStyle = 'symbols'; saveConfig(['minifyStyle']); clearOverlayCache(); ensureHook(); }});
|
|
||||||
|
|
||||||
$('op-autocap-toggle').addEventListener('click', () => { config.autoCapturePixelUrl = !config.autoCapturePixelUrl; saveConfig(['autoCapturePixelUrl']); ensureHook(); updateUI(); });
|
const styleDotsEl = $('op-style-dots') as HTMLInputElement | null;
|
||||||
|
styleDotsEl?.addEventListener('change', () => {
|
||||||
|
if (styleDotsEl.checked) {
|
||||||
|
config.minifyStyle = 'dots';
|
||||||
|
saveConfig(['minifyStyle']);
|
||||||
|
clearOverlayCache();
|
||||||
|
ensureHook();
|
||||||
|
updatePaletteSymbols();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$('op-add-overlay').addEventListener('click', async () => { try { await addBlankOverlay(); } catch (e) { console.error(e); } });
|
const styleSymbolsEl = $('op-style-symbols') as HTMLInputElement | null;
|
||||||
$('op-import-overlay').addEventListener('click', async () => { const text = prompt('Paste overlay JSON (single or array):'); if (!text) return; await importOverlayFromJSON(text); });
|
styleSymbolsEl?.addEventListener('change', () => {
|
||||||
$('op-export-overlay').addEventListener('click', () => exportActiveOverlayToClipboard());
|
if (styleSymbolsEl.checked) {
|
||||||
$('op-collapse-list').addEventListener('click', () => { config.collapseList = !config.collapseList; saveConfig(['collapseList']); updateUI(); });
|
config.minifyStyle = 'symbols';
|
||||||
$('op-collapse-editor').addEventListener('click', () => { config.collapseEditor = !config.collapseEditor; saveConfig(['collapseEditor']); updateUI(); });
|
saveConfig(['minifyStyle']);
|
||||||
$('op-collapse-positioning').addEventListener('click', () => { config.collapsePositioning = !config.collapsePositioning; saveConfig(['collapsePositioning']); updateUI(); });
|
clearOverlayCache();
|
||||||
|
ensureHook();
|
||||||
|
updatePaletteSymbols();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$('op-name').addEventListener('change', async (e: any) => {
|
$('op-autocap-toggle')?.addEventListener('click', () => { config.autoCapturePixelUrl = !config.autoCapturePixelUrl; saveConfig(['autoCapturePixelUrl']); ensureHook(); updateUI(); });
|
||||||
|
|
||||||
|
$('op-add-overlay')?.addEventListener('click', async () => { try { await addBlankOverlay(); } catch (e) { console.error(e); } });
|
||||||
|
$('op-import-overlay')?.addEventListener('click', async () => { const text = prompt('Paste overlay JSON (single or array):'); if (!text) return; await importOverlayFromJSON(text); });
|
||||||
|
$('op-export-overlay')?.addEventListener('click', () => exportActiveOverlayToClipboard());
|
||||||
|
$('op-collapse-list')?.addEventListener('click', () => { config.collapseList = !config.collapseList; saveConfig(['collapseList']); updateUI(); });
|
||||||
|
$('op-collapse-editor')?.addEventListener('click', () => { config.collapseEditor = !config.collapseEditor; saveConfig(['collapseEditor']); updateUI(); });
|
||||||
|
$('op-collapse-positioning')?.addEventListener('click', () => { config.collapsePositioning = !config.collapsePositioning; saveConfig(['collapsePositioning']); updateUI(); });
|
||||||
|
|
||||||
|
const nameInput = $('op-name') as HTMLInputElement | null;
|
||||||
|
nameInput?.addEventListener('change', async (e: Event) => {
|
||||||
const ov = getActiveOverlay(); if (!ov) return;
|
const ov = getActiveOverlay(); if (!ov) return;
|
||||||
const desired = (e.target.value || '').trim() || 'Overlay';
|
const target = e.target as HTMLInputElement;
|
||||||
|
const desired = (target.value || '').trim() || 'Overlay';
|
||||||
if (config.overlays.some(o => o.id !== ov.id && (o.name || '').toLowerCase() === desired.toLowerCase())) {
|
if (config.overlays.some(o => o.id !== ov.id && (o.name || '').toLowerCase() === desired.toLowerCase())) {
|
||||||
ov.name = uniqueName(desired, config.overlays.map(o => o.name || ''));
|
ov.name = uniqueName(desired, config.overlays.map(o => o.name || ''));
|
||||||
showToast(`Name in use. Renamed to "${ov.name}".`);
|
showToast(`Name in use. Renamed to "${ov.name}".`);
|
||||||
|
|
@ -312,48 +362,56 @@ function addEventListeners(panel: HTMLDivElement) {
|
||||||
await saveConfig(['overlays']); rebuildOverlayListUI();
|
await saveConfig(['overlays']); rebuildOverlayListUI();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('op-fetch').addEventListener('click', async () => {
|
$('op-fetch')?.addEventListener('click', async () => {
|
||||||
const ov = getActiveOverlay(); if (!ov) { alert('No active overlay selected.'); return; }
|
const ov = getActiveOverlay(); if (!ov) { alert('No active overlay selected.'); return; }
|
||||||
if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; }
|
if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; }
|
||||||
const url = ( $('op-image-url') as HTMLInputElement ).value.trim(); if (!url) { alert('Enter an image link first.'); return; }
|
const urlEl = $('op-image-url') as HTMLInputElement | null;
|
||||||
|
const url = urlEl?.value.trim(); if (!url) { alert('Enter an image link first.'); return; }
|
||||||
try { await setOverlayImageFromURL(ov, url); } catch (e) { console.error(e); alert('Failed to fetch image.'); }
|
try { await setOverlayImageFromURL(ov, url); } catch (e) { console.error(e); alert('Failed to fetch image.'); }
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropzone = $('op-dropzone');
|
const dropzone = $('op-dropzone') as HTMLDivElement | null;
|
||||||
dropzone.addEventListener('click', () => $('op-file-input').click());
|
dropzone?.addEventListener('click', () => ( $('op-file-input') as HTMLInputElement | null )?.click());
|
||||||
$('op-file-input').addEventListener('change', async (e: any) => {
|
const fileInput = $('op-file-input') as HTMLInputElement | null;
|
||||||
const file = e.target.files && e.target.files[0]; e.target.value=''; if (!file) return;
|
fileInput?.addEventListener('change', async (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const file = target?.files?.[0]; if (target) target.value = ''; if (!file) return;
|
||||||
const ov = getActiveOverlay(); if (!ov) 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; }
|
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.'); }
|
try { await setOverlayImageFromFile(ov, file); } catch (err) { console.error(err); alert('Failed to load local image.'); }
|
||||||
});
|
});
|
||||||
['dragenter', 'dragover'].forEach(evt => dropzone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drop-highlight'); }));
|
if (dropzone) {
|
||||||
['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'); }));
|
['dragenter', 'dragover'].forEach(evt => dropzone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drop-highlight'); }));
|
||||||
dropzone.addEventListener('drop', async (e: any) => {
|
['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'); }));
|
||||||
const dt = e.dataTransfer; if (!dt) return; const file = dt.files && dt.files[0]; if (!file) return;
|
dropzone.addEventListener('drop', async (e: DragEvent) => {
|
||||||
const ov = getActiveOverlay(); if (!ov) return;
|
const dt = e.dataTransfer; if (!dt) return; const file = dt?.files?.[0]; if (!file) return;
|
||||||
if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; }
|
const ov = getActiveOverlay(); if (!ov) return;
|
||||||
try { await setOverlayImageFromFile(ov, file); } catch (err) { console.error(err); alert('Failed to load dropped image.'); }
|
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.'); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const nudge = async (dx: number, dy: number) => {
|
const nudge = async (dx: number, dy: number) => {
|
||||||
const ov = getActiveOverlay(); if (!ov) return;
|
const ov = getActiveOverlay(); if (!ov) return;
|
||||||
ov.offsetX += dx; ov.offsetY += dy;
|
ov.offsetX += dx; ov.offsetY += dy;
|
||||||
await saveConfig(['overlays']); clearOverlayCache(); updateUI();
|
await saveConfig(['overlays']); clearOverlayCache(); updateUI();
|
||||||
};
|
};
|
||||||
$('op-nudge-up').addEventListener('click', () => nudge(0, -1));
|
$('op-nudge-up')?.addEventListener('click', () => nudge(0, -1));
|
||||||
$('op-nudge-down').addEventListener('click', () => nudge(0, 1));
|
$('op-nudge-down')?.addEventListener('click', () => nudge(0, 1));
|
||||||
$('op-nudge-left').addEventListener('click', () => nudge(-1, 0));
|
$('op-nudge-left')?.addEventListener('click', () => nudge(-1, 0));
|
||||||
$('op-nudge-right').addEventListener('click', () => nudge(1, 0));
|
$('op-nudge-right')?.addEventListener('click', () => nudge(1, 0));
|
||||||
|
|
||||||
$('op-opacity-slider').addEventListener('input', (e: any) => {
|
const opacitySlider = $('op-opacity-slider') as HTMLInputElement | null;
|
||||||
|
opacitySlider?.addEventListener('input', (e: Event) => {
|
||||||
const ov = getActiveOverlay(); if (!ov) return;
|
const ov = getActiveOverlay(); if (!ov) return;
|
||||||
ov.opacity = parseFloat(e.target.value);
|
const target = e.target as HTMLInputElement;
|
||||||
$('op-opacity-value').textContent = Math.round(ov.opacity * 100) + '%';
|
ov.opacity = parseFloat(target.value);
|
||||||
|
const valEl = $('op-opacity-value');
|
||||||
|
if (valEl) valEl.textContent = `${Math.round(ov.opacity * 100)}%`;
|
||||||
});
|
});
|
||||||
$('op-opacity-slider').addEventListener('change', async () => { await saveConfig(['overlays']); clearOverlayCache(); });
|
opacitySlider?.addEventListener('change', async () => { await saveConfig(['overlays']); clearOverlayCache(); });
|
||||||
|
|
||||||
$('op-download-overlay').addEventListener('click', () => {
|
$('op-download-overlay')?.addEventListener('click', () => {
|
||||||
const ov = getActiveOverlay();
|
const ov = getActiveOverlay();
|
||||||
if (!ov || !ov.imageBase64) { showToast('No overlay image to download.'); return; }
|
if (!ov || !ov.imageBase64) { showToast('No overlay image to download.'); return; }
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|
@ -364,7 +422,7 @@ function addEventListeners(panel: HTMLDivElement) {
|
||||||
a.remove();
|
a.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('op-open-cc').addEventListener('click', () => {
|
$('op-open-cc')?.addEventListener('click', () => {
|
||||||
const ov = getActiveOverlay(); if (!ov || !ov.imageBase64) { showToast('No overlay image to edit.'); return; }
|
const ov = getActiveOverlay(); if (!ov || !ov.imageBase64) { showToast('No overlay image to edit.'); return; }
|
||||||
openCCModal(ov);
|
openCCModal(ov);
|
||||||
});
|
});
|
||||||
|
|
@ -379,7 +437,7 @@ function addEventListeners(panel: HTMLDivElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableDrag(panel: HTMLDivElement) {
|
function enableDrag(panel: HTMLDivElement) {
|
||||||
const header = panel.querySelector('#op-header') as HTMLDivElement;
|
const header = panel.querySelector('#op-header') as HTMLDivElement | null;
|
||||||
if (!header) return;
|
if (!header) return;
|
||||||
|
|
||||||
let isDragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0, moved = false;
|
let isDragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0, moved = false;
|
||||||
|
|
@ -396,8 +454,8 @@ function enableDrag(panel: HTMLDivElement) {
|
||||||
const dx = e.clientX - startX, dy = e.clientY - startY;
|
const dx = e.clientX - startX, dy = e.clientY - startY;
|
||||||
const maxLeft = Math.max(8, window.innerWidth - panel.offsetWidth - 8);
|
const maxLeft = Math.max(8, window.innerWidth - panel.offsetWidth - 8);
|
||||||
const maxTop = Math.max(8, window.innerHeight - panel.offsetHeight - 8);
|
const maxTop = Math.max(8, window.innerHeight - panel.offsetHeight - 8);
|
||||||
panel.style.left = clamp(startLeft + dx, 8, maxLeft) + 'px';
|
panel.style.left = `${clamp(startLeft + dx, 8, maxLeft)}px`;
|
||||||
panel.style.top = clamp(startTop + dy, 8, maxTop) + 'px';
|
panel.style.top = `${clamp(startTop + dy, 8, maxTop)}px`;
|
||||||
moved = true;
|
moved = true;
|
||||||
};
|
};
|
||||||
const onPointerUp = (e: PointerEvent) => {
|
const onPointerUp = (e: PointerEvent) => {
|
||||||
|
|
@ -420,47 +478,51 @@ function enableDrag(panel: HTMLDivElement) {
|
||||||
const maxTop = Math.max(8, window.innerHeight - panel.offsetHeight - 8);
|
const maxTop = Math.max(8, window.innerHeight - panel.offsetHeight - 8);
|
||||||
const newLeft = Math.min(Math.max(rect.left, 8), maxLeft);
|
const newLeft = Math.min(Math.max(rect.left, 8), maxLeft);
|
||||||
const newTop = Math.min(Math.max(rect.top, 8), maxTop);
|
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']);
|
config.panelX = newLeft; config.panelY = newTop; saveConfig(['panelX', 'panelY']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEditorUI() {
|
function updateEditorUI() {
|
||||||
const editorSect = $('op-editor-section');
|
const editorSect = $('op-editor-section') as HTMLDivElement | null;
|
||||||
const editorBody = $('op-editor-body');
|
const editorBody = $('op-editor-body') as HTMLDivElement | null;
|
||||||
const ov = getActiveOverlay();
|
const ov = getActiveOverlay();
|
||||||
|
|
||||||
|
if (!editorSect) return;
|
||||||
editorSect.style.display = ov ? 'flex' : 'none';
|
editorSect.style.display = ov ? 'flex' : 'none';
|
||||||
if (!ov) return;
|
if (!ov || !editorBody) return;
|
||||||
|
|
||||||
( $('op-name') as HTMLInputElement ).value = ov.name || '';
|
const nameEl = $('op-name') as HTMLInputElement | null;
|
||||||
|
if (nameEl) {
|
||||||
|
nameEl.value = ov.name || '';
|
||||||
|
}
|
||||||
|
|
||||||
const srcWrap = $('op-image-source');
|
const srcWrap = $('op-image-source') as HTMLDivElement | null;
|
||||||
const previewWrap = $('op-preview-wrap');
|
const previewWrap = $('op-preview-wrap') as HTMLDivElement | null;
|
||||||
const previewImg = $('op-image-preview') as HTMLImageElement;
|
const previewImg = $('op-image-preview') as HTMLImageElement | null;
|
||||||
const ccRow = $('op-cc-btn-row');
|
const ccRow = $('op-cc-btn-row') as HTMLDivElement | null;
|
||||||
|
|
||||||
if (ov.imageBase64) {
|
if (ov.imageBase64) {
|
||||||
srcWrap.style.display = 'none';
|
if (srcWrap) srcWrap.style.display = 'none';
|
||||||
previewWrap.style.display = 'flex';
|
if (previewWrap) previewWrap.style.display = 'flex';
|
||||||
previewImg.src = ov.imageBase64;
|
if (previewImg) previewImg.src = ov.imageBase64;
|
||||||
ccRow.style.display = 'flex';
|
if (ccRow) ccRow.style.display = 'flex';
|
||||||
} else {
|
} else {
|
||||||
srcWrap.style.display = 'block';
|
if (srcWrap) srcWrap.style.display = 'block';
|
||||||
previewWrap.style.display = 'none';
|
if (previewWrap) previewWrap.style.display = 'none';
|
||||||
ccRow.style.display = 'none';
|
if (ccRow) ccRow.style.display = 'none';
|
||||||
( $('op-image-url') as HTMLInputElement ).value = ov.imageUrl || '';
|
const urlInput = $('op-image-url') as HTMLInputElement | null;
|
||||||
|
if (urlInput) urlInput.value = ov.imageUrl || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const coords = ov.pixelUrl ? extractPixelCoords(ov.pixelUrl) : { chunk1: '-', chunk2: '-', posX: '-', posY: '-' } as any;
|
const coords = ov.pixelUrl ? extractPixelCoords(ov.pixelUrl) : { chunk1: '-', chunk2: '-', posX: '-', posY: '-' } as any;
|
||||||
const coordDisplay = $('op-coord-display');
|
const coordDisplay = $('op-coord-display');
|
||||||
if(coordDisplay) {
|
if (coordDisplay) {
|
||||||
coordDisplay.textContent = ov.pixelUrl
|
coordDisplay.textContent = ov.pixelUrl
|
||||||
? `Ref: chunk ${coords.chunk1}/${coords.chunk2} at (${coords.posX}, ${coords.posY})`
|
? `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');
|
const indicator = $('op-offset-indicator');
|
||||||
if (indicator) indicator.textContent = `Offset X ${ov.offsetX}, Y ${ov.offsetY}`;
|
if (indicator) indicator.textContent = `Offset X ${ov.offsetX}, Y ${ov.offsetY}`;
|
||||||
|
|
||||||
|
|
@ -474,12 +536,14 @@ export function updateUI() {
|
||||||
|
|
||||||
applyTheme();
|
applyTheme();
|
||||||
|
|
||||||
const content = $('op-content');
|
const content = $('op-content') as HTMLDivElement | null;
|
||||||
const toggle = $('op-panel-toggle');
|
const toggle = $('op-panel-toggle') as HTMLButtonElement | null;
|
||||||
const collapsed = !!config.isPanelCollapsed;
|
const collapsed = !!config.isPanelCollapsed;
|
||||||
content.style.display = collapsed ? 'none' : 'flex';
|
if (content) content.style.display = collapsed ? 'none' : 'flex';
|
||||||
toggle.textContent = collapsed ? '▸' : '▾';
|
if (toggle) {
|
||||||
toggle.title = collapsed ? 'Expand' : 'Collapse';
|
toggle.textContent = collapsed ? '▸' : '▾';
|
||||||
|
toggle.title = collapsed ? 'Expand' : 'Collapse';
|
||||||
|
}
|
||||||
|
|
||||||
// --- Mode Tabs ---
|
// --- Mode Tabs ---
|
||||||
panelEl.querySelectorAll('.op-tab-btn').forEach(btn => {
|
panelEl.querySelectorAll('.op-tab-btn').forEach(btn => {
|
||||||
|
|
@ -490,69 +554,79 @@ export function updateUI() {
|
||||||
} else {
|
} else {
|
||||||
isActive = mode === config.overlayMode;
|
isActive = mode === config.overlayMode;
|
||||||
}
|
}
|
||||||
btn.classList.toggle('active', isActive);
|
(btn as HTMLButtonElement).classList.toggle('active', isActive);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Mode Settings ---
|
// --- Mode Settings ---
|
||||||
const fullOverlaySettings = $('op-mode-settings').querySelector('[data-setting="above"]') as HTMLDivElement;
|
const settingsRoot = $('op-mode-settings') as HTMLDivElement | null;
|
||||||
const minifySettings = $('op-mode-settings').querySelector('[data-setting="minify"]') as HTMLDivElement;
|
const fullOverlaySettings = settingsRoot?.querySelector('[data-setting="above"]') as HTMLDivElement | null;
|
||||||
|
const minifySettings = settingsRoot?.querySelector('[data-setting="minify"]') as HTMLDivElement | null;
|
||||||
|
|
||||||
if (config.overlayMode === 'above' || config.overlayMode === 'behind') {
|
if (config.overlayMode === 'above' || config.overlayMode === 'behind') {
|
||||||
fullOverlaySettings.classList.add('active');
|
fullOverlaySettings?.classList.add('active');
|
||||||
minifySettings.classList.remove('active');
|
minifySettings?.classList.remove('active');
|
||||||
const ov = getActiveOverlay();
|
const ov = getActiveOverlay();
|
||||||
if(ov) {
|
if (ov) {
|
||||||
( $('op-opacity-slider') as HTMLInputElement ).value = String(ov.opacity);
|
const opacitySlider = $('op-opacity-slider') as HTMLInputElement | null;
|
||||||
$('op-opacity-value').textContent = Math.round(ov.opacity * 100) + '%';
|
const opacityVal = $('op-opacity-value');
|
||||||
|
if (opacitySlider) opacitySlider.value = String(ov.opacity);
|
||||||
|
if (opacityVal) opacityVal.textContent = `${Math.round(ov.opacity * 100)}%`;
|
||||||
}
|
}
|
||||||
} else if (config.overlayMode === 'minify') {
|
} else if (config.overlayMode === 'minify') {
|
||||||
fullOverlaySettings.classList.remove('active');
|
fullOverlaySettings?.classList.remove('active');
|
||||||
minifySettings.classList.add('active');
|
minifySettings?.classList.add('active');
|
||||||
} else {
|
} else {
|
||||||
fullOverlaySettings.classList.remove('active');
|
fullOverlaySettings?.classList.remove('active');
|
||||||
minifySettings.classList.remove('active');
|
minifySettings?.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
($('op-style-dots') as HTMLInputElement).checked = config.minifyStyle === 'dots';
|
const dotsEl = $('op-style-dots') as HTMLInputElement | null;
|
||||||
($('op-style-symbols') as HTMLInputElement).checked = config.minifyStyle === 'symbols';
|
if (dotsEl) dotsEl.checked = config.minifyStyle === 'dots';
|
||||||
|
const symbolsEl = $('op-style-symbols') as HTMLInputElement | null;
|
||||||
const layeringBtns = $('op-layering-btns');
|
if (symbolsEl) symbolsEl.checked = config.minifyStyle === 'symbols';
|
||||||
layeringBtns.innerHTML = '';
|
|
||||||
const behindBtn = document.createElement('button');
|
|
||||||
behindBtn.textContent = 'Behind';
|
|
||||||
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.addEventListener('click', () => { config.overlayMode = 'above'; saveConfig(['overlayMode']); ensureHook(); updateUI(); });
|
|
||||||
layeringBtns.appendChild(behindBtn);
|
|
||||||
layeringBtns.appendChild(aboveBtn);
|
|
||||||
|
|
||||||
|
const layeringBtns = $('op-layering-btns') as HTMLDivElement | null;
|
||||||
|
if (layeringBtns) {
|
||||||
|
layeringBtns.innerHTML = '';
|
||||||
|
const behindBtn = document.createElement('button');
|
||||||
|
behindBtn.textContent = 'Behind';
|
||||||
|
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.addEventListener('click', () => { config.overlayMode = 'above'; saveConfig(['overlayMode']); ensureHook(); updateUI(); });
|
||||||
|
layeringBtns.appendChild(behindBtn);
|
||||||
|
layeringBtns.appendChild(aboveBtn);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Positioning Section ---
|
// --- Positioning Section ---
|
||||||
const autoBtn = $('op-autocap-toggle');
|
const autoBtn = $('op-autocap-toggle');
|
||||||
const placeLabel = $('op-place-label');
|
const placeLabel = $('op-place-label');
|
||||||
autoBtn.textContent = config.autoCapturePixelUrl ? 'Enabled' : 'Disabled';
|
if (autoBtn) {
|
||||||
autoBtn.classList.toggle('op-danger', !!config.autoCapturePixelUrl);
|
autoBtn.textContent = config.autoCapturePixelUrl ? 'Enabled' : 'Disabled';
|
||||||
if(placeLabel) placeLabel.classList.toggle('op-danger-text', !!config.autoCapturePixelUrl);
|
autoBtn.classList.toggle('op-danger', !!config.autoCapturePixelUrl);
|
||||||
|
}
|
||||||
|
if (placeLabel) placeLabel.classList.toggle('op-danger-text', !!config.autoCapturePixelUrl);
|
||||||
|
|
||||||
const positioningBody = $('op-positioning-body');
|
const positioningBody = $('op-positioning-body');
|
||||||
const positioningCz = $('op-collapse-positioning');
|
const positioningCz = $('op-collapse-positioning');
|
||||||
if(positioningBody) positioningBody.style.display = config.collapsePositioning ? 'none' : 'block';
|
if (positioningBody) positioningBody.style.display = config.collapsePositioning ? 'none' : 'block';
|
||||||
if (positioningCz) positioningCz.textContent = config.collapsePositioning ? '▸' : '▾';
|
if (positioningCz) (positioningCz as HTMLButtonElement).textContent = config.collapsePositioning ? '▸' : '▾';
|
||||||
|
|
||||||
const listWrap = $('op-list-wrap');
|
const listWrap = $('op-list-wrap') as HTMLDivElement | null;
|
||||||
const listCz = $('op-collapse-list');
|
const listCz = $('op-collapse-list');
|
||||||
listWrap.style.display = config.collapseList ? 'none' : 'block';
|
if (listWrap) listWrap.style.display = config.collapseList ? 'none' : 'block';
|
||||||
if (listCz) listCz.textContent = config.collapseList ? '▸' : '▾';
|
if (listCz) (listCz as HTMLButtonElement).textContent = config.collapseList ? '▸' : '▾';
|
||||||
|
|
||||||
rebuildOverlayListUI();
|
rebuildOverlayListUI();
|
||||||
updateEditorUI();
|
updateEditorUI();
|
||||||
|
|
||||||
const exportBtn = $('op-export-overlay') as HTMLButtonElement;
|
const exportBtn = $('op-export-overlay') as HTMLButtonElement | null;
|
||||||
const ov = getActiveOverlay();
|
const ov = getActiveOverlay();
|
||||||
const canExport = !!(ov && ov.imageUrl && !ov.isLocal);
|
const canExport = !!(ov?.imageUrl && !ov?.isLocal);
|
||||||
exportBtn.disabled = !canExport;
|
if (exportBtn) {
|
||||||
exportBtn.title = canExport ? 'Export active overlay JSON' : 'Export disabled for local images';
|
exportBtn.disabled = !canExport;
|
||||||
|
exportBtn.title = canExport ? 'Export active overlay JSON' : 'Export disabled for local images';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/// <reference types="tampermonkey" />
|
/// <reference types="tampermonkey" />
|
||||||
import { createCanvas, createHTMLCanvas, canvasToDataURLSafe, loadImage } from '../core/canvas';
|
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 { MAX_OVERLAY_DIM } from '../core/constants';
|
||||||
import { ensureHook } from '../core/hook';
|
import { ensureHook } from '../core/hook';
|
||||||
import { clearOverlayCache } from '../core/cache';
|
import { clearOverlayCache } from '../core/cache';
|
||||||
|
|
@ -291,10 +291,26 @@ export function buildRSModal() {
|
||||||
closeBtn: modal.querySelector('#op-rs-close') as HTMLButtonElement,
|
closeBtn: modal.querySelector('#op-rs-close') as HTMLButtonElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ctxPrev = refs.preview.getContext('2d', { willReadFrequently: true })!;
|
const ctxPrev: CanvasRenderingContext2D = (() => {
|
||||||
const ctxSimOrig = refs.simOrig.getContext('2d', { willReadFrequently: true })!;
|
const ctx = refs.preview.getContext('2d', { willReadFrequently: true });
|
||||||
const ctxSimNew = refs.simNew.getContext('2d', { willReadFrequently: true })!;
|
if (!ctx) throw new Error('Failed to get 2d context for preview canvas.');
|
||||||
const ctxRes = refs.resCanvas.getContext('2d', { willReadFrequently: true })!;
|
return ctx;
|
||||||
|
})();
|
||||||
|
const ctxSimOrig: CanvasRenderingContext2D = (() => {
|
||||||
|
const ctx = refs.simOrig.getContext('2d', { willReadFrequently: true });
|
||||||
|
if (!ctx) throw new Error('Failed to get 2d context for simOrig canvas.');
|
||||||
|
return ctx;
|
||||||
|
})();
|
||||||
|
const ctxSimNew: CanvasRenderingContext2D = (() => {
|
||||||
|
const ctx = refs.simNew.getContext('2d', { willReadFrequently: true });
|
||||||
|
if (!ctx) throw new Error('Failed to get 2d context for simNew canvas.');
|
||||||
|
return ctx;
|
||||||
|
})();
|
||||||
|
const ctxRes: CanvasRenderingContext2D = (() => {
|
||||||
|
const ctx = refs.resCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
if (!ctx) throw new Error('Failed to get 2d context for resCanvas.');
|
||||||
|
return ctx;
|
||||||
|
})();
|
||||||
|
|
||||||
rs = {
|
rs = {
|
||||||
...refs,
|
...refs,
|
||||||
|
|
@ -321,65 +337,70 @@ export function buildRSModal() {
|
||||||
calcReady: false,
|
calcReady: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const s = rs; // local non-null alias within this scope
|
||||||
|
|
||||||
function computeSimpleFooterText() {
|
function computeSimpleFooterText() {
|
||||||
const W = parseInt(rs!.w.value||'0',10);
|
const W = parseInt(s.w.value || '0', 10);
|
||||||
const H = parseInt(rs!.h.value||'0',10);
|
const H = parseInt(s.h.value || '0', 10);
|
||||||
const ok = Number.isFinite(W) && Number.isFinite(H) && W>0 && H>0;
|
const ok = Number.isFinite(W) && Number.isFinite(H) && W > 0 && H > 0;
|
||||||
const limit = (W >= MAX_OVERLAY_DIM || H >= MAX_OVERLAY_DIM);
|
const limit = (W >= MAX_OVERLAY_DIM || H >= MAX_OVERLAY_DIM);
|
||||||
return ok ? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})`
|
return ok ? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})`
|
||||||
: `Target: ${W}×${H} (OK)`)
|
: `Target: ${W}×${H} (OK)`)
|
||||||
: 'Enter positive width and height.';
|
: 'Enter positive width and height.';
|
||||||
}
|
}
|
||||||
function sampleDims() {
|
function sampleDims() {
|
||||||
const cols = Math.floor((rs!.origW - rs!.offx) / rs!.gapX);
|
const cols = Math.floor((s.origW - s.offx) / s.gapX);
|
||||||
const rows = Math.floor((rs!.origH - rs!.offy) / rs!.gapY);
|
const rows = Math.floor((s.origH - s.offy) / s.gapY);
|
||||||
return { cols: Math.max(0, cols), rows: Math.max(0, rows) };
|
return { cols: Math.max(0, cols), rows: Math.max(0, rows) };
|
||||||
}
|
}
|
||||||
function computeAdvancedFooterText() {
|
function computeAdvancedFooterText() {
|
||||||
const { cols, rows } = sampleDims();
|
const { cols, rows } = sampleDims();
|
||||||
const limit = (cols >= MAX_OVERLAY_DIM || rows >= MAX_OVERLAY_DIM);
|
const limit = (cols >= MAX_OVERLAY_DIM || rows >= MAX_OVERLAY_DIM);
|
||||||
return (cols>0 && rows>0)
|
return (cols > 0 && rows > 0)
|
||||||
? `Samples: ${cols} × ${rows} | Output: ${cols}×${rows}${limit ? ` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : ''}`
|
? `Samples: ${cols} × ${rows} | Output: ${cols}×${rows}${limit ? ` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : ''}`
|
||||||
: 'Adjust multiplier/offset until dots sit at centers.';
|
: 'Adjust multiplier/offset until dots sit at centers.';
|
||||||
}
|
}
|
||||||
const updateFooterMeta = () => {
|
const updateFooterMeta = () => {
|
||||||
rs!.meta.textContent = (rs!.mode === 'advanced') ? computeAdvancedFooterText() : computeSimpleFooterText();
|
s.meta.textContent = (s.mode === 'advanced') ? computeAdvancedFooterText() : computeSimpleFooterText();
|
||||||
};
|
};
|
||||||
|
|
||||||
function drawSimplePreview() {
|
function drawSimplePreview() {
|
||||||
if (!rs!.img) return;
|
if (!s.img) return;
|
||||||
const leftLabelH = rs!.colLeft.querySelector('.pad-top')!.clientHeight;
|
const padTopL = s.colLeft.querySelector('.pad-top') as HTMLElement | null;
|
||||||
const rightLabelH = rs!.colRight.querySelector('.pad-top')!.clientHeight;
|
const padTopR = s.colRight.querySelector('.pad-top') as HTMLElement | null;
|
||||||
const leftW = rs!.colLeft.clientWidth;
|
const leftLabelH = padTopL ? padTopL.clientHeight : 0;
|
||||||
const rightW = rs!.colRight.clientWidth;
|
const rightLabelH = padTopR ? padTopR.clientHeight : 0;
|
||||||
const leftH = rs!.colLeft.clientHeight - leftLabelH;
|
const leftW = s.colLeft.clientWidth;
|
||||||
const rightH = rs!.colRight.clientHeight - rightLabelH;
|
const rightW = s.colRight.clientWidth;
|
||||||
|
const leftH = s.colLeft.clientHeight - leftLabelH;
|
||||||
|
const rightH = s.colRight.clientHeight - rightLabelH;
|
||||||
|
|
||||||
rs!.simOrig.width = leftW; rs!.simOrig.height = leftH;
|
s.simOrig.width = leftW; s.simOrig.height = leftH;
|
||||||
rs!.simNew.width = rightW; rs!.simNew.height = rightH;
|
s.simNew.width = rightW; s.simNew.height = rightH;
|
||||||
|
|
||||||
ctxSimOrig.save();
|
ctxSimOrig.save();
|
||||||
ctxSimOrig.imageSmoothingEnabled = false;
|
ctxSimOrig.imageSmoothingEnabled = false;
|
||||||
ctxSimOrig.clearRect(0,0,leftW,leftH);
|
ctxSimOrig.clearRect(0,0,leftW,leftH);
|
||||||
const sFit = Math.min(leftW / rs!.origW, leftH / rs!.origH);
|
const sFit = Math.min(leftW / s.origW, leftH / s.origH);
|
||||||
const dW = Math.max(1, Math.floor(rs!.origW * sFit));
|
const dW = Math.max(1, Math.floor(s.origW * sFit));
|
||||||
const dH = Math.max(1, Math.floor(rs!.origH * sFit));
|
const dH = Math.max(1, Math.floor(s.origH * sFit));
|
||||||
const dx0 = Math.floor((leftW - dW) / 2);
|
const dx0 = Math.floor((leftW - dW) / 2);
|
||||||
const dy0 = Math.floor((leftH - dH) / 2);
|
const dy0 = Math.floor((leftH - dH) / 2);
|
||||||
ctxSimOrig.drawImage(rs!.img!, 0,0, rs!.origW,rs!.origH, dx0,dy0, dW,dH);
|
ctxSimOrig.drawImage(s.img, 0,0, s.origW,s.origH, dx0,dy0, dW,dH);
|
||||||
ctxSimOrig.restore();
|
ctxSimOrig.restore();
|
||||||
|
|
||||||
const W = parseInt(rs!.w.value||'0',10);
|
const W = parseInt(s.w.value || '0', 10);
|
||||||
const H = parseInt(rs!.h.value||'0',10);
|
const H = parseInt(s.h.value || '0', 10);
|
||||||
ctxSimNew.save();
|
ctxSimNew.save();
|
||||||
ctxSimNew.imageSmoothingEnabled = false;
|
ctxSimNew.imageSmoothingEnabled = false;
|
||||||
ctxSimNew.clearRect(0,0,rightW,rightH);
|
ctxSimNew.clearRect(0,0,rightW,rightH);
|
||||||
if (Number.isFinite(W) && Number.isFinite(H) && W>0 && H>0) {
|
if (Number.isFinite(W) && Number.isFinite(H) && W>0 && H>0) {
|
||||||
const tiny = createCanvas(W, H) as any;
|
const tiny = createCanvas(W, H) as HTMLCanvasElement;
|
||||||
const tctx = tiny.getContext('2d', { willReadFrequently: true })!;
|
const tctx = tiny.getContext('2d', { willReadFrequently: true });
|
||||||
|
if (!tctx) throw new Error('Failed to get 2d context for tiny canvas.');
|
||||||
tctx.imageSmoothingEnabled = false;
|
tctx.imageSmoothingEnabled = false;
|
||||||
tctx.clearRect(0,0,W,H);
|
tctx.clearRect(0,0,W,H);
|
||||||
tctx.drawImage(rs!.img!, 0,0, rs!.origW,rs!.origH, 0,0, W,H);
|
tctx.drawImage(s.img, 0,0, s.origW,s.origH, 0,0, W,H);
|
||||||
const id = tctx.getImageData(0,0,W,H);
|
const id = tctx.getImageData(0,0,W,H);
|
||||||
const data = id.data;
|
const data = id.data;
|
||||||
for (let i=0;i<data.length;i+=4) { if (data[i+3] !== 0) data[i+3]=255; }
|
for (let i=0;i<data.length;i+=4) { if (data[i+3] !== 0) data[i+3]=255; }
|
||||||
|
|
@ -392,64 +413,63 @@ export function buildRSModal() {
|
||||||
const dy2 = Math.floor((rightH - dH2)/2);
|
const dy2 = Math.floor((rightH - dH2)/2);
|
||||||
ctxSimNew.drawImage(tiny, 0,0, W,H, dx2,dy2, dW2,dH2);
|
ctxSimNew.drawImage(tiny, 0,0, W,H, dx2,dy2, dW2,dH2);
|
||||||
} else {
|
} else {
|
||||||
ctxSimNew.drawImage(rs!.img!, 0,0, rs!.origW,rs!.origH, dx0,dy0, dW,dH);
|
ctxSimNew.drawImage(s.img, 0,0, s.origW,s.origH, dx0,dy0, dW,dH);
|
||||||
}
|
}
|
||||||
ctxSimNew.restore();
|
ctxSimNew.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncAdvancedMeta() {
|
function syncAdvancedMeta() {
|
||||||
const { cols, rows } = sampleDims();
|
sampleDims();
|
||||||
const limit = (cols >= MAX_OVERLAY_DIM || rows >= MAX_OVERLAY_DIM);
|
if (s.mode === 'advanced') {
|
||||||
if (rs!.mode === 'advanced') {
|
s.applyBtn.disabled = !s.calcReady;
|
||||||
rs!.applyBtn.disabled = !rs!.calcReady;
|
|
||||||
} else {
|
} else {
|
||||||
const W = parseInt(rs!.w.value||'0',10), H = parseInt(rs!.h.value||'0',10);
|
const W = parseInt(s.w.value||'0',10), H = parseInt(s.h.value||'0',10);
|
||||||
const ok = Number.isFinite(W)&&Number.isFinite(H)&&W>0&&H>0&&W<MAX_OVERLAY_DIM&&H<MAX_OVERLAY_DIM;
|
const ok = Number.isFinite(W)&&Number.isFinite(H)&&W>0&&H>0&&W<MAX_OVERLAY_DIM&&H<MAX_OVERLAY_DIM;
|
||||||
rs!.applyBtn.disabled = !ok;
|
s.applyBtn.disabled = !ok;
|
||||||
}
|
}
|
||||||
updateFooterMeta();
|
updateFooterMeta();
|
||||||
}
|
}
|
||||||
function drawAdvancedPreview() {
|
function drawAdvancedPreview() {
|
||||||
if (rs!.mode !== 'advanced' || !rs!.img) return;
|
if (s.mode !== 'advanced' || !s.img) return;
|
||||||
const w = rs!.origW, h = rs!.origH;
|
const w = s.origW, h = s.origH;
|
||||||
|
|
||||||
const destW = Math.max(50, Math.floor(rs!.advWrap.clientWidth));
|
const destW = Math.max(50, Math.floor(s.advWrap.clientWidth));
|
||||||
const destH = Math.max(50, Math.floor(rs!.advWrap.clientHeight));
|
const destH = Math.max(50, Math.floor(s.advWrap.clientHeight));
|
||||||
rs!.preview.width = destW;
|
s.preview.width = destW;
|
||||||
rs!.preview.height = destH;
|
s.preview.height = destH;
|
||||||
|
|
||||||
const sw = Math.max(1, Math.floor(destW / rs!.zoom));
|
const sw = Math.max(1, Math.floor(destW / s.zoom));
|
||||||
const sh = Math.max(1, Math.floor(destH / rs!.zoom));
|
const sh = Math.max(1, Math.floor(destH / s.zoom));
|
||||||
const maxX = Math.max(0, w - sw);
|
const maxX = Math.max(0, w - sw);
|
||||||
const maxY = Math.max(0, h - sh);
|
const maxY = Math.max(0, h - sh);
|
||||||
rs!.viewX = Math.min(Math.max(0, rs!.viewX), maxX);
|
s.viewX = Math.min(Math.max(0, s.viewX), maxX);
|
||||||
rs!.viewY = Math.min(Math.max(0, rs!.viewY), maxY);
|
s.viewY = Math.min(Math.max(0, s.viewY), maxY);
|
||||||
|
|
||||||
ctxPrev.save();
|
ctxPrev.save();
|
||||||
ctxPrev.imageSmoothingEnabled = false;
|
ctxPrev.imageSmoothingEnabled = false;
|
||||||
ctxPrev.clearRect(0,0,destW,destH);
|
ctxPrev.clearRect(0,0,destW,destH);
|
||||||
ctxPrev.drawImage(rs!.img!, rs!.viewX, rs!.viewY, sw, sh, 0, 0, destW, destH);
|
ctxPrev.drawImage(s.img, s.viewX, s.viewY, sw, sh, 0, 0, destW, destH);
|
||||||
|
|
||||||
if (rs!.gridToggle.checked && rs!.gapX >= 1 && rs!.gapY >= 1) {
|
if (s.gridToggle.checked && s.gapX >= 1 && s.gapY >= 1) {
|
||||||
ctxPrev.strokeStyle = 'rgba(255,59,48,0.45)';
|
ctxPrev.strokeStyle = 'rgba(255,59,48,0.45)';
|
||||||
ctxPrev.lineWidth = 1;
|
ctxPrev.lineWidth = 1;
|
||||||
const startGX = Math.ceil((rs!.viewX - rs!.offx) / rs!.gapX);
|
const startGX = Math.ceil((s.viewX - s.offx) / s.gapX);
|
||||||
const endGX = Math.floor((rs!.viewX + sw - rs!.offx) / rs!.gapX);
|
const endGX = Math.floor((s.viewX + sw - s.offx) / s.gapX);
|
||||||
const startGY = Math.ceil((rs!.viewY - rs!.offy) / rs!.gapY);
|
const startGY = Math.ceil((s.viewY - s.offy) / s.gapY);
|
||||||
const endGY = Math.floor((rs!.viewY + sh - rs!.offy) / rs!.gapY);
|
const endGY = Math.floor((s.viewY + sh - s.offy) / s.gapY);
|
||||||
const linesX = Math.max(0, endGX - startGX + 1);
|
const linesX = Math.max(0, endGX - startGX + 1);
|
||||||
const linesY = Math.max(0, endGY - startGY + 1);
|
const linesY = Math.max(0, endGY - startGY + 1);
|
||||||
if (linesX <= 4000 && linesY <= 4000) {
|
if (linesX <= 4000 && linesY <= 4000) {
|
||||||
ctxPrev.beginPath();
|
ctxPrev.beginPath();
|
||||||
for (let gx = startGX; gx <= endGX; gx++) {
|
for (let gx = startGX; gx <= endGX; gx++) {
|
||||||
const x = rs!.offx + gx * rs!.gapX;
|
const x = s.offx + gx * s.gapX;
|
||||||
const px = Math.round((x - rs!.viewX) * rs!.zoom);
|
const px = Math.round((x - s.viewX) * s.zoom);
|
||||||
ctxPrev.moveTo(px + 0.5, 0);
|
ctxPrev.moveTo(px + 0.5, 0);
|
||||||
ctxPrev.lineTo(px + 0.5, destH);
|
ctxPrev.lineTo(px + 0.5, destH);
|
||||||
}
|
}
|
||||||
for (let gy = startGY; gy <= endGY; gy++) {
|
for (let gy = startGY; gy <= endGY; gy++) {
|
||||||
const y = rs!.offy + gy * rs!.gapY;
|
const y = s.offy + gy * s.gapY;
|
||||||
const py = Math.round((y - rs!.viewY) * rs!.zoom);
|
const py = Math.round((y - s.viewY) * s.zoom);
|
||||||
ctxPrev.moveTo(0, py + 0.5);
|
ctxPrev.moveTo(0, py + 0.5);
|
||||||
ctxPrev.lineTo(destW, py + 0.5);
|
ctxPrev.lineTo(destW, py + 0.5);
|
||||||
}
|
}
|
||||||
|
|
@ -457,26 +477,26 @@ export function buildRSModal() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rs!.gapX >= 1 && rs!.gapY >= 1) {
|
if (s.gapX >= 1 && s.gapY >= 1) {
|
||||||
ctxPrev.fillStyle = '#ff3b30';
|
ctxPrev.fillStyle = '#ff3b30';
|
||||||
const cx0 = rs!.offx + Math.floor(rs!.gapX/2);
|
const cx0 = s.offx + Math.floor(s.gapX/2);
|
||||||
const cy0 = rs!.offy + Math.floor(rs!.gapY/2);
|
const cy0 = s.offy + Math.floor(s.gapY/2);
|
||||||
if (cx0 >= 0 && cy0 >= 0) {
|
if (cx0 >= 0 && cy0 >= 0) {
|
||||||
const startX = Math.ceil((rs!.viewX - cx0) / rs!.gapX);
|
const startX = Math.ceil((s.viewX - cx0) / s.gapX);
|
||||||
const startY = Math.ceil((rs!.viewY - cy0) / rs!.gapY);
|
const startY = Math.ceil((s.viewY - cy0) / s.gapY);
|
||||||
const endY = Math.floor((rs!.viewY + sh - 1 - cy0) / rs!.gapY);
|
const endY = Math.floor((s.viewY + sh - 1 - cy0) / s.gapY);
|
||||||
const endX2 = Math.floor((rs!.viewX + sw - 1 - cx0) / rs!.gapX);
|
const endX2 = Math.floor((s.viewX + sw - 1 - cx0) / s.gapX);
|
||||||
const r = rs!.dotr;
|
const r = s.dotr;
|
||||||
const dotsX = Math.max(0, endX2 - startX + 1);
|
const dotsX = Math.max(0, endX2 - startX + 1);
|
||||||
const dotsY = Math.max(0, endY - startY + 1);
|
const dotsY = Math.max(0, endY - startY + 1);
|
||||||
const maxDots = 300000;
|
const maxDots = 300000;
|
||||||
if (dotsX * dotsY <= maxDots) {
|
if (dotsX * dotsY <= maxDots) {
|
||||||
for (let gy = startY; gy <= endY; gy++) {
|
for (let gy = startY; gy <= endY; gy++) {
|
||||||
const y = cy0 + gy * rs!.gapY;
|
const y = cy0 + gy * s.gapY;
|
||||||
for (let gx = startX; gx <= endX2; gx++) {
|
for (let gx = startX; gx <= endX2; gx++) {
|
||||||
const x = cx0 + gx * rs!.gapX;
|
const x = cx0 + gx * s.gapX;
|
||||||
const px = Math.round((x - rs!.viewX) * rs!.zoom);
|
const px = Math.round((x - s.viewX) * s.zoom);
|
||||||
const py = Math.round((y - rs!.viewY) * rs!.zoom);
|
const py = Math.round((y - s.viewY) * s.zoom);
|
||||||
ctxPrev.beginPath();
|
ctxPrev.beginPath();
|
||||||
ctxPrev.arc(px, py, r, 0, Math.PI*2);
|
ctxPrev.arc(px, py, r, 0, Math.PI*2);
|
||||||
ctxPrev.fill();
|
ctxPrev.fill();
|
||||||
|
|
@ -489,44 +509,44 @@ export function buildRSModal() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawAdvancedResultPreview() {
|
function drawAdvancedResultPreview() {
|
||||||
const canvas = rs!.calcCanvas;
|
const canvas = s.calcCanvas;
|
||||||
const wrap = rs!.resWrap;
|
const wrap = s.resWrap;
|
||||||
if (!wrap || !canvas) {
|
if (!wrap || !canvas) {
|
||||||
ctxRes.clearRect(0,0, rs!.resCanvas.width, rs!.resCanvas.height);
|
ctxRes.clearRect(0,0, s.resCanvas.width, s.resCanvas.height);
|
||||||
rs!.resMeta.textContent = 'No result. Click Calculate.';
|
s.resMeta.textContent = 'No result. Click Calculate.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const W = canvas.width, H = canvas.height;
|
const W = canvas.width, H = canvas.height;
|
||||||
const availW = Math.max(50, Math.floor(wrap.clientWidth - 16));
|
const availW = Math.max(50, Math.floor(wrap.clientWidth - 16));
|
||||||
const availH = Math.max(50, Math.floor(wrap.clientHeight - 16));
|
const availH = Math.max(50, Math.floor(wrap.clientHeight - 16));
|
||||||
const s = Math.min(availW / W, availH / H);
|
const scale = Math.min(availW / W, availH / H);
|
||||||
const dW = Math.max(1, Math.floor(W * s));
|
const dW = Math.max(1, Math.floor(W * scale));
|
||||||
const dH = Math.max(1, Math.floor(H * s));
|
const dH = Math.max(1, Math.floor(H * scale));
|
||||||
rs!.resCanvas.width = dW;
|
s.resCanvas.width = dW;
|
||||||
rs!.resCanvas.height = dH;
|
s.resCanvas.height = dH;
|
||||||
ctxRes.save();
|
ctxRes.save();
|
||||||
ctxRes.imageSmoothingEnabled = false;
|
ctxRes.imageSmoothingEnabled = false;
|
||||||
ctxRes.clearRect(0,0,dW,dH);
|
ctxRes.clearRect(0,0,dW,dH);
|
||||||
ctxRes.drawImage(canvas, 0,0, W,H, 0,0, dW,dH);
|
ctxRes.drawImage(canvas, 0,0, W,H, 0,0, dW,dH);
|
||||||
ctxRes.restore();
|
ctxRes.restore();
|
||||||
rs!.resMeta.textContent = `Output: ${W}×${H}${(W>=MAX_OVERLAY_DIM||H>=MAX_OVERLAY_DIM) ? ` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : ''}`;
|
s.resMeta.textContent = `Output: ${W}×${H}${(W>=MAX_OVERLAY_DIM||H>=MAX_OVERLAY_DIM) ? ` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
rs._drawSimplePreview = drawSimplePreview;
|
s._drawSimplePreview = drawSimplePreview;
|
||||||
rs._drawAdvancedPreview = drawAdvancedPreview;
|
s._drawAdvancedPreview = drawAdvancedPreview;
|
||||||
rs._drawAdvancedResultPreview = drawAdvancedResultPreview;
|
s._drawAdvancedResultPreview = drawAdvancedResultPreview;
|
||||||
|
|
||||||
const setMode = (m: 'simple'|'advanced') => {
|
const setMode = (m: 'simple'|'advanced') => {
|
||||||
rs!.mode = m;
|
s.mode = m;
|
||||||
rs!.tabSimple.classList.toggle('active', m === 'simple');
|
s.tabSimple.classList.toggle('active', m === 'simple');
|
||||||
rs!.tabAdvanced.classList.toggle('active', m === 'advanced');
|
s.tabAdvanced.classList.toggle('active', m === 'advanced');
|
||||||
rs!.paneSimple.classList.toggle('show', m === 'simple');
|
s.paneSimple.classList.toggle('show', m === 'simple');
|
||||||
rs!.paneAdvanced.classList.toggle('show', m === 'advanced');
|
s.paneAdvanced.classList.toggle('show', m === 'advanced');
|
||||||
updateFooterMeta();
|
updateFooterMeta();
|
||||||
|
|
||||||
rs!.calcBtn.style.display = (m === 'advanced') ? 'inline-block' : 'none';
|
s.calcBtn.style.display = (m === 'advanced') ? 'inline-block' : 'none';
|
||||||
if (m === 'advanced') {
|
if (m === 'advanced') {
|
||||||
rs!.applyBtn.disabled = !rs!.calcReady;
|
s.applyBtn.disabled = !s.calcReady;
|
||||||
} else {
|
} else {
|
||||||
syncSimpleNote();
|
syncSimpleNote();
|
||||||
}
|
}
|
||||||
|
|
@ -539,144 +559,144 @@ export function buildRSModal() {
|
||||||
drawSimplePreview();
|
drawSimplePreview();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
rs._setMode = (m) => {
|
s._setMode = (m) => {
|
||||||
const evt = new Event('click');
|
const evt = new Event('click');
|
||||||
(m === 'simple' ? rs!.tabSimple : rs!.tabAdvanced).dispatchEvent(evt);
|
(m === 'simple' ? s.tabSimple : s.tabAdvanced).dispatchEvent(evt);
|
||||||
};
|
};
|
||||||
rs.tabSimple.addEventListener('click', () => setMode('simple'));
|
s.tabSimple.addEventListener('click', () => setMode('simple'));
|
||||||
rs.tabAdvanced.addEventListener('click', () => setMode('advanced'));
|
s.tabAdvanced.addEventListener('click', () => setMode('advanced'));
|
||||||
|
|
||||||
function onWidthInput() {
|
function onWidthInput() {
|
||||||
if (rs!.updating) return;
|
if (s.updating) return;
|
||||||
rs!.updating = true;
|
s.updating = true;
|
||||||
const W = parseInt(rs!.w.value||'0',10);
|
const W = parseInt(s.w.value||'0',10);
|
||||||
if (rs!.lock.checked && rs!.origW>0 && rs!.origH>0 && W>0) {
|
if (s.lock.checked && s.origW>0 && s.origH>0 && W>0) {
|
||||||
rs!.h.value = String(Math.max(1, Math.round(W * rs!.origH / rs!.origW)));
|
s.h.value = String(Math.max(1, Math.round(W * s.origH / s.origW)));
|
||||||
}
|
}
|
||||||
rs!.updating = false;
|
s.updating = false;
|
||||||
syncSimpleNote();
|
syncSimpleNote();
|
||||||
if (rs!.mode === 'simple') drawSimplePreview();
|
if (s.mode === 'simple') drawSimplePreview();
|
||||||
}
|
}
|
||||||
function onHeightInput() {
|
function onHeightInput() {
|
||||||
if (rs!.updating) return;
|
if (s.updating) return;
|
||||||
rs!.updating = true;
|
s.updating = true;
|
||||||
const H = parseInt(rs!.h.value||'0',10);
|
const H = parseInt(s.h.value||'0',10);
|
||||||
if (rs!.lock.checked && rs!.origW>0 && rs!.origH>0 && H>0) {
|
if (s.lock.checked && s.origW>0 && s.origH>0 && H>0) {
|
||||||
rs!.w.value = String(Math.max(1, Math.round(H * rs!.origW / rs!.origH)));
|
s.w.value = String(Math.max(1, Math.round(H * s.origW / s.origH)));
|
||||||
}
|
}
|
||||||
rs!.updating = false;
|
s.updating = false;
|
||||||
syncSimpleNote();
|
syncSimpleNote();
|
||||||
if (rs!.mode === 'simple') drawSimplePreview();
|
if (s.mode === 'simple') drawSimplePreview();
|
||||||
}
|
}
|
||||||
rs.w.addEventListener('input', onWidthInput);
|
s.w.addEventListener('input', onWidthInput);
|
||||||
rs.h.addEventListener('input', onHeightInput);
|
s.h.addEventListener('input', onHeightInput);
|
||||||
rs.onex.addEventListener('click', () => { applyScaleToFields(1); drawSimplePreview(); });
|
s.onex.addEventListener('click', () => { applyScaleToFields(1); drawSimplePreview(); });
|
||||||
rs.half.addEventListener('click', () => { applyScaleToFields(0.5); drawSimplePreview(); });
|
s.half.addEventListener('click', () => { applyScaleToFields(0.5); drawSimplePreview(); });
|
||||||
rs.third.addEventListener('click', () => { applyScaleToFields(1/3); drawSimplePreview(); });
|
s.third.addEventListener('click', () => { applyScaleToFields(1/3); drawSimplePreview(); });
|
||||||
rs.quarter.addEventListener('click', () => { applyScaleToFields(1/4); drawSimplePreview(); });
|
s.quarter.addEventListener('click', () => { applyScaleToFields(1/4); drawSimplePreview(); });
|
||||||
rs.double.addEventListener('click', () => { applyScaleToFields(2); drawSimplePreview(); });
|
s.double.addEventListener('click', () => { applyScaleToFields(2); drawSimplePreview(); });
|
||||||
rs.applyScale.addEventListener('click', () => {
|
s.applyScale.addEventListener('click', () => {
|
||||||
const s = parseFloat(rs!.scale.value||'');
|
const scaleVal = parseFloat(s.scale.value||'');
|
||||||
if (!Number.isFinite(s) || s<=0) { showToast('Enter a valid scale factor > 0'); return; }
|
if (!Number.isFinite(scaleVal) || scaleVal<=0) { showToast('Enter a valid scale factor > 0'); return; }
|
||||||
applyScaleToFields(s);
|
applyScaleToFields(scaleVal);
|
||||||
drawSimplePreview();
|
drawSimplePreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
const markCalcStale = () => {
|
const markCalcStale = () => {
|
||||||
if (rs!.mode === 'advanced') {
|
if (s.mode === 'advanced') {
|
||||||
rs!.calcReady = false;
|
s.calcReady = false;
|
||||||
rs!.applyBtn.disabled = true;
|
s.applyBtn.disabled = true;
|
||||||
drawAdvancedResultPreview();
|
drawAdvancedResultPreview();
|
||||||
updateFooterMeta();
|
updateFooterMeta();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMultChange = (v: string) => {
|
const onMultChange = (v: string) => {
|
||||||
if (rs!.updating) return;
|
if (s.updating) return;
|
||||||
const parsed = parseFloat(v);
|
const parsed = parseFloat(v);
|
||||||
if (!Number.isFinite(parsed)) return;
|
if (!Number.isFinite(parsed)) return;
|
||||||
const clamped = Math.min(Math.max(parsed, 1), 128);
|
const clamped = Math.min(Math.max(parsed, 1), 128);
|
||||||
rs!.mult = clamped;
|
s.mult = clamped;
|
||||||
if (rs!.bind.checked) { rs!.gapX = clamped; rs!.gapY = clamped; }
|
if (s.bind.checked) { s.gapX = clamped; s.gapY = clamped; }
|
||||||
syncAdvFieldsToState();
|
syncAdvFieldsToState();
|
||||||
syncAdvancedMeta();
|
syncAdvancedMeta();
|
||||||
drawAdvancedPreview();
|
drawAdvancedPreview();
|
||||||
markCalcStale();
|
markCalcStale();
|
||||||
};
|
};
|
||||||
rs.multRange.addEventListener('input', (e) => onMultChange((e.target as HTMLInputElement).value));
|
s.multRange.addEventListener('input', (e) => onMultChange((e.target as HTMLInputElement).value));
|
||||||
rs.multInput.addEventListener('input', (e) => {
|
s.multInput.addEventListener('input', (e) => {
|
||||||
const v = (e.target as HTMLInputElement).value;
|
const v = (e.target as HTMLInputElement).value;
|
||||||
if (!Number.isFinite(parseFloat(v))) return;
|
if (!Number.isFinite(parseFloat(v))) return;
|
||||||
onMultChange(v);
|
onMultChange(v);
|
||||||
});
|
});
|
||||||
rs.bind.addEventListener('change', () => {
|
s.bind.addEventListener('change', () => {
|
||||||
if (rs!.bind.checked) { rs!.gapX = rs!.mult; rs!.gapY = rs!.mult; syncAdvFieldsToState(); }
|
if (s.bind.checked) { s.gapX = s.mult; s.gapY = s.mult; syncAdvFieldsToState(); }
|
||||||
syncAdvancedMeta();
|
syncAdvancedMeta();
|
||||||
drawAdvancedPreview();
|
drawAdvancedPreview();
|
||||||
markCalcStale();
|
markCalcStale();
|
||||||
});
|
});
|
||||||
rs.blockW.addEventListener('input', (e) => {
|
s.blockW.addEventListener('input', (e) => {
|
||||||
const val = parseFloat((e.target as HTMLInputElement).value);
|
const val = parseFloat((e.target as HTMLInputElement).value);
|
||||||
if (!Number.isFinite(val)) return;
|
if (!Number.isFinite(val)) return;
|
||||||
rs!.gapX = Math.min(Math.max(val, 1), 4096);
|
s.gapX = Math.min(Math.max(val, 1), 4096);
|
||||||
if (rs!.bind.checked) { rs!.mult = rs!.gapX; rs!.gapY = rs!.gapX; }
|
if (s.bind.checked) { s.mult = s.gapX; s.gapY = s.gapX; }
|
||||||
syncAdvFieldsToState();
|
syncAdvFieldsToState();
|
||||||
syncAdvancedMeta();
|
syncAdvancedMeta();
|
||||||
drawAdvancedPreview();
|
drawAdvancedPreview();
|
||||||
markCalcStale();
|
markCalcStale();
|
||||||
});
|
});
|
||||||
rs.blockH.addEventListener('input', (e) => {
|
s.blockH.addEventListener('input', (e) => {
|
||||||
const val = parseFloat((e.target as HTMLInputElement).value);
|
const val = parseFloat((e.target as HTMLInputElement).value);
|
||||||
if (!Number.isFinite(val)) return;
|
if (!Number.isFinite(val)) return;
|
||||||
rs!.gapY = Math.min(Math.max(val, 1), 4096);
|
s.gapY = Math.min(Math.max(val, 1), 4096);
|
||||||
if (rs!.bind.checked) { rs!.mult = rs!.gapY; rs!.gapX = rs!.gapY; }
|
if (s.bind.checked) { s.mult = s.gapY; s.gapX = s.gapY; }
|
||||||
syncAdvFieldsToState();
|
syncAdvFieldsToState();
|
||||||
syncAdvancedMeta();
|
syncAdvancedMeta();
|
||||||
drawAdvancedPreview();
|
drawAdvancedPreview();
|
||||||
markCalcStale();
|
markCalcStale();
|
||||||
});
|
});
|
||||||
rs.offX.addEventListener('input', (e) => {
|
s.offX.addEventListener('input', (e) => {
|
||||||
const val = parseFloat((e.target as HTMLInputElement).value);
|
const val = parseFloat((e.target as HTMLInputElement).value);
|
||||||
if (!Number.isFinite(val)) return;
|
if (!Number.isFinite(val)) return;
|
||||||
rs!.offx = Math.min(Math.max(val, 0), Math.max(0, rs!.origH-0.0001));
|
s.offx = Math.min(Math.max(val, 0), Math.max(0, s.origH-0.0001));
|
||||||
rs!.viewX = Math.min(rs!.viewX, Math.max(0, rs!.origW - 1));
|
s.viewX = Math.min(s.viewX, Math.max(0, s.origW - 1));
|
||||||
syncAdvancedMeta();
|
syncAdvancedMeta();
|
||||||
drawAdvancedPreview();
|
drawAdvancedPreview();
|
||||||
markCalcStale();
|
markCalcStale();
|
||||||
});
|
});
|
||||||
rs.offY.addEventListener('input', (e) => {
|
s.offY.addEventListener('input', (e) => {
|
||||||
const val = parseFloat((e.target as HTMLInputElement).value);
|
const val = parseFloat((e.target as HTMLInputElement).value);
|
||||||
if (!Number.isFinite(val)) return;
|
if (!Number.isFinite(val)) return;
|
||||||
rs!.offy = Math.min(Math.max(val, 0), Math.max(0, rs!.origH-0.0001));
|
s.offy = Math.min(Math.max(val, 0), Math.max(0, s.origH-0.0001));
|
||||||
rs!.viewY = Math.min(rs!.viewY, Math.max(0, rs!.origH - 1));
|
s.viewY = Math.min(s.viewY, Math.max(0, s.origH - 1));
|
||||||
syncAdvancedMeta();
|
syncAdvancedMeta();
|
||||||
drawAdvancedPreview();
|
drawAdvancedPreview();
|
||||||
markCalcStale();
|
markCalcStale();
|
||||||
});
|
});
|
||||||
rs.dotR.addEventListener('input', (e) => {
|
s.dotR.addEventListener('input', (e) => {
|
||||||
rs!.dotr = Math.max(1, Math.round(Number((e.target as HTMLInputElement).value)||1));
|
s.dotr = Math.max(1, Math.round(Number((e.target as HTMLInputElement).value)||1));
|
||||||
rs!.dotRVal.textContent = String(rs!.dotr);
|
s.dotRVal.textContent = String(s.dotr);
|
||||||
drawAdvancedPreview();
|
drawAdvancedPreview();
|
||||||
});
|
});
|
||||||
rs.gridToggle.addEventListener('change', drawAdvancedPreview);
|
s.gridToggle.addEventListener('change', drawAdvancedPreview);
|
||||||
|
|
||||||
function applyZoom(factor: number) {
|
function applyZoom(factor: number) {
|
||||||
const destW = Math.max(50, Math.floor(rs!.advWrap.clientWidth));
|
const destW = Math.max(50, Math.floor(s.advWrap.clientWidth));
|
||||||
const destH = Math.max(50, Math.floor(rs!.advWrap.clientHeight));
|
const destH = Math.max(50, Math.floor(s.advWrap.clientHeight));
|
||||||
const sw = Math.max(1, Math.floor(destW / rs!.zoom));
|
const sw = Math.max(1, Math.floor(destW / s.zoom));
|
||||||
const sh = Math.max(1, Math.floor(destH / rs!.zoom));
|
const sh = Math.max(1, Math.floor(destH / s.zoom));
|
||||||
const cx = rs!.viewX + sw / 2;
|
const cx = s.viewX + sw / 2;
|
||||||
const cy = rs!.viewY + sh / 2;
|
const cy = s.viewY + sh / 2;
|
||||||
rs!.zoom = Math.min(32, Math.max(0.1, rs!.zoom * factor));
|
s.zoom = Math.min(32, Math.max(0.1, s.zoom * factor));
|
||||||
const sw2 = Math.max(1, Math.floor(destW / rs!.zoom));
|
const sw2 = Math.max(1, Math.floor(destW / s.zoom));
|
||||||
const sh2 = Math.max(1, Math.floor(destH / rs!.zoom));
|
const sh2 = Math.max(1, Math.floor(destH / s.zoom));
|
||||||
rs!.viewX = Math.min(Math.max(0, Math.round(cx - sw2 / 2)), Math.max(0, rs!.origW - sw2));
|
s.viewX = Math.min(Math.max(0, Math.round(cx - sw2 / 2)), Math.max(0, s.origW - sw2));
|
||||||
rs!.viewY = Math.min(Math.max(0, Math.round(cy - sh2 / 2)), Math.max(0, rs!.origH - sh2));
|
s.viewY = Math.min(Math.max(0, Math.round(cy - sh2 / 2)), Math.max(0, s.origH - sh2));
|
||||||
drawAdvancedPreview();
|
drawAdvancedPreview();
|
||||||
}
|
}
|
||||||
rs.zoomIn.addEventListener('click', () => applyZoom(1.25));
|
s.zoomIn.addEventListener('click', () => applyZoom(1.25));
|
||||||
rs.zoomOut.addEventListener('click', () => applyZoom(1/1.25));
|
s.zoomOut.addEventListener('click', () => applyZoom(1/1.25));
|
||||||
rs.advWrap.addEventListener('wheel', (e) => {
|
s.advWrap.addEventListener('wheel', (e) => {
|
||||||
if (!(e as WheelEvent).ctrlKey) return;
|
if (!(e as WheelEvent).ctrlKey) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const delta = (e as WheelEvent).deltaY || 0;
|
const delta = (e as WheelEvent).deltaY || 0;
|
||||||
|
|
@ -685,59 +705,59 @@ export function buildRSModal() {
|
||||||
|
|
||||||
const onPanDown = (e: PointerEvent) => {
|
const onPanDown = (e: PointerEvent) => {
|
||||||
if ((e.target as HTMLElement).closest('.op-rs-zoom')) return;
|
if ((e.target as HTMLElement).closest('.op-rs-zoom')) return;
|
||||||
rs!.panning = true;
|
s.panning = true;
|
||||||
rs!.panStart = { x: e.clientX, y: e.clientY, viewX: rs!.viewX, viewY: rs!.viewY };
|
s.panStart = { x: e.clientX, y: e.clientY, viewX: s.viewX, viewY: s.viewY };
|
||||||
rs!.advWrap.classList.remove('op-pan-grab');
|
s.advWrap.classList.remove('op-pan-grab');
|
||||||
rs!.advWrap.classList.add('op-pan-grabbing');
|
s.advWrap.classList.add('op-pan-grabbing');
|
||||||
(rs!.advWrap as any).setPointerCapture?.(e.pointerId);
|
(s.advWrap as any).setPointerCapture?.(e.pointerId);
|
||||||
};
|
};
|
||||||
const onPanMove = (e: PointerEvent) => {
|
const onPanMove = (e: PointerEvent) => {
|
||||||
if (!rs!.panning) return;
|
if (!s.panning || !s.panStart) return;
|
||||||
const dx = e.clientX - rs!.panStart!.x;
|
const dx = e.clientX - s.panStart.x;
|
||||||
const dy = e.clientY - rs!.panStart!.y;
|
const dy = e.clientY - s.panStart.y;
|
||||||
const wrapW = rs!.advWrap.clientWidth;
|
const wrapW = s.advWrap.clientWidth;
|
||||||
const wrapH = rs!.advWrap.clientHeight;
|
const wrapH = s.advWrap.clientHeight;
|
||||||
const sw = Math.max(1, Math.floor(wrapW / rs!.zoom));
|
const sw = Math.max(1, Math.floor(wrapW / s.zoom));
|
||||||
const sh = Math.max(1, Math.floor(wrapH / rs!.zoom));
|
const sh = Math.max(1, Math.floor(wrapH / s.zoom));
|
||||||
let nx = rs!.panStart!.viewX - Math.round(dx / rs!.zoom);
|
let nx = s.panStart.viewX - Math.round(dx / s.zoom);
|
||||||
let ny = rs!.panStart!.viewY - Math.round(dy / rs!.zoom);
|
let ny = s.panStart.viewY - Math.round(dy / s.zoom);
|
||||||
nx = Math.min(Math.max(0, nx), Math.max(0, rs!.origW - sw));
|
nx = Math.min(Math.max(0, nx), Math.max(0, s.origW - sw));
|
||||||
ny = Math.min(Math.max(0, ny), Math.max(0, rs!.origH - sh));
|
ny = Math.min(Math.max(0, ny), Math.max(0, s.origH - sh));
|
||||||
rs!.viewX = nx;
|
s.viewX = nx;
|
||||||
rs!.viewY = ny;
|
s.viewY = ny;
|
||||||
drawAdvancedPreview();
|
drawAdvancedPreview();
|
||||||
};
|
};
|
||||||
const onPanUp = (e: PointerEvent) => {
|
const onPanUp = (e: PointerEvent) => {
|
||||||
if (!rs!.panning) return;
|
if (!s.panning) return;
|
||||||
rs!.panning = false;
|
s.panning = false;
|
||||||
rs!.panStart = null;
|
s.panStart = null;
|
||||||
rs!.advWrap.classList.remove('op-pan-grabbing');
|
s.advWrap.classList.remove('op-pan-grabbing');
|
||||||
rs!.advWrap.classList.add('op-pan-grab');
|
s.advWrap.classList.add('op-pan-grab');
|
||||||
(rs!.advWrap as any).releasePointerCapture?.(e.pointerId);
|
(s.advWrap as any).releasePointerCapture?.(e.pointerId);
|
||||||
};
|
};
|
||||||
rs.advWrap.addEventListener('pointerdown', onPanDown);
|
s.advWrap.addEventListener('pointerdown', onPanDown);
|
||||||
rs.advWrap.addEventListener('pointermove', onPanMove);
|
s.advWrap.addEventListener('pointermove', onPanMove);
|
||||||
rs.advWrap.addEventListener('pointerup', onPanUp);
|
s.advWrap.addEventListener('pointerup', onPanUp);
|
||||||
rs.advWrap.addEventListener('pointercancel', onPanUp);
|
s.advWrap.addEventListener('pointercancel', onPanUp);
|
||||||
rs.advWrap.addEventListener('pointerleave', onPanUp);
|
s.advWrap.addEventListener('pointerleave', onPanUp);
|
||||||
|
|
||||||
const close = () => closeRSModal();
|
const close = () => closeRSModal();
|
||||||
rs.cancelBtn.addEventListener('click', close);
|
s.cancelBtn.addEventListener('click', close);
|
||||||
rs.closeBtn.addEventListener('click', close);
|
s.closeBtn.addEventListener('click', close);
|
||||||
backdrop.addEventListener('click', close);
|
backdrop.addEventListener('click', close);
|
||||||
|
|
||||||
rs.calcBtn.addEventListener('click', async () => {
|
s.calcBtn.addEventListener('click', async () => {
|
||||||
if (rs!.mode !== 'advanced' || !rs!.img) return;
|
if (s.mode !== 'advanced' || !s.img) return;
|
||||||
try {
|
try {
|
||||||
const { cols, rows } = sampleDims();
|
const { cols, rows } = sampleDims();
|
||||||
if (cols<=0 || rows<=0) { showToast('No samples. Adjust multiplier/offset.'); return; }
|
if (cols<=0 || rows<=0) { showToast('No samples. Adjust multiplier/offset.'); return; }
|
||||||
if (cols >= MAX_OVERLAY_DIM || rows >= MAX_OVERLAY_DIM) { showToast(`Output too large. Must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM}.`); return; }
|
if (cols >= MAX_OVERLAY_DIM || rows >= MAX_OVERLAY_DIM) { showToast(`Output too large. Must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM}.`); return; }
|
||||||
const canvas = await reconstructViaGrid(rs!.img, rs!.origW, rs!.origH, rs!.offx, rs!.offy, rs!.gapX, rs!.gapY);
|
const canvas = await reconstructViaGrid(s.img, s.origW, s.origH, s.offx, s.offy, s.gapX, s.gapY);
|
||||||
rs!.calcCanvas = canvas;
|
s.calcCanvas = canvas;
|
||||||
rs!.calcCols = cols;
|
s.calcCols = cols;
|
||||||
rs!.calcRows = rows;
|
s.calcRows = rows;
|
||||||
rs!.calcReady = true;
|
s.calcReady = true;
|
||||||
rs!.applyBtn.disabled = false;
|
s.applyBtn.disabled = false;
|
||||||
drawAdvancedResultPreview();
|
drawAdvancedResultPreview();
|
||||||
updateFooterMeta();
|
updateFooterMeta();
|
||||||
showToast(`Calculated ${cols}×${rows}. Review preview, then Apply.`);
|
showToast(`Calculated ${cols}×${rows}. Review preview, then Apply.`);
|
||||||
|
|
@ -747,29 +767,29 @@ export function buildRSModal() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rs.applyBtn.addEventListener('click', async () => {
|
s.applyBtn.addEventListener('click', async () => {
|
||||||
if (!rs!.ov) return;
|
if (!s.ov) return;
|
||||||
try {
|
try {
|
||||||
if (rs!.mode === 'simple') {
|
if (s.mode === 'simple') {
|
||||||
const W = parseInt(rs!.w.value||'0',10);
|
const W = parseInt(s.w.value||'0',10);
|
||||||
const H = parseInt(rs!.h.value||'0',10);
|
const H = parseInt(s.h.value||'0',10);
|
||||||
if (!Number.isFinite(W) || !Number.isFinite(H) || W<=0 || H<=0) { showToast('Invalid dimensions'); return; }
|
if (!Number.isFinite(W) || !Number.isFinite(H) || W<=0 || H<=0) { showToast('Invalid dimensions'); return; }
|
||||||
if (W >= MAX_OVERLAY_DIM || H >= MAX_OVERLAY_DIM) { showToast(`Too large. Must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM}.`); return; }
|
if (W >= MAX_OVERLAY_DIM || H >= MAX_OVERLAY_DIM) { showToast(`Too large. Must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM}.`); return; }
|
||||||
await resizeOverlayImage(rs!.ov, W, H);
|
await resizeOverlayImage(s.ov, W, H);
|
||||||
closeRSModal();
|
closeRSModal();
|
||||||
showToast(`Resized to ${W}×${H}.`);
|
showToast(`Resized to ${W}×${H}.`);
|
||||||
} else {
|
} else {
|
||||||
if (!rs!.calcReady || !rs!.calcCanvas) { showToast('Calculate first.'); return; }
|
if (!s.calcReady || !s.calcCanvas) { showToast('Calculate first.'); return; }
|
||||||
const dataUrl = await canvasToDataURLSafe(rs!.calcCanvas);
|
const dataUrl = await canvasToDataURLSafe(s.calcCanvas);
|
||||||
rs!.ov.imageBase64 = dataUrl;
|
s.ov.imageBase64 = dataUrl;
|
||||||
rs!.ov.imageUrl = null;
|
s.ov.imageUrl = null;
|
||||||
rs!.ov.isLocal = true;
|
s.ov.isLocal = true;
|
||||||
await saveConfig(['overlays']);
|
await saveConfig(['overlays']);
|
||||||
clearOverlayCache();
|
clearOverlayCache();
|
||||||
ensureHook();
|
ensureHook();
|
||||||
emitOverlayChanged();
|
emitOverlayChanged();
|
||||||
closeRSModal();
|
closeRSModal();
|
||||||
showToast(`Applied ${rs!.calcCols}×${rs!.calcRows}.`);
|
showToast(`Applied ${s.calcCols}×${s.calcRows}.`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
@ -778,51 +798,52 @@ export function buildRSModal() {
|
||||||
});
|
});
|
||||||
|
|
||||||
function syncSimpleNote() {
|
function syncSimpleNote() {
|
||||||
const W = parseInt(rs!.w.value||'0',10);
|
const W = parseInt(s.w.value||'0',10);
|
||||||
const H = parseInt(rs!.h.value||'0',10);
|
const H = parseInt(s.h.value||'0',10);
|
||||||
const ok = Number.isFinite(W) && Number.isFinite(H) && W>0 && H>0;
|
const ok = Number.isFinite(W) && Number.isFinite(H) && W>0 && H>0;
|
||||||
const limit = (W >= MAX_OVERLAY_DIM || H >= MAX_OVERLAY_DIM);
|
const limit = (W >= MAX_OVERLAY_DIM || H >= MAX_OVERLAY_DIM);
|
||||||
const simpleText = ok
|
const simpleText = ok
|
||||||
? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})`
|
? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})`
|
||||||
: `Target: ${W}×${H} (OK)`)
|
: `Target: ${W}×${H} (OK)`)
|
||||||
: 'Enter positive width and height.';
|
: 'Enter positive width and height.';
|
||||||
if (rs!.note) rs!.note.textContent = simpleText;
|
if (s.note) s.note.textContent = simpleText;
|
||||||
if (rs!.mode === 'simple') rs!.applyBtn.disabled = (!ok || limit);
|
if (s.mode === 'simple') s.applyBtn.disabled = (!ok || limit);
|
||||||
if (rs!.mode === 'simple') rs!.meta.textContent = simpleText;
|
if (s.mode === 'simple') s.meta.textContent = simpleText;
|
||||||
}
|
}
|
||||||
function applyScaleToFields(scale: number) {
|
function applyScaleToFields(scale: number) {
|
||||||
const W = Math.max(1, Math.round(rs!.origW * scale));
|
const W = Math.max(1, Math.round(s.origW * scale));
|
||||||
const H = Math.max(1, Math.round(rs!.origH * scale));
|
const H = Math.max(1, Math.round(s.origH * scale));
|
||||||
rs!.updating = true;
|
s.updating = true;
|
||||||
rs!.w.value = String(W);
|
s.w.value = String(W);
|
||||||
rs!.h.value = rs!.lock.checked ? String(Math.max(1, Math.round(W * rs!.origH / rs!.origW))) : String(H);
|
s.h.value = s.lock.checked ? String(Math.max(1, Math.round(W * s.origH / s.origW))) : String(H);
|
||||||
rs!.updating = false;
|
s.updating = false;
|
||||||
syncSimpleNote();
|
syncSimpleNote();
|
||||||
}
|
}
|
||||||
function syncAdvFieldsToState() {
|
function syncAdvFieldsToState() {
|
||||||
rs!.updating = true;
|
s.updating = true;
|
||||||
rs!.multRange.value = String(rs!.mult);
|
s.multRange.value = String(s.mult);
|
||||||
rs!.multInput.value = String(rs!.mult);
|
s.multInput.value = String(s.mult);
|
||||||
rs!.blockW.value = String(rs!.gapX);
|
s.blockW.value = String(s.gapX);
|
||||||
rs!.blockH.value = String(rs!.gapY);
|
s.blockH.value = String(s.gapY);
|
||||||
rs!.offX.value = String(rs!.offx);
|
s.offX.value = String(s.offx);
|
||||||
rs!.offY.value = String(rs!.offy);
|
s.offY.value = String(s.offy);
|
||||||
rs!.dotR.value = String(rs!.dotr);
|
s.dotR.value = String(s.dotr);
|
||||||
rs!.dotRVal.textContent = String(rs!.dotr);
|
s.dotRVal.textContent = String(s.dotr);
|
||||||
rs!.updating = false;
|
s.updating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rs._syncAdvancedMeta = syncAdvancedMeta;
|
s._syncAdvancedMeta = syncAdvancedMeta;
|
||||||
rs._syncSimpleNote = syncSimpleNote;
|
s._syncSimpleNote = syncSimpleNote;
|
||||||
|
|
||||||
rs._resizeHandler = () => {
|
s._resizeHandler = () => {
|
||||||
if (rs!.mode === 'simple') rs!._drawSimplePreview?.();
|
if (!rs) return;
|
||||||
|
if (rs.mode === 'simple') rs._drawSimplePreview?.();
|
||||||
else {
|
else {
|
||||||
rs!._drawAdvancedPreview?.();
|
rs._drawAdvancedPreview?.();
|
||||||
rs!._drawAdvancedResultPreview?.();
|
rs._drawAdvancedResultPreview?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('resize', rs._resizeHandler);
|
window.addEventListener('resize', s._resizeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openRSModal(overlay: any) {
|
export function openRSModal(overlay: any) {
|
||||||
|
|
@ -831,60 +852,64 @@ export function openRSModal(overlay: any) {
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
rs!.img = img;
|
const s = rs;
|
||||||
rs!.origW = img.width; rs!.origH = img.height;
|
if (!s) return;
|
||||||
|
|
||||||
rs!.orig.value = `${rs!.origW}×${rs!.origH}`;
|
s.img = img;
|
||||||
rs!.w.value = String(rs!.origW);
|
s.origW = img.width; s.origH = img.height;
|
||||||
rs!.h.value = String(rs!.origH);
|
|
||||||
rs!.lock.checked = true;
|
|
||||||
|
|
||||||
rs!.zoom = 1.0;
|
s.orig.value = `${s.origW}×${s.origH}`;
|
||||||
rs!.mult = 4;
|
s.w.value = String(s.origW);
|
||||||
rs!.gapX = 4; rs!.gapY = 4;
|
s.h.value = String(s.origH);
|
||||||
rs!.offx = 0; rs!.offy = 0;
|
s.lock.checked = true;
|
||||||
rs!.dotr = 1;
|
|
||||||
rs!.viewX = 0; rs!.viewY = 0;
|
|
||||||
|
|
||||||
rs!.bind.checked = true;
|
s.zoom = 1.0;
|
||||||
rs!.multRange.value = '4';
|
s.mult = 4;
|
||||||
rs!.multInput.value = '4';
|
s.gapX = 4; s.gapY = 4;
|
||||||
rs!.blockW.value = '4';
|
s.offx = 0; s.offy = 0;
|
||||||
rs!.blockH.value = '4';
|
s.dotr = 1;
|
||||||
rs!.offX.value = '0';
|
s.viewX = 0; s.viewY = 0;
|
||||||
rs!.offY.value = '0';
|
|
||||||
rs!.dotR.value = '1';
|
|
||||||
rs!.dotRVal.textContent = '1';
|
|
||||||
rs!.gridToggle.checked = true;
|
|
||||||
|
|
||||||
rs!.calcCanvas = null;
|
s.bind.checked = true;
|
||||||
rs!.calcCols = 0;
|
s.multRange.value = '4';
|
||||||
rs!.calcRows = 0;
|
s.multInput.value = '4';
|
||||||
rs!.calcReady = false;
|
s.blockW.value = '4';
|
||||||
rs!.applyBtn.disabled = (rs!.mode === 'advanced');
|
s.blockH.value = '4';
|
||||||
|
s.offX.value = '0';
|
||||||
|
s.offY.value = '0';
|
||||||
|
s.dotR.value = '1';
|
||||||
|
s.dotRVal.textContent = '1';
|
||||||
|
s.gridToggle.checked = true;
|
||||||
|
|
||||||
rs!._setMode!('simple');
|
s.calcCanvas = null;
|
||||||
|
s.calcCols = 0;
|
||||||
|
s.calcRows = 0;
|
||||||
|
s.calcReady = false;
|
||||||
|
s.applyBtn.disabled = (s.mode === 'advanced');
|
||||||
|
|
||||||
|
if (s._setMode) s._setMode('simple');
|
||||||
|
|
||||||
document.body.classList.add('op-scroll-lock');
|
document.body.classList.add('op-scroll-lock');
|
||||||
rs!.backdrop.classList.add('show');
|
s.backdrop.classList.add('show');
|
||||||
rs!.modal.style.display = 'flex';
|
s.modal.style.display = 'flex';
|
||||||
|
|
||||||
rs!._drawSimplePreview?.();
|
s._drawSimplePreview?.();
|
||||||
rs!._drawAdvancedPreview?.();
|
s._drawAdvancedPreview?.();
|
||||||
rs!._drawAdvancedResultPreview?.();
|
s._drawAdvancedResultPreview?.();
|
||||||
rs!._syncAdvancedMeta?.();
|
s._syncAdvancedMeta?.();
|
||||||
rs!._syncSimpleNote?.();
|
s._syncSimpleNote?.();
|
||||||
|
|
||||||
const setFooterNow = () => {
|
const setFooterNow = () => {
|
||||||
if (rs!.mode === 'advanced') {
|
if (!rs) return;
|
||||||
const cols = Math.floor((rs!.origW - rs!.offx) / rs!.gapX);
|
if (rs.mode === 'advanced') {
|
||||||
const rows = Math.floor((rs!.origH - rs!.offy) / rs!.gapY);
|
const cols = Math.floor((rs.origW - rs.offx) / rs.gapX);
|
||||||
rs!.meta.textContent = (cols>0&&rows>0) ? `Samples: ${cols} × ${rows} | Output: ${cols}×${rows}${(cols>=MAX_OVERLAY_DIM||rows>=MAX_OVERLAY_DIM)?` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})`:''}` : 'Adjust multiplier/offset until dots sit at centers.';
|
const rows = Math.floor((rs.origH - rs.offy) / rs.gapY);
|
||||||
|
rs.meta.textContent = (cols>0&&rows>0) ? `Samples: ${cols} × ${rows} | Output: ${cols}×${rows}${(cols>=MAX_OVERLAY_DIM||rows>=MAX_OVERLAY_DIM)?` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})`:''}` : 'Adjust multiplier/offset until dots sit at centers.';
|
||||||
} else {
|
} else {
|
||||||
const W = parseInt(rs!.w.value||'0',10); const H = parseInt(rs!.h.value||'0',10);
|
const W = parseInt(rs.w.value||'0',10); const H = parseInt(rs.h.value||'0',10);
|
||||||
const ok = Number.isFinite(W)&&Number.isFinite(H)&&W>0&&H>0;
|
const ok = Number.isFinite(W)&&Number.isFinite(H)&&W>0&&H>0;
|
||||||
const limit = (W>=MAX_OVERLAY_DIM||H>=MAX_OVERLAY_DIM);
|
const limit = (W>=MAX_OVERLAY_DIM||H>=MAX_OVERLAY_DIM);
|
||||||
rs!.meta.textContent = ok ? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : `Target: ${W}×${H} (OK)`) : 'Enter positive width and height.';
|
rs.meta.textContent = ok ? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : `Target: ${W}×${H} (OK)`) : 'Enter positive width and height.';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
setFooterNow();
|
setFooterNow();
|
||||||
|
|
@ -903,8 +928,9 @@ function closeRSModal() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reconstructViaGrid(img: HTMLImageElement, origW: number, origH: number, offx: number, offy: number, gapX: number, gapY: number) {
|
async function reconstructViaGrid(img: HTMLImageElement, origW: number, origH: number, offx: number, offy: number, gapX: number, gapY: number) {
|
||||||
const srcCanvas = createCanvas(origW, origH) as any;
|
const srcCanvas = createCanvas(origW, origH) as HTMLCanvasElement;
|
||||||
const sctx = srcCanvas.getContext('2d', { willReadFrequently: true })!;
|
const sctx = srcCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
if (!sctx) throw new Error('Failed to get 2d context for srcCanvas.');
|
||||||
sctx.imageSmoothingEnabled = false;
|
sctx.imageSmoothingEnabled = false;
|
||||||
sctx.drawImage(img, 0, 0);
|
sctx.drawImage(img, 0, 0);
|
||||||
const srcData = sctx.getImageData(0,0,origW,origH).data;
|
const srcData = sctx.getImageData(0,0,origW,origH).data;
|
||||||
|
|
@ -914,7 +940,8 @@ async function reconstructViaGrid(img: HTMLImageElement, origW: number, origH: n
|
||||||
if (cols <= 0 || rows <= 0) throw new Error('No samples available with current offset/gap');
|
if (cols <= 0 || rows <= 0) throw new Error('No samples available with current offset/gap');
|
||||||
|
|
||||||
const outCanvas = createHTMLCanvas(cols, rows);
|
const outCanvas = createHTMLCanvas(cols, rows);
|
||||||
const octx = outCanvas.getContext('2d')!;
|
const octx = outCanvas.getContext('2d');
|
||||||
|
if (!octx) throw new Error('Failed to get 2d context for outCanvas.');
|
||||||
const out = octx.createImageData(cols, rows);
|
const out = octx.createImageData(cols, rows);
|
||||||
const odata = out.data;
|
const odata = out.data;
|
||||||
|
|
||||||
|
|
@ -948,7 +975,8 @@ async function reconstructViaGrid(img: HTMLImageElement, origW: number, origH: n
|
||||||
async function resizeOverlayImage(ov: any, targetW: number, targetH: number) {
|
async function resizeOverlayImage(ov: any, targetW: number, targetH: number) {
|
||||||
const img = await loadImage(ov.imageBase64);
|
const img = await loadImage(ov.imageBase64);
|
||||||
const canvas = createHTMLCanvas(targetW, targetH);
|
const canvas = createHTMLCanvas(targetW, targetH);
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
|
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
if (!ctx) throw new Error('Failed to get 2d context for resize canvas.');
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
ctx.clearRect(0,0,targetW,targetH);
|
ctx.clearRect(0,0,targetW,targetH);
|
||||||
ctx.drawImage(img, 0,0, img.width,img.height, 0,0, targetW,targetH);
|
ctx.drawImage(img, 0,0, img.width,img.height, 0,0, targetW,targetH);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue