Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions string-art-demo/src/App.css
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -89,6 +92,7 @@
}

.generate-button {
margin-top: 15px;
width: 100%;
padding: 1rem;
background: #28a745;
Expand Down
52 changes: 10 additions & 42 deletions string-art-demo/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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<Uint8Array | null>(null);
const [imageUrl, setImageUrl] = useState<string>('');
const [isGenerating, setIsGenerating] = useState(false);
const [currentPath, setCurrentPath] = useState<number[]>([]);
const [nailCoords, setNailCoords] = useState<Array<[number, number]>>([]);
const [progress, setProgress] = useState<ProgressInfo | null>(null);
const [config, setConfig] = useState<StringArtConfig>(presets.balanced());

const { generateStringArt, isLoading, error, settings } = useStringArt();
const handleImageSelected = useCallback((data: Uint8Array, url: string) => {
setImageData(data);
setImageUrl(url);
Expand All @@ -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([]);
Expand All @@ -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);
Expand All @@ -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 (
Expand Down Expand Up @@ -98,33 +92,8 @@ function App() {

{imageData && (
<div className="generation-controls">
<div className="config-section">
<h3>Quality Presets</h3>
<div className="preset-buttons">
<button
onClick={() => handlePresetChange('fast')}
className={`preset-button ${config.num_nails === 360 ? 'active' : ''}`}
disabled={isGenerating}
>
Fast (360 nails)
</button>
<button
onClick={() => handlePresetChange('balanced')}
className={`preset-button ${config.num_nails === 720 ? 'active' : ''}`}
disabled={isGenerating}
>
Balanced (720 nails)
</button>
<button
onClick={() => handlePresetChange('highQuality')}
className={`preset-button ${config.num_nails === 1440 ? 'active' : ''}`}
disabled={isGenerating}
>
High Quality (1440 nails)
</button>
</div>
</div>

<StringArtConfigSection key={"stringArt"} settings={settings} />

<button
onClick={handleStartGeneration}
disabled={isGenerating}
Expand All @@ -146,7 +115,6 @@ function App() {
></div>
</div>
<div className="progress-details">
<span>Current: Nail {progress.current_nail} → {progress.next_nail}</span>
<span>Score: {progress.score.toFixed(1)}</span>
</div>
</div>
Expand Down
74 changes: 42 additions & 32 deletions string-art-demo/src/components/StringArtCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,21 @@ export const StringArtCanvas: React.FC<StringArtCanvasProps> = ({
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;
Expand All @@ -44,38 +51,41 @@ export const StringArtCanvas: React.FC<StringArtCanvasProps> = ({
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 (
Expand Down Expand Up @@ -206,4 +216,4 @@ export const StringArtCanvas: React.FC<StringArtCanvasProps> = ({
`}</style>
</div>
);
};
};
Original file line number Diff line number Diff line change
@@ -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<StringArtConfig>;
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 (
<div key={index}>
<input
defaultValue={start}
type="range"
id="volume"
name="volume"
min={min}
max={max}
step={step}
onChange={({ target }) => {
(settingsRef.current[index] as unknown) = target.value;
setVal(target.value);
}}
/>
<label htmlFor="volume">{title} ({val})</label>
</div>
);
};

const StringArtConfigSection = ({
settings,
}: {
settings: RefObject<StringArtConfig>;
}) => {
if (!settings.current) {
return null;
}
return (
<section>
<h2>String Art Configuration</h2>
{(Object.keys(settings.current) as (keyof StringArtConfig)[]).map(
(key) => {
return (
<Slider
key={key}
index={key}
title={key.split("_").join(" ")}
settingsRef={settings}
/>
);
}
)}
</section>
);
};

export default StringArtConfigSection;
Empty file.
Loading
Loading