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';
type Tab = 'original' | 'tile' | 'repeat';
@@ -21,7 +21,6 @@ export default function PreviewPanel({
const [tab, setTab] = useState<Tab>('original');
const [sourceUrl, setSourceUrl] = useState<string | null>(null);
const [resultUrl, setResultUrl] = useState<string | null>(null);
const repeatRef = useRef<HTMLDivElement>(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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Tab bar */}
@@ -127,7 +117,13 @@ export default function PreviewPanel({
<div style={{ position: 'relative' }}>
<div
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"
/>
<div style={{
+16 -45
View File
@@ -136,6 +136,7 @@ export function applyWrappedOffset(
export function repairCenterSeams(
source: HTMLCanvasElement,
original: HTMLCanvasElement,
settings: Pick<TilesetSettings, 'seamWidth' | 'blendStrength' | 'detailPreservation'>,
): 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<void>((res) => setTimeout(res, 0));
const repaired = repairCenterSeams(offsetCanvas, settings);
const repaired = repairCenterSeams(offsetCanvas, normalized, settings);
onProgress?.('Done');
return repaired;