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';
|
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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user