diff --git a/string-art-demo/src/App.css b/string-art-demo/src/App.css index 27d7713..b0313c4 100644 --- a/string-art-demo/src/App.css +++ b/string-art-demo/src/App.css @@ -1,10 +1,11 @@ .app { min-height: 100vh; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - padding: 1rem; + width: 100vw; + background: linear-gradient(0deg, #8f91ff 0%, #7476ff 100%); } .app-header { + padding-top: 1rem; text-align: center; color: white; margin-bottom: 2rem; @@ -47,6 +48,8 @@ background: white; border-radius: 8px; padding: 1.5rem; + display: flex; + flex-direction: column; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } @@ -89,6 +92,7 @@ } .generate-button { + margin-top: 15px; width: 100%; padding: 1rem; background: #28a745; diff --git a/string-art-demo/src/App.tsx b/string-art-demo/src/App.tsx index 634f1c8..18d1e8e 100644 --- a/string-art-demo/src/App.tsx +++ b/string-art-demo/src/App.tsx @@ -1,19 +1,17 @@ import { useState, useCallback } from 'react'; import { ImageUploader } from './components/ImageUploader'; import { StringArtCanvas } from './components/StringArtCanvas'; -import { useStringArt, type StringArtConfig, type ProgressInfo } from './hooks/useStringArt'; import './App.css'; - +import StringArtConfigSection from './components/StringArtConfig/StringArtConfigSection'; +import { useStringArt, type ProgressInfo } from './useStringArt'; function App() { - const { wasmModule, isLoading, error, generateStringArt, presets } = useStringArt(); const [imageData, setImageData] = useState(null); const [imageUrl, setImageUrl] = useState(''); const [isGenerating, setIsGenerating] = useState(false); const [currentPath, setCurrentPath] = useState([]); const [nailCoords, setNailCoords] = useState>([]); const [progress, setProgress] = useState(null); - const [config, setConfig] = useState(presets.balanced()); - + const { generateStringArt, isLoading, error, settings } = useStringArt(); const handleImageSelected = useCallback((data: Uint8Array, url: string) => { setImageData(data); setImageUrl(url); @@ -22,8 +20,8 @@ function App() { setNailCoords([]); // Clear until we generate the string art }, []); - const handleStartGeneration = useCallback(async () => { - if (!imageData || !wasmModule) return; + const handleStartGeneration = async () => { + if (!imageData) return; setIsGenerating(true); setCurrentPath([]); @@ -41,9 +39,9 @@ function App() { setNailCoords(coords); }; - const result = await generateStringArt(imageData, config, onProgress, onNailCoords); + const result = await generateStringArt(imageData, onProgress, onNailCoords); if (result.path) { - setCurrentPath(result.path); + setCurrentPath((prevPath) => [...prevPath, ...(result.path || [])]); } if (result.nailCoords.length > 0) { setNailCoords(result.nailCoords); @@ -54,11 +52,7 @@ function App() { } finally { setIsGenerating(false); } - }, [imageData, wasmModule, config, generateStringArt]); - - const handlePresetChange = useCallback((presetName: 'fast' | 'balanced' | 'highQuality') => { - setConfig(presets[presetName]()); - }, [presets]); + }; if (isLoading) { return ( @@ -98,33 +92,8 @@ function App() { {imageData && (
-
-

Quality Presets

-
- - - -
-
- + +
- Current: Nail {progress.current_nail} → {progress.next_nail} Score: {progress.score.toFixed(1)}
diff --git a/string-art-demo/src/components/StringArtCanvas.tsx b/string-art-demo/src/components/StringArtCanvas.tsx index 28fc0ce..9cb40e3 100644 --- a/string-art-demo/src/components/StringArtCanvas.tsx +++ b/string-art-demo/src/components/StringArtCanvas.tsx @@ -27,14 +27,21 @@ export const StringArtCanvas: React.FC = ({ const ctx = canvas.getContext('2d'); if (!ctx) return; - // Clear canvas + // Always clear the canvas at the start of a render ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, width, height); + ctx.clearRect(0, 0, width, height); + + let isCancelled = false; if (showOriginal && imageUrl) { - // Show original image const img = new Image(); img.onload = () => { + if (isCancelled) return; + + // Clear canvas again right before drawing to prevent race conditions + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, width, height); + const scale = Math.min(width / img.width, height / img.height); const scaledWidth = img.width * scale; const scaledHeight = img.height * scale; @@ -44,38 +51,41 @@ export const StringArtCanvas: React.FC = ({ ctx.drawImage(img, x, y, scaledWidth, scaledHeight); }; img.src = imageUrl; - return; - } - - // Draw nails as small circles - ctx.fillStyle = '#666'; - nailCoords.forEach(([x, y]) => { - ctx.beginPath(); - ctx.arc(x, y, 2, 0, 2 * Math.PI); - ctx.fill(); - }); - - // Draw string path - if (currentPath.length > 1) { - ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)'; - ctx.lineWidth = 0.5; - ctx.globalCompositeOperation = 'multiply'; - - for (let i = 0; i < currentPath.length - 1; i++) { - const fromNail = currentPath[i]; - const toNail = currentPath[i + 1]; - - if (fromNail < nailCoords.length && toNail < nailCoords.length) { - const [x1, y1] = nailCoords[fromNail]; - const [x2, y2] = nailCoords[toNail]; + } else { + // Draw nails as small circles + ctx.fillStyle = '#666'; + nailCoords.forEach(([x, y]) => { + ctx.beginPath(); + ctx.arc(x, y, 2, 0, 2 * Math.PI); + ctx.fill(); + }); + + // Draw string path + if (currentPath.length > 1) { + ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)'; + ctx.lineWidth = 0.5; + ctx.globalCompositeOperation = 'multiply'; + + for (let i = 0; i < currentPath.length - 1; i++) { + const fromNail = currentPath[i]; + const toNail = currentPath[i + 1]; - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.stroke(); + if (fromNail < nailCoords.length && toNail < nailCoords.length) { + const [x1, y1] = nailCoords[fromNail]; + const [x2, y2] = nailCoords[toNail]; + + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + } } } } + + return () => { + isCancelled = true; + }; }, [width, height, nailCoords, currentPath, showOriginal, imageUrl]); return ( @@ -206,4 +216,4 @@ export const StringArtCanvas: React.FC = ({ `} ); -}; \ No newline at end of file +}; diff --git a/string-art-demo/src/components/StringArtConfig/StringArtConfigSection.tsx b/string-art-demo/src/components/StringArtConfig/StringArtConfigSection.tsx new file mode 100644 index 0000000..ff6ea67 --- /dev/null +++ b/string-art-demo/src/components/StringArtConfig/StringArtConfigSection.tsx @@ -0,0 +1,91 @@ +import { useEffect, useState, type RefObject } from "react"; +import { type StringArtConfig } from "../../useStringArt"; + +const Slider = ({ + title, + settingsRef, + index, +}: { + title: string; + settingsRef: RefObject; + index: keyof StringArtConfig; +}) => { + const minMaxVals: Record< + keyof Omit< + StringArtConfig, + | "extract_subject" + | "remove_shadows" + | "preserve_eyes" + | "preserve_negative_space" + >, + [number, number, number, number] + > = { + //min, max, start + image_size: [500, 2000, 500, 100], + line_darkness: [25, 300, 100, 5], + max_lines: [800, 5000, 1000, 100], + min_improvement_score: [0, 100, 15, 1], + negative_space_penalty: [0, 100, 0, 1], + negative_space_threshold: [0, 100, 0, 1], + num_nails: [360, 1440, 360, 360/4], + progress_frequency: [200, 500, 200, 50], + }; + + const [val, setVal] = useState("0"); + const dontRender = !index || !Object.keys(minMaxVals).includes(index as string) + const [min, max, start, step] = dontRender? [0,0,0,0] : minMaxVals[index as keyof typeof minMaxVals]; + + useEffect(() => { + setVal(String(start)) + }, [start]) + + if (dontRender) return null; + if (!settingsRef?.current) return null; + return ( +
+ { + (settingsRef.current[index] as unknown) = target.value; + setVal(target.value); + }} + /> + +
+ ); +}; + +const StringArtConfigSection = ({ + settings, +}: { + settings: RefObject; +}) => { + if (!settings.current) { + return null; + } + return ( +
+

String Art Configuration

+ {(Object.keys(settings.current) as (keyof StringArtConfig)[]).map( + (key) => { + return ( + + ); + } + )} +
+ ); +}; + +export default StringArtConfigSection; diff --git a/string-art-demo/src/components/StringArtConfig/index.ts b/string-art-demo/src/components/StringArtConfig/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/string-art-demo/src/hooks/useStringArt.ts b/string-art-demo/src/hooks/useStringArt.ts deleted file mode 100644 index ee6d4cf..0000000 --- a/string-art-demo/src/hooks/useStringArt.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import init, { - StringArtWasm, - WasmStringArtConfig, - test_wasm, - get_version -} from '../wasm/string_art_rust_impl.js'; - -interface StringArtConfig { - num_nails: number; - image_size: number; - extract_subject: boolean; - remove_shadows: boolean; - preserve_eyes: boolean; - preserve_negative_space: boolean; - negative_space_penalty: number; - negative_space_threshold: number; -} - -interface ProgressInfo { - lines_completed: number; - total_lines: number; - current_nail: number; - next_nail: number; - score: number; - completion_percent: number; - path_segment: [number, number]; - current_path: number[]; // Added current_path to match the updated callback -} - -interface WasmModule { - StringArtWasm: typeof StringArtWasm; - WasmStringArtConfig: typeof WasmStringArtConfig; - test_wasm: typeof test_wasm; - get_version: typeof get_version; -} - -export interface UseStringArtReturn { - wasmModule: WasmModule | null; - isLoading: boolean; - error: string | null; - generateStringArt: ( - imageData: Uint8Array, - config: StringArtConfig, - onProgress: (progress: ProgressInfo) => void, - onNailCoords?: (coords: Array<[number, number]>) => void - ) => Promise<{ path: number[] | null; nailCoords: Array<[number, number]> }>; - presets: { - fast: () => StringArtConfig; - balanced: () => StringArtConfig; - highQuality: () => StringArtConfig; - }; -} - -export const useStringArt = (): UseStringArtReturn => { - const [wasmModule, setWasmModule] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const loadWasm = async () => { - try { - setIsLoading(true); - setError(null); - - // Initialize the WASM module - await init(); - - // Create the WASM interface - const wasmInterface: WasmModule = { - StringArtWasm, - WasmStringArtConfig, - test_wasm, - get_version, - }; - - setWasmModule(wasmInterface); - console.log('WASM module loaded successfully:', test_wasm()); - - } catch (err) { - console.error('Failed to load WASM module:', err); - setError(err instanceof Error ? err.message : 'Failed to load WASM module'); - } finally { - setIsLoading(false); - } - }; - - loadWasm(); - }, []); - - const workerRef = useRef(null); - - useEffect(() => { - // Initialize the service worker - workerRef.current = new Worker(new URL('../workers/stringArtWorker.ts', import.meta.url), { type: 'module' }); - return () => { - workerRef.current?.terminate(); - }; - }, []); - - const generateStringArt = useCallback( - async ( - imageData: Uint8Array, - config: StringArtConfig, - onProgress: (progress: ProgressInfo) => void, - onNailCoords?: (coords: Array<[number, number]>) => void - ): Promise<{ path: number[] | null; nailCoords: Array<[number, number]> }> => { - if (!workerRef.current) { - throw new Error('Service worker not initialized'); - } - - return new Promise((resolve, reject) => { - workerRef.current!.onmessage = (event) => { - const { type, data } = event.data; - - if (type === 'nailCoords' && onNailCoords) { - onNailCoords(data); - } else if (type === 'progress') { - onProgress(data); - } else if (type === 'complete') { - resolve({ path: data, nailCoords: [] }); - } else if (type === 'error') { - reject(new Error(data)); - } - }; - - workerRef.current!.postMessage({ - imageData, - config, - wasmModuleUrl: '../wasm/string_art_rust_impl.js', - }); - }); - }, - [] -); - - const presets = { - fast: () => ({ num_nails: 360, image_size: 500, extract_subject: true, remove_shadows: true, preserve_eyes: true, preserve_negative_space: true, negative_space_penalty: 5, negative_space_threshold: 5 }), - balanced: () => ({ num_nails: 720, image_size: 1000, extract_subject: true, remove_shadows: true, preserve_eyes: true, preserve_negative_space: true, negative_space_penalty: 10, negative_space_threshold: 10 }), - highQuality: () => ({ num_nails: 1440, image_size: 2000, extract_subject: true, remove_shadows: true, preserve_eyes: true, preserve_negative_space: true, negative_space_penalty: 15, negative_space_threshold: 20 }), - }; - - return { - wasmModule, - isLoading, - error, - generateStringArt, - presets, - }; -}; - -export type { StringArtConfig, ProgressInfo }; diff --git a/string-art-demo/src/interfaces/stringArtConfig.ts b/string-art-demo/src/interfaces/stringArtConfig.ts new file mode 100644 index 0000000..f50e27e --- /dev/null +++ b/string-art-demo/src/interfaces/stringArtConfig.ts @@ -0,0 +1,17 @@ +import type { WasmStringArtConfig } from "../wasm/string_art_rust_impl"; + +export type StringArtConfig = Pick & { + max_lines: number, + line_darkness: number, + min_improvement_score: number, + progress_frequency: number, +} \ No newline at end of file diff --git a/string-art-demo/src/main.tsx b/string-art-demo/src/main.tsx index bef5202..9d93057 100644 --- a/string-art-demo/src/main.tsx +++ b/string-art-demo/src/main.tsx @@ -1,10 +1,7 @@ -import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( - - - , + , ) diff --git a/string-art-demo/src/useStringArt.ts b/string-art-demo/src/useStringArt.ts new file mode 100644 index 0000000..853f5b9 --- /dev/null +++ b/string-art-demo/src/useStringArt.ts @@ -0,0 +1,125 @@ +import { + useState, + useEffect, + useCallback, + useRef, +} from "react"; +import type { StringArtConfig } from "./interfaces/stringArtConfig"; +import init, { + StringArtWasm, + WasmStringArtConfig, + test_wasm, + get_version, + ProgressInfo, +} from "./wasm/string_art_rust_impl"; +import workerUrl from "./workers/stringArtWorker?url" + +interface WasmModule { + StringArtWasm: typeof StringArtWasm; + WasmStringArtConfig: typeof WasmStringArtConfig; + test_wasm: typeof test_wasm; + get_version: typeof get_version; +} + +export const useStringArt = () => { + const [, setWasmModule] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const settings = useRef({ + num_nails: 500, + image_size: 500, + extract_subject: false, + remove_shadows: false, + preserve_eyes: true, + preserve_negative_space: false, + negative_space_penalty: 5, + negative_space_threshold: 0.5, + max_lines: 1000, + line_darkness: 50, + min_improvement_score: 15, + progress_frequency: 300, + } as StringArtConfig); + + useEffect(() => { + const loadWasm = async () => { + try { + setIsLoading(true); + setError(null); + + // Initialize the WASM module + await init(); + + // Create the WASM interface + const wasmInterface: WasmModule = { + StringArtWasm, + WasmStringArtConfig, + test_wasm, + get_version, + }; + + setWasmModule(wasmInterface); + console.log("WASM module loaded successfully:", test_wasm()); + } catch (err) { + console.error("Failed to load WASM module:", err); + setError( + err instanceof Error ? err.message : "Failed to load WASM module" + ); + } + setTimeout(() => setIsLoading(false), 1000); + }; + + loadWasm(); + }, [setIsLoading, setError, setWasmModule]); + + const workerRef = useRef(null); + + useEffect(() => { + // Initialize the service worker + workerRef.current = new Worker(workerUrl, { type: "module" }); + return () => { + workerRef.current?.terminate(); + }; + }, []); + + const generateStringArt = useCallback( + async ( + imageData: Uint8Array, + onProgress: (progress: ProgressInfo) => void, + onNailCoords?: (coords: Array<[number, number]>) => void + ): Promise<{ + path: number[] | null; + nailCoords: Array<[number, number]>; + }> => { + if (!workerRef.current) { + throw new Error("Service worker not initialized"); + } + + return new Promise((resolve, reject) => { + workerRef.current!.onmessage = (event) => { + const { type, data } = event.data; + + if (type === "nailCoords" && onNailCoords) { + onNailCoords(data); + } else if (type === "progress") { + onProgress(data); + } else if (type === "complete") { + resolve({ path: data, nailCoords: [] }); + } else if (type === "error") { + reject(new Error(data)); + } + }; + + workerRef.current!.postMessage({ + imageData, + config: settings.current, + wasmModuleUrl: "../wasm/string_art_rust_impl.js", + }); + }); + }, + [settings] + ); + + return { isLoading, error, generateStringArt, settings }; +}; + +export type { StringArtConfig, ProgressInfo }; diff --git a/string-art-demo/src/workers/stringArtWorker.ts b/string-art-demo/src/workers/stringArtWorker.ts index 3855a14..2e5a554 100644 --- a/string-art-demo/src/workers/stringArtWorker.ts +++ b/string-art-demo/src/workers/stringArtWorker.ts @@ -1,23 +1,13 @@ -import init, { StringArtWasm, WasmStringArtConfig } from '../wasm/string_art_rust_impl.js'; +import type { StringArtConfig } from '../interfaces/stringArtConfig.js'; +import init, { ProgressInfo, StringArtWasm, WasmStringArtConfig } from '../wasm/string_art_rust_impl.js'; interface WorkerMessage { imageData: Uint8Array; - config: WasmStringArtConfig; -} - -interface ProgressInfo { - lines_completed: number; - total_lines: number; - current_nail: number; - next_nail: number; - score: number; - completion_percent: number; - path_segment: [number, number]; - current_path: number[]; + config: StringArtConfig; } self.onmessage = async (event: MessageEvent) => { - const { imageData, config } = event.data; + const { imageData, config } = event.data as { imageData: Uint8Array, config: StringArtConfig}; try { // Initialize the WASM module @@ -40,10 +30,10 @@ self.onmessage = async (event: MessageEvent) => { // Generate the string art path with progress updates const path = await generator.generate_path_streaming_with_frequency( - 2000, // max_lines - 50.0, // line_darkness - 15.0, // min_improvement_score - 300, // progress_frequency + config.max_lines, // max_lines + config.line_darkness, // line_darkness + config.min_improvement_score, // min_improvement_score + config.progress_frequency, // progress_frequency (progress: ProgressInfo) => { const { current_path } = progress; self.postMessage({ type: 'progress', data: { ...progress, current_path } });