From 67260d78e7bfb38041f4b086c232d0ce36375173 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 15 May 2026 01:55:37 -0700 Subject: [PATCH] refactor: replace imperative ref-based DOM manipulation with declarative styles in PreviewPanel --- src/components/PreviewPanel.tsx | 26 ++++++-------- src/lib/imageProcessor.ts | 61 +++++++++------------------------ 2 files changed, 27 insertions(+), 60 deletions(-) diff --git a/src/components/PreviewPanel.tsx b/src/components/PreviewPanel.tsx index 637e9e8..23e5665 100644 --- a/src/components/PreviewPanel.tsx +++ b/src/components/PreviewPanel.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { PreviewMode, TilesetSettings } from '../lib/imageProcessor'; type Tab = 'original' | 'tile' | 'repeat'; @@ -21,7 +21,6 @@ export default function PreviewPanel({ const [tab, setTab] = useState('original'); const [sourceUrl, setSourceUrl] = useState(null); const [resultUrl, setResultUrl] = useState(null); - const repeatRef = useRef(null); // Generate object URL for source image useEffect(() => { @@ -39,21 +38,12 @@ export default function PreviewPanel({ setTab('tile'); }, [resultCanvas]); - // Update repeated preview background - useEffect(() => { - if (!repeatRef.current || !resultUrl) return; - const gridCount = previewModeToGrid(settings.previewMode); - const tileSize = Math.floor(400 / gridCount); - repeatRef.current.style.backgroundImage = `url(${resultUrl})`; - repeatRef.current.style.backgroundSize = `${tileSize}px ${tileSize}px`; - repeatRef.current.style.backgroundRepeat = 'repeat'; - repeatRef.current.style.width = `${tileSize * gridCount}px`; - repeatRef.current.style.height = `${tileSize * gridCount}px`; - }, [resultUrl, settings.previewMode]); - const hasResult = !!resultCanvas; const hasSource = !!sourceFile; + const gridCount = previewModeToGrid(settings.previewMode); + const tileSize = Math.floor(400 / gridCount); + return (
{/* Tab bar */} @@ -127,7 +117,13 @@ export default function PreviewPanel({
, ): HTMLCanvasElement { const { width: w, height: h } = source; @@ -150,8 +151,12 @@ export function repairCenterSeams( const srcData = srcCtx.getImageData(0, 0, w, h); const outData = outCtx.getImageData(0, 0, w, h); + const origCtx = original.getContext('2d')!; + const origData = origCtx.getImageData(0, 0, w, h); + const src = srcData.data; const dst = outData.data; + const orig = origData.data; // How much original pixel to keep (adds "detail preservation") const detailRetain = @@ -161,34 +166,6 @@ export function repairCenterSeams( const blendFactor = blendStrength / 100; const halfSeam = seamWidth / 2; - // Helper: get RGBA at (x, y) with wrapping - const getPixel = (x: number, y: number, ch: number): number => { - const px = ((x % w) + w) % w; - const py = ((y % h) + h) % h; - return src[(py * w + px) * 4 + ch]; - }; - - // Helper: sample a soft average of neighbor pixels across the seam - const sampleNeighbors = ( - x: number, - y: number, - direction: 'h' | 'v', // 'h' = horizontal seam (sample above/below), 'v' = vertical - reach: number, - ch: number, - ): number => { - let total = 0; - let weight = 0; - const steps = Math.min(reach, 8); // sample up to 8 pixels away - for (let i = 1; i <= steps; i++) { - const w1 = (steps - i + 1) / steps; - const [ax, ay] = direction === 'v' ? [x + i, y] : [x, y + i]; - const [bx, by] = direction === 'v' ? [x - i, y] : [x, y - i]; - total += getPixel(ax, ay, ch) * w1 + getPixel(bx, by, ch) * w1; - weight += 2 * w1; - } - return weight > 0 ? total / weight : getPixel(x, y, ch); - }; - const cx = Math.floor(w / 2); const cy = Math.floor(h / 2); @@ -204,26 +181,20 @@ export function repairCenterSeams( if (!inV && !inH) continue; + let mask = 0; + if (inV) mask = Math.max(mask, 1 - smoothstep(0, halfSeam, dv)); + if (inH) mask = Math.max(mask, 1 - smoothstep(0, halfSeam, dh)); + + mask *= blendFactor; + for (let ch = 0; ch < 4; ch++) { - const orig = src[idx + ch]; - let blended = orig; + const offsetPx = src[idx + ch]; + const unoffsetPx = orig[idx + ch]; - if (inV) { - const tV = 1 - smoothstep(0, halfSeam, dv); - const neighborV = sampleNeighbors(x, y, 'v', Math.ceil(halfSeam), ch); - const mixV = orig * (1 - tV * blendFactor) + neighborV * (tV * blendFactor); - blended = mixV; - } - - if (inH) { - const tH = 1 - smoothstep(0, halfSeam, dh); - const neighborH = sampleNeighbors(x, y, 'h', Math.ceil(halfSeam), ch); - const mixH = blended * (1 - tH * blendFactor) + neighborH * (tH * blendFactor); - blended = mixH; - } + const blended = offsetPx * (1 - mask) + unoffsetPx * mask; // Apply detail preservation — pull back toward original - dst[idx + ch] = Math.round(blended * (1 - detailRetain) + orig * detailRetain); + dst[idx + ch] = Math.round(blended * (1 - detailRetain) + offsetPx * detailRetain); } } } @@ -255,7 +226,7 @@ export async function generateTileableTexture( onProgress?.('Repairing seams…'); // Run seam repair in next microtask so the UI can update await new Promise((res) => setTimeout(res, 0)); - const repaired = repairCenterSeams(offsetCanvas, settings); + const repaired = repairCenterSeams(offsetCanvas, normalized, settings); onProgress?.('Done'); return repaired;