refactor: replace imperative ref-based DOM manipulation with declarative styles in PreviewPanel
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user