diff --git a/src/ui/panel.ts b/src/ui/panel.ts index 005566f..d155f2e 100644 --- a/src/ui/panel.ts +++ b/src/ui/panel.ts @@ -12,13 +12,7 @@ import { EV_ANCHOR_SET, EV_AUTOCAP_CHANGED } from '../core/events'; let panelEl: HTMLDivElement | null = null; -function $(id: string): HTMLElement { - const el = document.getElementById(id); - if (!el) { - throw new Error(`Element with id "${id}" not found.`); - } - return el; -} +function $(id: string): HTMLElement | null { return document.getElementById(id); } export function createUI() { if (document.getElementById('overlay-pro-panel')) return; @@ -29,8 +23,10 @@ export function createUI() { const panelW = 340; const defaultLeft = Math.max(12, window.innerWidth - panelW - 80); - panel.style.left = `${Number.isFinite(config.panelX as any) ? (config.panelX as any) : defaultLeft}px`; - panel.style.top = `${Number.isFinite(config.panelY as any) ? (config.panelY as any) : 120}px`; + const initLeft = (typeof config.panelX === 'number' && Number.isFinite(config.panelX)) ? config.panelX : defaultLeft; + 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 = `
@@ -179,7 +175,8 @@ export function createUI() { } function rebuildOverlayListUI() { - const list = $('op-overlay-list'); + const list = $('op-overlay-list') as HTMLDivElement | null; + if (!list) return; list.innerHTML = ''; for (const ov of config.overlays) { const item = document.createElement('div'); @@ -213,7 +210,7 @@ function rebuildOverlayListUI() { async function addBlankOverlay() { 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.activeOverlayId = ov.id; await saveConfig(['overlays', 'activeOverlayId']); @@ -240,7 +237,7 @@ async function setOverlayImageFromFile(ov: any, file: File) { showToast('Local image loaded. Placement mode ON -- click once to set anchor.'); } -type ImportedOverlay = { +type ImportOverlay = { name?: string; imageUrl?: string; pixelUrl?: string | null; @@ -249,18 +246,31 @@ type ImportedOverlay = { opacity?: number; }; +function isImportOverlay(val: unknown): val is ImportOverlay { + return typeof val === 'object' && val !== null; +} + async function importOverlayFromJSON(jsonText: string) { - let obj: unknown; - try { obj = JSON.parse(jsonText) as unknown; } catch { alert('Invalid JSON'); return; } - const arr: ImportedOverlay[] = Array.isArray(obj) ? (obj as ImportedOverlay[]) : [obj as ImportedOverlay]; + let parsed: unknown; + 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; for (const item of arr) { const name = uniqueName(item?.name || 'Imported Overlay', config.overlays.map(o => o.name || '')); const imageUrl = item?.imageUrl; - const pixelUrl = item?.pixelUrl ?? null; - const offsetX = Number.isFinite(item?.offsetX) ? (item?.offsetX as number) : 0; - const offsetY = Number.isFinite(item?.offsetY) ? (item?.offsetY as number) : 0; - const opacity = Number.isFinite(item?.opacity) ? (item?.opacity as number) : 0.7; + const pixelUrl = (item?.pixelUrl ?? null) as string | null; + const offsetX = (typeof item?.offsetX === 'number' && Number.isFinite(item.offsetX)) ? item.offsetX : 0; + const offsetY = (typeof item?.offsetY === 'number' && Number.isFinite(item.offsetY)) ? item.offsetY : 0; + const opacity = (typeof item?.opacity === 'number' && Number.isFinite(item.opacity)) ? item.opacity : 0.7; if (!imageUrl) { failed++; continue; } try { const base64 = await urlToDataURL(imageUrl); @@ -269,7 +279,7 @@ async function importOverlayFromJSON(jsonText: string) { } catch (e) { console.error('Import failed for', imageUrl, e); failed++; } } if (imported > 0) { - config.activeOverlayId = config.overlays[config.overlays.length - 1]?.id || null; + config.activeOverlayId = config.overlays[config.overlays.length - 1].id; await saveConfig(['overlays', 'activeOverlayId']); clearOverlayCache(); ensureHook(); updateUI(); } alert(`Import finished. Imported: ${imported}${failed ? `, Failed: ${failed}` : ''}`); @@ -289,9 +299,9 @@ function copyText(text: string) { } 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-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-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-panel-toggle')?.addEventListener('click', (e) => { e.stopPropagation(); config.isPanelCollapsed = !config.isPanelCollapsed; saveConfig(['isPanelCollapsed']); updateUI(); }); panel.querySelectorAll('.op-tab-btn').forEach(btn => { btn.addEventListener('click', () => { @@ -306,21 +316,27 @@ function addEventListeners(panel: HTMLDivElement) { updateUI(); }); }); - $('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(); } }); - $('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 styleSymbolsEl = $('op-style-symbols') as HTMLInputElement | null; + styleSymbolsEl?.addEventListener('change', () => { if (styleSymbolsEl.checked) { config.minifyStyle = 'symbols'; saveConfig(['minifyStyle']); clearOverlayCache(); ensureHook(); } }); - $('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 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())) { ov.name = uniqueName(desired, config.overlays.map(o => o.name || '')); showToast(`Name in use. Renamed to "${ov.name}".`); @@ -328,48 +344,56 @@ function addEventListeners(panel: HTMLDivElement) { 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; } 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.'); } }); - const dropzone = $('op-dropzone'); - dropzone.addEventListener('click', () => $('op-file-input').click()); - $('op-file-input').addEventListener('change', async (e: any) => { - const file = e.target.files?.[0]; e.target.value=''; if (!file) return; + const dropzone = $('op-dropzone') as HTMLDivElement | null; + dropzone?.addEventListener('click', () => ( $('op-file-input') as HTMLInputElement | null )?.click()); + const fileInput = $('op-file-input') as HTMLInputElement | null; + 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; 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.'); } }); - ['dragenter', 'dragover'].forEach(evt => dropzone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drop-highlight'); })); - ['dragleave', 'drop'].forEach(evt => dropzone.addEventListener(evt, (e: any) => { e.preventDefault(); e.stopPropagation(); if (evt === 'dragleave' && e.target !== dropzone) return; dropzone.classList.remove('drop-highlight'); })); - dropzone.addEventListener('drop', async (e: any) => { - const dt = e.dataTransfer; if (!dt) return; const file = dt.files?.[0]; if (!file) return; - const ov = getActiveOverlay(); if (!ov) return; - if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; } - try { await setOverlayImageFromFile(ov, file); } catch (err) { console.error(err); alert('Failed to load dropped image.'); } - }); + if (dropzone) { + ['dragenter', 'dragover'].forEach(evt => dropzone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drop-highlight'); })); + ['dragleave', 'drop'].forEach(evt => dropzone.addEventListener(evt, (e: any) => { e.preventDefault(); e.stopPropagation(); if (evt === 'dragleave' && e.target !== dropzone) return; dropzone.classList.remove('drop-highlight'); })); + dropzone.addEventListener('drop', async (e: DragEvent) => { + const dt = e.dataTransfer; if (!dt) return; const file = dt?.files?.[0]; if (!file) return; + const ov = getActiveOverlay(); if (!ov) return; + if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; } + try { await setOverlayImageFromFile(ov, file); } catch (err) { console.error(err); alert('Failed to load dropped image.'); } + }); + } const nudge = async (dx: number, dy: number) => { const ov = getActiveOverlay(); if (!ov) return; ov.offsetX += dx; ov.offsetY += dy; await saveConfig(['overlays']); clearOverlayCache(); updateUI(); }; - $('op-nudge-up').addEventListener('click', () => nudge(0, -1)); - $('op-nudge-down').addEventListener('click', () => nudge(0, 1)); - $('op-nudge-left').addEventListener('click', () => nudge(-1, 0)); - $('op-nudge-right').addEventListener('click', () => nudge(1, 0)); + $('op-nudge-up')?.addEventListener('click', () => nudge(0, -1)); + $('op-nudge-down')?.addEventListener('click', () => nudge(0, 1)); + $('op-nudge-left')?.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; - ov.opacity = parseFloat(e.target.value); - $('op-opacity-value').textContent = `${Math.round(ov.opacity * 100)}%`; + const target = e.target as HTMLInputElement; + 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(); if (!ov || !ov.imageBase64) { showToast('No overlay image to download.'); return; } const a = document.createElement('a'); @@ -380,7 +404,7 @@ function addEventListeners(panel: HTMLDivElement) { 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; } openCCModal(ov); }); @@ -395,7 +419,7 @@ function addEventListeners(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; let isDragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0, moved = false; @@ -413,7 +437,7 @@ function enableDrag(panel: HTMLDivElement) { const maxLeft = Math.max(8, window.innerWidth - panel.offsetWidth - 8); const maxTop = Math.max(8, window.innerHeight - panel.offsetHeight - 8); panel.style.left = `${clamp(startLeft + dx, 8, maxLeft)}px`; - panel.style.top = `${clamp(startTop + dy, 8, maxTop)}px`; + panel.style.top = `${clamp(startTop + dy, 8, maxTop)}px`; moved = true; }; const onPointerUp = (e: PointerEvent) => { @@ -441,31 +465,36 @@ function enableDrag(panel: HTMLDivElement) { }); } -export function updateEditorUI(): void { - const editorSect = $('op-editor-section'); - const editorBody = $('op-editor-body'); +function updateEditorUI() { + const editorSect = $('op-editor-section') as HTMLDivElement | null; + const editorBody = $('op-editor-body') as HTMLDivElement | null; const ov = getActiveOverlay(); + if (!editorSect) return; 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 previewWrap = $('op-preview-wrap'); - const previewImg = $('op-image-preview') as HTMLImageElement; - const ccRow = $('op-cc-btn-row'); + const srcWrap = $('op-image-source') as HTMLDivElement | null; + const previewWrap = $('op-preview-wrap') as HTMLDivElement | null; + const previewImg = $('op-image-preview') as HTMLImageElement | null; + const ccRow = $('op-cc-btn-row') as HTMLDivElement | null; if (ov.imageBase64) { - srcWrap.style.display = 'none'; - previewWrap.style.display = 'flex'; - previewImg.src = ov.imageBase64; - ccRow.style.display = 'flex'; + if (srcWrap) srcWrap.style.display = 'none'; + if (previewWrap) previewWrap.style.display = 'flex'; + if (previewImg) previewImg.src = ov.imageBase64; + if (ccRow) ccRow.style.display = 'flex'; } else { - srcWrap.style.display = 'block'; - previewWrap.style.display = 'none'; - ccRow.style.display = 'none'; - ( $('op-image-url') as HTMLInputElement ).value = ov.imageUrl || ''; + if (srcWrap) srcWrap.style.display = 'block'; + if (previewWrap) previewWrap.style.display = 'none'; + if (ccRow) ccRow.style.display = 'none'; + 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; @@ -489,86 +518,97 @@ export function updateUI() { applyTheme(); - const content = $('op-content'); - const toggle = $('op-panel-toggle'); + const content = $('op-content') as HTMLDivElement | null; + const toggle = $('op-panel-toggle') as HTMLButtonElement | null; const collapsed = !!config.isPanelCollapsed; - content.style.display = collapsed ? 'none' : 'flex'; - toggle.textContent = collapsed ? '▸' : '▾'; - toggle.title = collapsed ? 'Expand' : 'Collapse'; + if (content) content.style.display = collapsed ? 'none' : 'flex'; + if (toggle) { + toggle.textContent = collapsed ? '▸' : '▾'; + toggle.title = collapsed ? 'Expand' : 'Collapse'; + } // --- Mode Tabs --- - if (panelEl) { - panelEl.querySelectorAll('.op-tab-btn').forEach(btn => { - const mode = btn.getAttribute('data-mode'); - let isActive = false; - if (mode === 'above' && (config.overlayMode === 'above' || config.overlayMode === 'behind')) { + panelEl.querySelectorAll('.op-tab-btn').forEach(btn => { + const mode = btn.getAttribute('data-mode'); + let isActive = false; + if (mode === 'above' && (config.overlayMode === 'above' || config.overlayMode === 'behind')) { isActive = true; - } else { + } else { isActive = mode === config.overlayMode; - } - btn.classList.toggle('active', isActive); - }); - } + } + (btn as HTMLButtonElement).classList.toggle('active', isActive); + }); // --- Mode Settings --- - const fullOverlaySettings = $('op-mode-settings').querySelector('[data-setting="above"]') as HTMLDivElement; - const minifySettings = $('op-mode-settings').querySelector('[data-setting="minify"]') as HTMLDivElement; + const settingsRoot = $('op-mode-settings') as HTMLDivElement | null; + 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') { - fullOverlaySettings.classList.add('active'); - minifySettings.classList.remove('active'); + fullOverlaySettings?.classList.add('active'); + minifySettings?.classList.remove('active'); const ov = getActiveOverlay(); if (ov) { - ( $('op-opacity-slider') as HTMLInputElement ).value = String(ov.opacity); - $('op-opacity-value').textContent = `${Math.round(ov.opacity * 100)}%`; + const opacitySlider = $('op-opacity-slider') as HTMLInputElement | null; + 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') { - fullOverlaySettings.classList.remove('active'); - minifySettings.classList.add('active'); + fullOverlaySettings?.classList.remove('active'); + minifySettings?.classList.add('active'); } else { - fullOverlaySettings.classList.remove('active'); - minifySettings.classList.remove('active'); + fullOverlaySettings?.classList.remove('active'); + minifySettings?.classList.remove('active'); } - ($('op-style-dots') as HTMLInputElement).checked = config.minifyStyle === 'dots'; - ($('op-style-symbols') as HTMLInputElement).checked = config.minifyStyle === 'symbols'; - - const layeringBtns = $('op-layering-btns'); - layeringBtns.innerHTML = ''; - const behindBtn = document.createElement('button'); - behindBtn.textContent = 'Behind'; - behindBtn.className = `op-button${config.overlayMode === 'behind' ? ' active' : ''}`; - behindBtn.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 dotsEl = $('op-style-dots') as HTMLInputElement | null; + if (dotsEl) dotsEl.checked = config.minifyStyle === 'dots'; + const symbolsEl = $('op-style-symbols') as HTMLInputElement | null; + if (symbolsEl) symbolsEl.checked = config.minifyStyle === 'symbols'; + + 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 --- const autoBtn = $('op-autocap-toggle'); const placeLabel = $('op-place-label'); - autoBtn.textContent = config.autoCapturePixelUrl ? 'Enabled' : 'Disabled'; - autoBtn.classList.toggle('op-danger', !!config.autoCapturePixelUrl); - placeLabel?.classList.toggle('op-danger-text', !!config.autoCapturePixelUrl); + if (autoBtn) { + autoBtn.textContent = config.autoCapturePixelUrl ? 'Enabled' : 'Disabled'; + autoBtn.classList.toggle('op-danger', !!config.autoCapturePixelUrl); + } + if (placeLabel) placeLabel.classList.toggle('op-danger-text', !!config.autoCapturePixelUrl); - const positioningBody: HTMLElement = $('op-positioning-body'); + const positioningBody = $('op-positioning-body'); const positioningCz = $('op-collapse-positioning'); - positioningBody.style.display = config.collapsePositioning ? 'none' : 'block'; - if (positioningCz) positioningCz.textContent = config.collapsePositioning ? '▸' : '▾'; + if (positioningBody) positioningBody.style.display = config.collapsePositioning ? 'none' : 'block'; + 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'); - listWrap.style.display = config.collapseList ? 'none' : 'block'; - if (listCz) listCz.textContent = config.collapseList ? '▸' : '▾'; + if (listWrap) listWrap.style.display = config.collapseList ? 'none' : 'block'; + if (listCz) (listCz as HTMLButtonElement).textContent = config.collapseList ? '▸' : '▾'; rebuildOverlayListUI(); - updateEditorUI(); // <- now exported, so noUnusedLocals won't flag it + updateEditorUI(); - const exportBtn = $('op-export-overlay') as HTMLButtonElement; + const exportBtn = $('op-export-overlay') as HTMLButtonElement | null; const ov = getActiveOverlay(); const canExport = !!(ov?.imageUrl && !ov?.isLocal); - exportBtn.disabled = !canExport; - exportBtn.title = canExport ? 'Export active overlay JSON' : 'Export disabled for local images'; + if (exportBtn) { + exportBtn.disabled = !canExport; + exportBtn.title = canExport ? 'Export active overlay JSON' : 'Export disabled for local images'; + } } \ No newline at end of file diff --git a/src/ui/rsModal.ts b/src/ui/rsModal.ts index 3a9549b..8434438 100644 --- a/src/ui/rsModal.ts +++ b/src/ui/rsModal.ts @@ -291,14 +291,26 @@ export function buildRSModal() { closeBtn: modal.querySelector('#op-rs-close') as HTMLButtonElement, }; - const ctxPrev = refs.preview.getContext('2d', { willReadFrequently: true }); - if (!ctxPrev) throw new Error('Failed to get 2d context for preview canvas.'); - const ctxSimOrig = refs.simOrig.getContext('2d', { willReadFrequently: true }); - if (!ctxSimOrig) throw new Error('Failed to get 2d context for simOrig canvas.'); - const ctxSimNew = refs.simNew.getContext('2d', { willReadFrequently: true }); - if (!ctxSimNew) throw new Error('Failed to get 2d context for simNew canvas.'); - const ctxRes = refs.resCanvas.getContext('2d', { willReadFrequently: true }); - if (!ctxRes) throw new Error('Failed to get 2d context for resCanvas.'); + const ctxPrev: CanvasRenderingContext2D = (() => { + const ctx = refs.preview.getContext('2d', { willReadFrequently: true }); + if (!ctx) throw new Error('Failed to get 2d context for preview canvas.'); + 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 = { ...refs, @@ -325,65 +337,70 @@ export function buildRSModal() { calcReady: false, }; + const s = rs; // local non-null alias within this scope + function computeSimpleFooterText() { - 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 W = parseInt(s.w.value || '0', 10); + const H = parseInt(s.h.value || '0', 10); + const ok = Number.isFinite(W) && Number.isFinite(H) && W > 0 && H > 0; 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})` : `Target: ${W}×${H} (OK)`) : 'Enter positive width and height.'; } function sampleDims() { - const cols = Math.floor((rs!.origW - rs!.offx) / rs!.gapX); - const rows = Math.floor((rs!.origH - rs!.offy) / rs!.gapY); + const cols = Math.floor((s.origW - s.offx) / s.gapX); + const rows = Math.floor((s.origH - s.offy) / s.gapY); return { cols: Math.max(0, cols), rows: Math.max(0, rows) }; } function computeAdvancedFooterText() { const { cols, rows } = sampleDims(); 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})` : ''}` : 'Adjust multiplier/offset until dots sit at centers.'; } const updateFooterMeta = () => { - rs!.meta.textContent = (rs!.mode === 'advanced') ? computeAdvancedFooterText() : computeSimpleFooterText(); + s.meta.textContent = (s.mode === 'advanced') ? computeAdvancedFooterText() : computeSimpleFooterText(); }; function drawSimplePreview() { - if (!rs!.img) return; - const leftLabelH = rs!.colLeft.querySelector('.pad-top')!.clientHeight; - const rightLabelH = rs!.colRight.querySelector('.pad-top')!.clientHeight; - const leftW = rs!.colLeft.clientWidth; - const rightW = rs!.colRight.clientWidth; - const leftH = rs!.colLeft.clientHeight - leftLabelH; - const rightH = rs!.colRight.clientHeight - rightLabelH; + if (!s.img) return; + const padTopL = s.colLeft.querySelector('.pad-top') as HTMLElement | null; + const padTopR = s.colRight.querySelector('.pad-top') as HTMLElement | null; + const leftLabelH = padTopL ? padTopL.clientHeight : 0; + const rightLabelH = padTopR ? padTopR.clientHeight : 0; + const leftW = s.colLeft.clientWidth; + 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; - rs!.simNew.width = rightW; rs!.simNew.height = rightH; + s.simOrig.width = leftW; s.simOrig.height = leftH; + s.simNew.width = rightW; s.simNew.height = rightH; ctxSimOrig.save(); ctxSimOrig.imageSmoothingEnabled = false; ctxSimOrig.clearRect(0,0,leftW,leftH); - const sFit = Math.min(leftW / rs!.origW, leftH / rs!.origH); - const dW = Math.max(1, Math.floor(rs!.origW * sFit)); - const dH = Math.max(1, Math.floor(rs!.origH * sFit)); + const sFit = Math.min(leftW / s.origW, leftH / s.origH); + const dW = Math.max(1, Math.floor(s.origW * sFit)); + const dH = Math.max(1, Math.floor(s.origH * sFit)); const dx0 = Math.floor((leftW - dW) / 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(); - const W = parseInt(rs!.w.value||'0',10); - const H = parseInt(rs!.h.value||'0',10); + const W = parseInt(s.w.value || '0', 10); + const H = parseInt(s.h.value || '0', 10); ctxSimNew.save(); ctxSimNew.imageSmoothingEnabled = false; ctxSimNew.clearRect(0,0,rightW,rightH); if (Number.isFinite(W) && Number.isFinite(H) && W>0 && H>0) { - const tiny = createCanvas(W, H) as any; - const tctx = tiny.getContext('2d', { willReadFrequently: true })!; + const tiny = createCanvas(W, H) as HTMLCanvasElement; + const tctx = tiny.getContext('2d', { willReadFrequently: true }); + if (!tctx) throw new Error('Failed to get 2d context for tiny canvas.'); tctx.imageSmoothingEnabled = false; 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 data = id.data; for (let i=0;i0&&H>0&&W= 1 && rs!.gapY >= 1) { + if (s.gridToggle.checked && s.gapX >= 1 && s.gapY >= 1) { ctxPrev.strokeStyle = 'rgba(255,59,48,0.45)'; ctxPrev.lineWidth = 1; - const startGX = Math.ceil((rs!.viewX - rs!.offx) / rs!.gapX); - const endGX = Math.floor((rs!.viewX + sw - rs!.offx) / rs!.gapX); - const startGY = Math.ceil((rs!.viewY - rs!.offy) / rs!.gapY); - const endGY = Math.floor((rs!.viewY + sh - rs!.offy) / rs!.gapY); + const startGX = Math.ceil((s.viewX - s.offx) / s.gapX); + const endGX = Math.floor((s.viewX + sw - s.offx) / s.gapX); + const startGY = Math.ceil((s.viewY - s.offy) / s.gapY); + const endGY = Math.floor((s.viewY + sh - s.offy) / s.gapY); const linesX = Math.max(0, endGX - startGX + 1); const linesY = Math.max(0, endGY - startGY + 1); if (linesX <= 4000 && linesY <= 4000) { ctxPrev.beginPath(); for (let gx = startGX; gx <= endGX; gx++) { - const x = rs!.offx + gx * rs!.gapX; - const px = Math.round((x - rs!.viewX) * rs!.zoom); + const x = s.offx + gx * s.gapX; + const px = Math.round((x - s.viewX) * s.zoom); ctxPrev.moveTo(px + 0.5, 0); ctxPrev.lineTo(px + 0.5, destH); } for (let gy = startGY; gy <= endGY; gy++) { - const y = rs!.offy + gy * rs!.gapY; - const py = Math.round((y - rs!.viewY) * rs!.zoom); + const y = s.offy + gy * s.gapY; + const py = Math.round((y - s.viewY) * s.zoom); ctxPrev.moveTo(0, py + 0.5); ctxPrev.lineTo(destW, py + 0.5); } @@ -460,26 +477,26 @@ export function buildRSModal() { } } - if (rs!.gapX >= 1 && rs!.gapY >= 1) { + if (s.gapX >= 1 && s.gapY >= 1) { ctxPrev.fillStyle = '#ff3b30'; - const cx0 = rs!.offx + Math.floor(rs!.gapX/2); - const cy0 = rs!.offy + Math.floor(rs!.gapY/2); + const cx0 = s.offx + Math.floor(s.gapX/2); + const cy0 = s.offy + Math.floor(s.gapY/2); if (cx0 >= 0 && cy0 >= 0) { - const startX = Math.ceil((rs!.viewX - cx0) / rs!.gapX); - const startY = Math.ceil((rs!.viewY - cy0) / rs!.gapY); - const endY = Math.floor((rs!.viewY + sh - 1 - cy0) / rs!.gapY); - const endX2 = Math.floor((rs!.viewX + sw - 1 - cx0) / rs!.gapX); - const r = rs!.dotr; + const startX = Math.ceil((s.viewX - cx0) / s.gapX); + const startY = Math.ceil((s.viewY - cy0) / s.gapY); + const endY = Math.floor((s.viewY + sh - 1 - cy0) / s.gapY); + const endX2 = Math.floor((s.viewX + sw - 1 - cx0) / s.gapX); + const r = s.dotr; const dotsX = Math.max(0, endX2 - startX + 1); const dotsY = Math.max(0, endY - startY + 1); const maxDots = 300000; if (dotsX * dotsY <= maxDots) { 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++) { - const x = cx0 + gx * rs!.gapX; - const px = Math.round((x - rs!.viewX) * rs!.zoom); - const py = Math.round((y - rs!.viewY) * rs!.zoom); + const x = cx0 + gx * s.gapX; + const px = Math.round((x - s.viewX) * s.zoom); + const py = Math.round((y - s.viewY) * s.zoom); ctxPrev.beginPath(); ctxPrev.arc(px, py, r, 0, Math.PI*2); ctxPrev.fill(); @@ -492,44 +509,44 @@ export function buildRSModal() { } function drawAdvancedResultPreview() { - const canvas = rs!.calcCanvas; - const wrap = rs!.resWrap; + const canvas = s.calcCanvas; + const wrap = s.resWrap; if (!wrap || !canvas) { - ctxRes.clearRect(0,0, rs!.resCanvas.width, rs!.resCanvas.height); - rs!.resMeta.textContent = 'No result. Click Calculate.'; + ctxRes.clearRect(0,0, s.resCanvas.width, s.resCanvas.height); + s.resMeta.textContent = 'No result. Click Calculate.'; return; } const W = canvas.width, H = canvas.height; const availW = Math.max(50, Math.floor(wrap.clientWidth - 16)); const availH = Math.max(50, Math.floor(wrap.clientHeight - 16)); - const s = Math.min(availW / W, availH / H); - const dW = Math.max(1, Math.floor(W * s)); - const dH = Math.max(1, Math.floor(H * s)); - rs!.resCanvas.width = dW; - rs!.resCanvas.height = dH; + const scale = Math.min(availW / W, availH / H); + const dW = Math.max(1, Math.floor(W * scale)); + const dH = Math.max(1, Math.floor(H * scale)); + s.resCanvas.width = dW; + s.resCanvas.height = dH; ctxRes.save(); ctxRes.imageSmoothingEnabled = false; ctxRes.clearRect(0,0,dW,dH); ctxRes.drawImage(canvas, 0,0, W,H, 0,0, dW,dH); 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; - rs._drawAdvancedPreview = drawAdvancedPreview; - rs._drawAdvancedResultPreview = drawAdvancedResultPreview; + s._drawSimplePreview = drawSimplePreview; + s._drawAdvancedPreview = drawAdvancedPreview; + s._drawAdvancedResultPreview = drawAdvancedResultPreview; const setMode = (m: 'simple'|'advanced') => { - rs!.mode = m; - rs!.tabSimple.classList.toggle('active', m === 'simple'); - rs!.tabAdvanced.classList.toggle('active', m === 'advanced'); - rs!.paneSimple.classList.toggle('show', m === 'simple'); - rs!.paneAdvanced.classList.toggle('show', m === 'advanced'); + s.mode = m; + s.tabSimple.classList.toggle('active', m === 'simple'); + s.tabAdvanced.classList.toggle('active', m === 'advanced'); + s.paneSimple.classList.toggle('show', m === 'simple'); + s.paneAdvanced.classList.toggle('show', m === 'advanced'); updateFooterMeta(); - rs!.calcBtn.style.display = (m === 'advanced') ? 'inline-block' : 'none'; + s.calcBtn.style.display = (m === 'advanced') ? 'inline-block' : 'none'; if (m === 'advanced') { - rs!.applyBtn.disabled = !rs!.calcReady; + s.applyBtn.disabled = !s.calcReady; } else { syncSimpleNote(); } @@ -542,144 +559,144 @@ export function buildRSModal() { drawSimplePreview(); } }; - rs._setMode = (m) => { + s._setMode = (m) => { 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')); - rs.tabAdvanced.addEventListener('click', () => setMode('advanced')); + s.tabSimple.addEventListener('click', () => setMode('simple')); + s.tabAdvanced.addEventListener('click', () => setMode('advanced')); function onWidthInput() { - if (rs!.updating) return; - rs!.updating = true; - const W = parseInt(rs!.w.value||'0',10); - if (rs!.lock.checked && rs!.origW>0 && rs!.origH>0 && W>0) { - rs!.h.value = String(Math.max(1, Math.round(W * rs!.origH / rs!.origW))); + if (s.updating) return; + s.updating = true; + const W = parseInt(s.w.value||'0',10); + if (s.lock.checked && s.origW>0 && s.origH>0 && W>0) { + s.h.value = String(Math.max(1, Math.round(W * s.origH / s.origW))); } - rs!.updating = false; + s.updating = false; syncSimpleNote(); - if (rs!.mode === 'simple') drawSimplePreview(); + if (s.mode === 'simple') drawSimplePreview(); } function onHeightInput() { - if (rs!.updating) return; - rs!.updating = true; - const H = parseInt(rs!.h.value||'0',10); - if (rs!.lock.checked && rs!.origW>0 && rs!.origH>0 && H>0) { - rs!.w.value = String(Math.max(1, Math.round(H * rs!.origW / rs!.origH))); + if (s.updating) return; + s.updating = true; + const H = parseInt(s.h.value||'0',10); + if (s.lock.checked && s.origW>0 && s.origH>0 && H>0) { + s.w.value = String(Math.max(1, Math.round(H * s.origW / s.origH))); } - rs!.updating = false; + s.updating = false; syncSimpleNote(); - if (rs!.mode === 'simple') drawSimplePreview(); + if (s.mode === 'simple') drawSimplePreview(); } - rs.w.addEventListener('input', onWidthInput); - rs.h.addEventListener('input', onHeightInput); - rs.onex.addEventListener('click', () => { applyScaleToFields(1); drawSimplePreview(); }); - rs.half.addEventListener('click', () => { applyScaleToFields(0.5); drawSimplePreview(); }); - rs.third.addEventListener('click', () => { applyScaleToFields(1/3); drawSimplePreview(); }); - rs.quarter.addEventListener('click', () => { applyScaleToFields(1/4); drawSimplePreview(); }); - rs.double.addEventListener('click', () => { applyScaleToFields(2); drawSimplePreview(); }); - rs.applyScale.addEventListener('click', () => { - const s = parseFloat(rs!.scale.value||''); - if (!Number.isFinite(s) || s<=0) { showToast('Enter a valid scale factor > 0'); return; } - applyScaleToFields(s); + s.w.addEventListener('input', onWidthInput); + s.h.addEventListener('input', onHeightInput); + s.onex.addEventListener('click', () => { applyScaleToFields(1); drawSimplePreview(); }); + s.half.addEventListener('click', () => { applyScaleToFields(0.5); drawSimplePreview(); }); + s.third.addEventListener('click', () => { applyScaleToFields(1/3); drawSimplePreview(); }); + s.quarter.addEventListener('click', () => { applyScaleToFields(1/4); drawSimplePreview(); }); + s.double.addEventListener('click', () => { applyScaleToFields(2); drawSimplePreview(); }); + s.applyScale.addEventListener('click', () => { + const scaleVal = parseFloat(s.scale.value||''); + if (!Number.isFinite(scaleVal) || scaleVal<=0) { showToast('Enter a valid scale factor > 0'); return; } + applyScaleToFields(scaleVal); drawSimplePreview(); }); const markCalcStale = () => { - if (rs!.mode === 'advanced') { - rs!.calcReady = false; - rs!.applyBtn.disabled = true; + if (s.mode === 'advanced') { + s.calcReady = false; + s.applyBtn.disabled = true; drawAdvancedResultPreview(); updateFooterMeta(); } }; const onMultChange = (v: string) => { - if (rs!.updating) return; + if (s.updating) return; const parsed = parseFloat(v); if (!Number.isFinite(parsed)) return; const clamped = Math.min(Math.max(parsed, 1), 128); - rs!.mult = clamped; - if (rs!.bind.checked) { rs!.gapX = clamped; rs!.gapY = clamped; } + s.mult = clamped; + if (s.bind.checked) { s.gapX = clamped; s.gapY = clamped; } syncAdvFieldsToState(); syncAdvancedMeta(); drawAdvancedPreview(); markCalcStale(); }; - rs.multRange.addEventListener('input', (e) => onMultChange((e.target as HTMLInputElement).value)); - rs.multInput.addEventListener('input', (e) => { + s.multRange.addEventListener('input', (e) => onMultChange((e.target as HTMLInputElement).value)); + s.multInput.addEventListener('input', (e) => { const v = (e.target as HTMLInputElement).value; if (!Number.isFinite(parseFloat(v))) return; onMultChange(v); }); - rs.bind.addEventListener('change', () => { - if (rs!.bind.checked) { rs!.gapX = rs!.mult; rs!.gapY = rs!.mult; syncAdvFieldsToState(); } + s.bind.addEventListener('change', () => { + if (s.bind.checked) { s.gapX = s.mult; s.gapY = s.mult; syncAdvFieldsToState(); } syncAdvancedMeta(); drawAdvancedPreview(); markCalcStale(); }); - rs.blockW.addEventListener('input', (e) => { + s.blockW.addEventListener('input', (e) => { const val = parseFloat((e.target as HTMLInputElement).value); if (!Number.isFinite(val)) return; - rs!.gapX = Math.min(Math.max(val, 1), 4096); - if (rs!.bind.checked) { rs!.mult = rs!.gapX; rs!.gapY = rs!.gapX; } + s.gapX = Math.min(Math.max(val, 1), 4096); + if (s.bind.checked) { s.mult = s.gapX; s.gapY = s.gapX; } syncAdvFieldsToState(); syncAdvancedMeta(); drawAdvancedPreview(); markCalcStale(); }); - rs.blockH.addEventListener('input', (e) => { + s.blockH.addEventListener('input', (e) => { const val = parseFloat((e.target as HTMLInputElement).value); if (!Number.isFinite(val)) return; - rs!.gapY = Math.min(Math.max(val, 1), 4096); - if (rs!.bind.checked) { rs!.mult = rs!.gapY; rs!.gapX = rs!.gapY; } + s.gapY = Math.min(Math.max(val, 1), 4096); + if (s.bind.checked) { s.mult = s.gapY; s.gapX = s.gapY; } syncAdvFieldsToState(); syncAdvancedMeta(); drawAdvancedPreview(); markCalcStale(); }); - rs.offX.addEventListener('input', (e) => { + s.offX.addEventListener('input', (e) => { const val = parseFloat((e.target as HTMLInputElement).value); if (!Number.isFinite(val)) return; - rs!.offx = Math.min(Math.max(val, 0), Math.max(0, rs!.origH-0.0001)); - rs!.viewX = Math.min(rs!.viewX, Math.max(0, rs!.origW - 1)); + s.offx = Math.min(Math.max(val, 0), Math.max(0, s.origH-0.0001)); + s.viewX = Math.min(s.viewX, Math.max(0, s.origW - 1)); syncAdvancedMeta(); drawAdvancedPreview(); markCalcStale(); }); - rs.offY.addEventListener('input', (e) => { + s.offY.addEventListener('input', (e) => { const val = parseFloat((e.target as HTMLInputElement).value); if (!Number.isFinite(val)) return; - rs!.offy = Math.min(Math.max(val, 0), Math.max(0, rs!.origH-0.0001)); - rs!.viewY = Math.min(rs!.viewY, Math.max(0, rs!.origH - 1)); + s.offy = Math.min(Math.max(val, 0), Math.max(0, s.origH-0.0001)); + s.viewY = Math.min(s.viewY, Math.max(0, s.origH - 1)); syncAdvancedMeta(); drawAdvancedPreview(); markCalcStale(); }); - rs.dotR.addEventListener('input', (e) => { - rs!.dotr = Math.max(1, Math.round(Number((e.target as HTMLInputElement).value)||1)); - rs!.dotRVal.textContent = String(rs!.dotr); + s.dotR.addEventListener('input', (e) => { + s.dotr = Math.max(1, Math.round(Number((e.target as HTMLInputElement).value)||1)); + s.dotRVal.textContent = String(s.dotr); drawAdvancedPreview(); }); - rs.gridToggle.addEventListener('change', drawAdvancedPreview); + s.gridToggle.addEventListener('change', drawAdvancedPreview); function applyZoom(factor: number) { - const destW = Math.max(50, Math.floor(rs!.advWrap.clientWidth)); - const destH = Math.max(50, Math.floor(rs!.advWrap.clientHeight)); - const sw = Math.max(1, Math.floor(destW / rs!.zoom)); - const sh = Math.max(1, Math.floor(destH / rs!.zoom)); - const cx = rs!.viewX + sw / 2; - const cy = rs!.viewY + sh / 2; - rs!.zoom = Math.min(32, Math.max(0.1, rs!.zoom * factor)); - const sw2 = Math.max(1, Math.floor(destW / rs!.zoom)); - const sh2 = Math.max(1, Math.floor(destH / rs!.zoom)); - rs!.viewX = Math.min(Math.max(0, Math.round(cx - sw2 / 2)), Math.max(0, rs!.origW - sw2)); - rs!.viewY = Math.min(Math.max(0, Math.round(cy - sh2 / 2)), Math.max(0, rs!.origH - sh2)); + const destW = Math.max(50, Math.floor(s.advWrap.clientWidth)); + const destH = Math.max(50, Math.floor(s.advWrap.clientHeight)); + const sw = Math.max(1, Math.floor(destW / s.zoom)); + const sh = Math.max(1, Math.floor(destH / s.zoom)); + const cx = s.viewX + sw / 2; + const cy = s.viewY + sh / 2; + s.zoom = Math.min(32, Math.max(0.1, s.zoom * factor)); + const sw2 = Math.max(1, Math.floor(destW / s.zoom)); + const sh2 = Math.max(1, Math.floor(destH / s.zoom)); + s.viewX = Math.min(Math.max(0, Math.round(cx - sw2 / 2)), Math.max(0, s.origW - sw2)); + s.viewY = Math.min(Math.max(0, Math.round(cy - sh2 / 2)), Math.max(0, s.origH - sh2)); drawAdvancedPreview(); } - rs.zoomIn.addEventListener('click', () => applyZoom(1.25)); - rs.zoomOut.addEventListener('click', () => applyZoom(1/1.25)); - rs.advWrap.addEventListener('wheel', (e) => { + s.zoomIn.addEventListener('click', () => applyZoom(1.25)); + s.zoomOut.addEventListener('click', () => applyZoom(1/1.25)); + s.advWrap.addEventListener('wheel', (e) => { if (!(e as WheelEvent).ctrlKey) return; e.preventDefault(); const delta = (e as WheelEvent).deltaY || 0; @@ -688,59 +705,59 @@ export function buildRSModal() { const onPanDown = (e: PointerEvent) => { if ((e.target as HTMLElement).closest('.op-rs-zoom')) return; - rs!.panning = true; - rs!.panStart = { x: e.clientX, y: e.clientY, viewX: rs!.viewX, viewY: rs!.viewY }; - rs!.advWrap.classList.remove('op-pan-grab'); - rs!.advWrap.classList.add('op-pan-grabbing'); - (rs!.advWrap as any).setPointerCapture?.(e.pointerId); + s.panning = true; + s.panStart = { x: e.clientX, y: e.clientY, viewX: s.viewX, viewY: s.viewY }; + s.advWrap.classList.remove('op-pan-grab'); + s.advWrap.classList.add('op-pan-grabbing'); + (s.advWrap as any).setPointerCapture?.(e.pointerId); }; const onPanMove = (e: PointerEvent) => { - if (!rs!.panning) return; - const dx = e.clientX - rs!.panStart!.x; - const dy = e.clientY - rs!.panStart!.y; - const wrapW = rs!.advWrap.clientWidth; - const wrapH = rs!.advWrap.clientHeight; - const sw = Math.max(1, Math.floor(wrapW / rs!.zoom)); - const sh = Math.max(1, Math.floor(wrapH / rs!.zoom)); - let nx = rs!.panStart!.viewX - Math.round(dx / rs!.zoom); - let ny = rs!.panStart!.viewY - Math.round(dy / rs!.zoom); - nx = Math.min(Math.max(0, nx), Math.max(0, rs!.origW - sw)); - ny = Math.min(Math.max(0, ny), Math.max(0, rs!.origH - sh)); - rs!.viewX = nx; - rs!.viewY = ny; + if (!s.panning || !s.panStart) return; + const dx = e.clientX - s.panStart.x; + const dy = e.clientY - s.panStart.y; + const wrapW = s.advWrap.clientWidth; + const wrapH = s.advWrap.clientHeight; + const sw = Math.max(1, Math.floor(wrapW / s.zoom)); + const sh = Math.max(1, Math.floor(wrapH / s.zoom)); + let nx = s.panStart.viewX - Math.round(dx / s.zoom); + let ny = s.panStart.viewY - Math.round(dy / s.zoom); + nx = Math.min(Math.max(0, nx), Math.max(0, s.origW - sw)); + ny = Math.min(Math.max(0, ny), Math.max(0, s.origH - sh)); + s.viewX = nx; + s.viewY = ny; drawAdvancedPreview(); }; const onPanUp = (e: PointerEvent) => { - if (!rs!.panning) return; - rs!.panning = false; - rs!.panStart = null; - rs!.advWrap.classList.remove('op-pan-grabbing'); - rs!.advWrap.classList.add('op-pan-grab'); - (rs!.advWrap as any).releasePointerCapture?.(e.pointerId); + if (!s.panning) return; + s.panning = false; + s.panStart = null; + s.advWrap.classList.remove('op-pan-grabbing'); + s.advWrap.classList.add('op-pan-grab'); + (s.advWrap as any).releasePointerCapture?.(e.pointerId); }; - rs.advWrap.addEventListener('pointerdown', onPanDown); - rs.advWrap.addEventListener('pointermove', onPanMove); - rs.advWrap.addEventListener('pointerup', onPanUp); - rs.advWrap.addEventListener('pointercancel', onPanUp); - rs.advWrap.addEventListener('pointerleave', onPanUp); + s.advWrap.addEventListener('pointerdown', onPanDown); + s.advWrap.addEventListener('pointermove', onPanMove); + s.advWrap.addEventListener('pointerup', onPanUp); + s.advWrap.addEventListener('pointercancel', onPanUp); + s.advWrap.addEventListener('pointerleave', onPanUp); const close = () => closeRSModal(); - rs.cancelBtn.addEventListener('click', close); - rs.closeBtn.addEventListener('click', close); + s.cancelBtn.addEventListener('click', close); + s.closeBtn.addEventListener('click', close); backdrop.addEventListener('click', close); - rs.calcBtn.addEventListener('click', async () => { - if (rs!.mode !== 'advanced' || !rs!.img) return; + s.calcBtn.addEventListener('click', async () => { + if (s.mode !== 'advanced' || !s.img) return; try { const { cols, rows } = sampleDims(); 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; } - const canvas = await reconstructViaGrid(rs!.img, rs!.origW, rs!.origH, rs!.offx, rs!.offy, rs!.gapX, rs!.gapY); - rs!.calcCanvas = canvas; - rs!.calcCols = cols; - rs!.calcRows = rows; - rs!.calcReady = true; - rs!.applyBtn.disabled = false; + const canvas = await reconstructViaGrid(s.img, s.origW, s.origH, s.offx, s.offy, s.gapX, s.gapY); + s.calcCanvas = canvas; + s.calcCols = cols; + s.calcRows = rows; + s.calcReady = true; + s.applyBtn.disabled = false; drawAdvancedResultPreview(); updateFooterMeta(); showToast(`Calculated ${cols}×${rows}. Review preview, then Apply.`); @@ -750,29 +767,29 @@ export function buildRSModal() { } }); - rs.applyBtn.addEventListener('click', async () => { - if (!rs!.ov) return; + s.applyBtn.addEventListener('click', async () => { + if (!s.ov) return; try { - if (rs!.mode === 'simple') { - const W = parseInt(rs!.w.value||'0',10); - const H = parseInt(rs!.h.value||'0',10); + if (s.mode === 'simple') { + const W = parseInt(s.w.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 (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(); showToast(`Resized to ${W}×${H}.`); } else { - if (!rs!.calcReady || !rs!.calcCanvas) { showToast('Calculate first.'); return; } - const dataUrl = await canvasToDataURLSafe(rs!.calcCanvas); - rs!.ov.imageBase64 = dataUrl; - rs!.ov.imageUrl = null; - rs!.ov.isLocal = true; + if (!s.calcReady || !s.calcCanvas) { showToast('Calculate first.'); return; } + const dataUrl = await canvasToDataURLSafe(s.calcCanvas); + s.ov.imageBase64 = dataUrl; + s.ov.imageUrl = null; + s.ov.isLocal = true; await saveConfig(['overlays']); clearOverlayCache(); ensureHook(); emitOverlayChanged(); closeRSModal(); - showToast(`Applied ${rs!.calcCols}×${rs!.calcRows}.`); + showToast(`Applied ${s.calcCols}×${s.calcRows}.`); } } catch (e) { console.error(e); @@ -781,51 +798,52 @@ export function buildRSModal() { }); function syncSimpleNote() { - const W = parseInt(rs!.w.value||'0',10); - const H = parseInt(rs!.h.value||'0',10); + const W = parseInt(s.w.value||'0',10); + const H = parseInt(s.h.value||'0',10); const ok = Number.isFinite(W) && Number.isFinite(H) && W>0 && H>0; const limit = (W >= MAX_OVERLAY_DIM || H >= MAX_OVERLAY_DIM); const simpleText = ok ? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : `Target: ${W}×${H} (OK)`) : 'Enter positive width and height.'; - if (rs!.note) rs!.note.textContent = simpleText; - if (rs!.mode === 'simple') rs!.applyBtn.disabled = (!ok || limit); - if (rs!.mode === 'simple') rs!.meta.textContent = simpleText; + if (s.note) s.note.textContent = simpleText; + if (s.mode === 'simple') s.applyBtn.disabled = (!ok || limit); + if (s.mode === 'simple') s.meta.textContent = simpleText; } function applyScaleToFields(scale: number) { - const W = Math.max(1, Math.round(rs!.origW * scale)); - const H = Math.max(1, Math.round(rs!.origH * scale)); - rs!.updating = true; - rs!.w.value = String(W); - rs!.h.value = rs!.lock.checked ? String(Math.max(1, Math.round(W * rs!.origH / rs!.origW))) : String(H); - rs!.updating = false; + const W = Math.max(1, Math.round(s.origW * scale)); + const H = Math.max(1, Math.round(s.origH * scale)); + s.updating = true; + s.w.value = String(W); + s.h.value = s.lock.checked ? String(Math.max(1, Math.round(W * s.origH / s.origW))) : String(H); + s.updating = false; syncSimpleNote(); } function syncAdvFieldsToState() { - rs!.updating = true; - rs!.multRange.value = String(rs!.mult); - rs!.multInput.value = String(rs!.mult); - rs!.blockW.value = String(rs!.gapX); - rs!.blockH.value = String(rs!.gapY); - rs!.offX.value = String(rs!.offx); - rs!.offY.value = String(rs!.offy); - rs!.dotR.value = String(rs!.dotr); - rs!.dotRVal.textContent = String(rs!.dotr); - rs!.updating = false; + s.updating = true; + s.multRange.value = String(s.mult); + s.multInput.value = String(s.mult); + s.blockW.value = String(s.gapX); + s.blockH.value = String(s.gapY); + s.offX.value = String(s.offx); + s.offY.value = String(s.offy); + s.dotR.value = String(s.dotr); + s.dotRVal.textContent = String(s.dotr); + s.updating = false; } - rs._syncAdvancedMeta = syncAdvancedMeta; - rs._syncSimpleNote = syncSimpleNote; + s._syncAdvancedMeta = syncAdvancedMeta; + s._syncSimpleNote = syncSimpleNote; - rs._resizeHandler = () => { - if (rs!.mode === 'simple') rs!._drawSimplePreview?.(); + s._resizeHandler = () => { + if (!rs) return; + if (rs.mode === 'simple') rs._drawSimplePreview?.(); else { - rs!._drawAdvancedPreview?.(); - rs!._drawAdvancedResultPreview?.(); + rs._drawAdvancedPreview?.(); + rs._drawAdvancedResultPreview?.(); } }; - window.addEventListener('resize', rs._resizeHandler); + window.addEventListener('resize', s._resizeHandler); } export function openRSModal(overlay: any) { @@ -834,60 +852,64 @@ export function openRSModal(overlay: any) { const img = new Image(); img.onload = () => { - rs!.img = img; - rs!.origW = img.width; rs!.origH = img.height; + const s = rs; + if (!s) return; - rs!.orig.value = `${rs!.origW}×${rs!.origH}`; - rs!.w.value = String(rs!.origW); - rs!.h.value = String(rs!.origH); - rs!.lock.checked = true; + s.img = img; + s.origW = img.width; s.origH = img.height; - rs!.zoom = 1.0; - rs!.mult = 4; - rs!.gapX = 4; rs!.gapY = 4; - rs!.offx = 0; rs!.offy = 0; - rs!.dotr = 1; - rs!.viewX = 0; rs!.viewY = 0; + s.orig.value = `${s.origW}×${s.origH}`; + s.w.value = String(s.origW); + s.h.value = String(s.origH); + s.lock.checked = true; - rs!.bind.checked = true; - rs!.multRange.value = '4'; - rs!.multInput.value = '4'; - rs!.blockW.value = '4'; - rs!.blockH.value = '4'; - rs!.offX.value = '0'; - rs!.offY.value = '0'; - rs!.dotR.value = '1'; - rs!.dotRVal.textContent = '1'; - rs!.gridToggle.checked = true; + s.zoom = 1.0; + s.mult = 4; + s.gapX = 4; s.gapY = 4; + s.offx = 0; s.offy = 0; + s.dotr = 1; + s.viewX = 0; s.viewY = 0; - rs!.calcCanvas = null; - rs!.calcCols = 0; - rs!.calcRows = 0; - rs!.calcReady = false; - rs!.applyBtn.disabled = (rs!.mode === 'advanced'); + s.bind.checked = true; + s.multRange.value = '4'; + s.multInput.value = '4'; + s.blockW.value = '4'; + 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'); - rs!.backdrop.classList.add('show'); - rs!.modal.style.display = 'flex'; + s.backdrop.classList.add('show'); + s.modal.style.display = 'flex'; - rs!._drawSimplePreview?.(); - rs!._drawAdvancedPreview?.(); - rs!._drawAdvancedResultPreview?.(); - rs!._syncAdvancedMeta?.(); - rs!._syncSimpleNote?.(); + s._drawSimplePreview?.(); + s._drawAdvancedPreview?.(); + s._drawAdvancedResultPreview?.(); + s._syncAdvancedMeta?.(); + s._syncSimpleNote?.(); const setFooterNow = () => { - if (rs!.mode === 'advanced') { - const cols = Math.floor((rs!.origW - rs!.offx) / rs!.gapX); - 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.'; + if (!rs) return; + if (rs.mode === 'advanced') { + const cols = Math.floor((rs.origW - rs.offx) / rs.gapX); + 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 { - 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 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(); @@ -906,8 +928,9 @@ function closeRSModal() { } 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 sctx = srcCanvas.getContext('2d', { willReadFrequently: true })!; + const srcCanvas = createCanvas(origW, origH) as HTMLCanvasElement; + const sctx = srcCanvas.getContext('2d', { willReadFrequently: true }); + if (!sctx) throw new Error('Failed to get 2d context for srcCanvas.'); sctx.imageSmoothingEnabled = false; sctx.drawImage(img, 0, 0); const srcData = sctx.getImageData(0,0,origW,origH).data; @@ -917,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'); 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 odata = out.data; @@ -951,7 +975,8 @@ async function reconstructViaGrid(img: HTMLImageElement, origW: number, origH: n async function resizeOverlayImage(ov: any, targetW: number, targetH: number) { const img = await loadImage(ov.imageBase64); 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.clearRect(0,0,targetW,targetH); ctx.drawImage(img, 0,0, img.width,img.height, 0,0, targetW,targetH);