refactor: replace imperative ref-based DOM manipulation with declarative styles in PreviewPanel

This commit is contained in:
Ben
2026-05-15 01:55:37 -07:00
parent 277ff29298
commit 67260d78e7
2 changed files with 27 additions and 60 deletions
+11 -15
View File
@@ -1,4 +1,4 @@
import { useRef, useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { PreviewMode, TilesetSettings } from '../lib/imageProcessor'; import { PreviewMode, TilesetSettings } from '../lib/imageProcessor';
type Tab = 'original' | 'tile' | 'repeat'; type Tab = 'original' | 'tile' | 'repeat';
@@ -21,7 +21,6 @@ export default function PreviewPanel({
const [tab, setTab] = useState<Tab>('original'); const [tab, setTab] = useState<Tab>('original');
const [sourceUrl, setSourceUrl] = useState<string | null>(null); const [sourceUrl, setSourceUrl] = useState<string | null>(null);
const [resultUrl, setResultUrl] = useState<string | null>(null); const [resultUrl, setResultUrl] = useState<string | null>(null);
const repeatRef = useRef<HTMLDivElement>(null);
// Generate object URL for source image // Generate object URL for source image
useEffect(() => { useEffect(() => {
@@ -39,21 +38,12 @@ export default function PreviewPanel({
setTab('tile'); setTab('tile');
}, [resultCanvas]); }, [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 hasResult = !!resultCanvas;
const hasSource = !!sourceFile; const hasSource = !!sourceFile;
const gridCount = previewModeToGrid(settings.previewMode);
const tileSize = Math.floor(400 / gridCount);
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Tab bar */} {/* Tab bar */}
@@ -127,7 +117,13 @@ export default function PreviewPanel({
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<div <div
className="preview-repeat-wrap" className="preview-repeat-wrap"
ref={repeatRef} style={{
backgroundImage: `url(${resultUrl})`,
backgroundSize: `${tileSize}px ${tileSize}px`,
backgroundRepeat: 'repeat',
width: `${tileSize * gridCount}px`,
height: `${tileSize * gridCount}px`,
}}
aria-label="Repeated texture preview" aria-label="Repeated texture preview"
/> />
<div style={{ <div style={{
+16 -45
View File
@@ -136,6 +136,7 @@ export function applyWrappedOffset(
export function repairCenterSeams( export function repairCenterSeams(
source: HTMLCanvasElement, source: HTMLCanvasElement,
original: HTMLCanvasElement,
settings: Pick<TilesetSettings, 'seamWidth' | 'blendStrength' | 'detailPreservation'>, settings: Pick<TilesetSettings, 'seamWidth' | 'blendStrength' | 'detailPreservation'>,
): HTMLCanvasElement { ): HTMLCanvasElement {
const { width: w, height: h } = source; const { width: w, height: h } = source;
@@ -150,8 +151,12 @@ export function repairCenterSeams(
const srcData = srcCtx.getImageData(0, 0, w, h); const srcData = srcCtx.getImageData(0, 0, w, h);
const outData = outCtx.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 src = srcData.data;
const dst = outData.data; const dst = outData.data;
const orig = origData.data;
// How much original pixel to keep (adds "detail preservation") // How much original pixel to keep (adds "detail preservation")
const detailRetain = const detailRetain =
@@ -161,34 +166,6 @@ export function repairCenterSeams(
const blendFactor = blendStrength / 100; const blendFactor = blendStrength / 100;
const halfSeam = seamWidth / 2; 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 cx = Math.floor(w / 2);
const cy = Math.floor(h / 2); const cy = Math.floor(h / 2);
@@ -204,26 +181,20 @@ export function repairCenterSeams(
if (!inV && !inH) continue; 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++) { for (let ch = 0; ch < 4; ch++) {
const orig = src[idx + ch]; const offsetPx = src[idx + ch];
let blended = orig; const unoffsetPx = orig[idx + ch];
if (inV) { const blended = offsetPx * (1 - mask) + unoffsetPx * mask;
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;
}
// Apply detail preservation — pull back toward original // 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…'); onProgress?.('Repairing seams…');
// Run seam repair in next microtask so the UI can update // Run seam repair in next microtask so the UI can update
await new Promise<void>((res) => setTimeout(res, 0)); await new Promise<void>((res) => setTimeout(res, 0));
const repaired = repairCenterSeams(offsetCanvas, settings); const repaired = repairCenterSeams(offsetCanvas, normalized, settings);
onProgress?.('Done'); onProgress?.('Done');
return repaired; return repaired;