Skip to content

Commit be7381a

Browse files
Merge pull request #8 from FireAndIceFrog/feature/add-custom-controls
Feature/add custom controls
2 parents 2995cfe + ee9d394 commit be7381a

File tree

10 files changed

+300
-250
lines changed

10 files changed

+300
-250
lines changed

string-art-demo/src/App.css

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
.app {
22
min-height: 100vh;
3-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
4-
padding: 1rem;
3+
width: 100vw;
4+
background: linear-gradient(0deg, #8f91ff 0%, #7476ff 100%);
55
}
66

77
.app-header {
8+
padding-top: 1rem;
89
text-align: center;
910
color: white;
1011
margin-bottom: 2rem;
@@ -47,6 +48,8 @@
4748
background: white;
4849
border-radius: 8px;
4950
padding: 1.5rem;
51+
display: flex;
52+
flex-direction: column;
5053
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
5154
}
5255

@@ -89,6 +92,7 @@
8992
}
9093

9194
.generate-button {
95+
margin-top: 15px;
9296
width: 100%;
9397
padding: 1rem;
9498
background: #28a745;

string-art-demo/src/App.tsx

Lines changed: 10 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
import { useState, useCallback } from 'react';
22
import { ImageUploader } from './components/ImageUploader';
33
import { StringArtCanvas } from './components/StringArtCanvas';
4-
import { useStringArt, type StringArtConfig, type ProgressInfo } from './hooks/useStringArt';
54
import './App.css';
6-
5+
import StringArtConfigSection from './components/StringArtConfig/StringArtConfigSection';
6+
import { useStringArt, type ProgressInfo } from './useStringArt';
77
function App() {
8-
const { wasmModule, isLoading, error, generateStringArt, presets } = useStringArt();
98
const [imageData, setImageData] = useState<Uint8Array | null>(null);
109
const [imageUrl, setImageUrl] = useState<string>('');
1110
const [isGenerating, setIsGenerating] = useState(false);
1211
const [currentPath, setCurrentPath] = useState<number[]>([]);
1312
const [nailCoords, setNailCoords] = useState<Array<[number, number]>>([]);
1413
const [progress, setProgress] = useState<ProgressInfo | null>(null);
15-
const [config, setConfig] = useState<StringArtConfig>(presets.balanced());
16-
14+
const { generateStringArt, isLoading, error, settings } = useStringArt();
1715
const handleImageSelected = useCallback((data: Uint8Array, url: string) => {
1816
setImageData(data);
1917
setImageUrl(url);
@@ -22,8 +20,8 @@ function App() {
2220
setNailCoords([]); // Clear until we generate the string art
2321
}, []);
2422

25-
const handleStartGeneration = useCallback(async () => {
26-
if (!imageData || !wasmModule) return;
23+
const handleStartGeneration = async () => {
24+
if (!imageData) return;
2725

2826
setIsGenerating(true);
2927
setCurrentPath([]);
@@ -41,9 +39,9 @@ function App() {
4139
setNailCoords(coords);
4240
};
4341

44-
const result = await generateStringArt(imageData, config, onProgress, onNailCoords);
42+
const result = await generateStringArt(imageData, onProgress, onNailCoords);
4543
if (result.path) {
46-
setCurrentPath(result.path);
44+
setCurrentPath((prevPath) => [...prevPath, ...(result.path || [])]);
4745
}
4846
if (result.nailCoords.length > 0) {
4947
setNailCoords(result.nailCoords);
@@ -54,11 +52,7 @@ function App() {
5452
} finally {
5553
setIsGenerating(false);
5654
}
57-
}, [imageData, wasmModule, config, generateStringArt]);
58-
59-
const handlePresetChange = useCallback((presetName: 'fast' | 'balanced' | 'highQuality') => {
60-
setConfig(presets[presetName]());
61-
}, [presets]);
55+
};
6256

6357
if (isLoading) {
6458
return (
@@ -98,33 +92,8 @@ function App() {
9892

9993
{imageData && (
10094
<div className="generation-controls">
101-
<div className="config-section">
102-
<h3>Quality Presets</h3>
103-
<div className="preset-buttons">
104-
<button
105-
onClick={() => handlePresetChange('fast')}
106-
className={`preset-button ${config.num_nails === 360 ? 'active' : ''}`}
107-
disabled={isGenerating}
108-
>
109-
Fast (360 nails)
110-
</button>
111-
<button
112-
onClick={() => handlePresetChange('balanced')}
113-
className={`preset-button ${config.num_nails === 720 ? 'active' : ''}`}
114-
disabled={isGenerating}
115-
>
116-
Balanced (720 nails)
117-
</button>
118-
<button
119-
onClick={() => handlePresetChange('highQuality')}
120-
className={`preset-button ${config.num_nails === 1440 ? 'active' : ''}`}
121-
disabled={isGenerating}
122-
>
123-
High Quality (1440 nails)
124-
</button>
125-
</div>
126-
</div>
127-
95+
<StringArtConfigSection key={"stringArt"} settings={settings} />
96+
12897
<button
12998
onClick={handleStartGeneration}
13099
disabled={isGenerating}
@@ -146,7 +115,6 @@ function App() {
146115
></div>
147116
</div>
148117
<div className="progress-details">
149-
<span>Current: Nail {progress.current_nail}{progress.next_nail}</span>
150118
<span>Score: {progress.score.toFixed(1)}</span>
151119
</div>
152120
</div>

string-art-demo/src/components/StringArtCanvas.tsx

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,21 @@ export const StringArtCanvas: React.FC<StringArtCanvasProps> = ({
2727
const ctx = canvas.getContext('2d');
2828
if (!ctx) return;
2929

30-
// Clear canvas
30+
// Always clear the canvas at the start of a render
3131
ctx.fillStyle = 'white';
32-
ctx.fillRect(0, 0, width, height);
32+
ctx.clearRect(0, 0, width, height);
33+
34+
let isCancelled = false;
3335

3436
if (showOriginal && imageUrl) {
35-
// Show original image
3637
const img = new Image();
3738
img.onload = () => {
39+
if (isCancelled) return;
40+
41+
// Clear canvas again right before drawing to prevent race conditions
42+
ctx.fillStyle = 'white';
43+
ctx.fillRect(0, 0, width, height);
44+
3845
const scale = Math.min(width / img.width, height / img.height);
3946
const scaledWidth = img.width * scale;
4047
const scaledHeight = img.height * scale;
@@ -44,38 +51,41 @@ export const StringArtCanvas: React.FC<StringArtCanvasProps> = ({
4451
ctx.drawImage(img, x, y, scaledWidth, scaledHeight);
4552
};
4653
img.src = imageUrl;
47-
return;
48-
}
49-
50-
// Draw nails as small circles
51-
ctx.fillStyle = '#666';
52-
nailCoords.forEach(([x, y]) => {
53-
ctx.beginPath();
54-
ctx.arc(x, y, 2, 0, 2 * Math.PI);
55-
ctx.fill();
56-
});
57-
58-
// Draw string path
59-
if (currentPath.length > 1) {
60-
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
61-
ctx.lineWidth = 0.5;
62-
ctx.globalCompositeOperation = 'multiply';
63-
64-
for (let i = 0; i < currentPath.length - 1; i++) {
65-
const fromNail = currentPath[i];
66-
const toNail = currentPath[i + 1];
67-
68-
if (fromNail < nailCoords.length && toNail < nailCoords.length) {
69-
const [x1, y1] = nailCoords[fromNail];
70-
const [x2, y2] = nailCoords[toNail];
54+
} else {
55+
// Draw nails as small circles
56+
ctx.fillStyle = '#666';
57+
nailCoords.forEach(([x, y]) => {
58+
ctx.beginPath();
59+
ctx.arc(x, y, 2, 0, 2 * Math.PI);
60+
ctx.fill();
61+
});
62+
63+
// Draw string path
64+
if (currentPath.length > 1) {
65+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
66+
ctx.lineWidth = 0.5;
67+
ctx.globalCompositeOperation = 'multiply';
68+
69+
for (let i = 0; i < currentPath.length - 1; i++) {
70+
const fromNail = currentPath[i];
71+
const toNail = currentPath[i + 1];
7172

72-
ctx.beginPath();
73-
ctx.moveTo(x1, y1);
74-
ctx.lineTo(x2, y2);
75-
ctx.stroke();
73+
if (fromNail < nailCoords.length && toNail < nailCoords.length) {
74+
const [x1, y1] = nailCoords[fromNail];
75+
const [x2, y2] = nailCoords[toNail];
76+
77+
ctx.beginPath();
78+
ctx.moveTo(x1, y1);
79+
ctx.lineTo(x2, y2);
80+
ctx.stroke();
81+
}
7682
}
7783
}
7884
}
85+
86+
return () => {
87+
isCancelled = true;
88+
};
7989
}, [width, height, nailCoords, currentPath, showOriginal, imageUrl]);
8090

8191
return (
@@ -206,4 +216,4 @@ export const StringArtCanvas: React.FC<StringArtCanvasProps> = ({
206216
`}</style>
207217
</div>
208218
);
209-
};
219+
};
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { useEffect, useState, type RefObject } from "react";
2+
import { type StringArtConfig } from "../../useStringArt";
3+
4+
const Slider = ({
5+
title,
6+
settingsRef,
7+
index,
8+
}: {
9+
title: string;
10+
settingsRef: RefObject<StringArtConfig>;
11+
index: keyof StringArtConfig;
12+
}) => {
13+
const minMaxVals: Record<
14+
keyof Omit<
15+
StringArtConfig,
16+
| "extract_subject"
17+
| "remove_shadows"
18+
| "preserve_eyes"
19+
| "preserve_negative_space"
20+
>,
21+
[number, number, number, number]
22+
> = {
23+
//min, max, start
24+
image_size: [500, 2000, 500, 100],
25+
line_darkness: [25, 300, 100, 5],
26+
max_lines: [800, 5000, 1000, 100],
27+
min_improvement_score: [0, 100, 15, 1],
28+
negative_space_penalty: [0, 100, 0, 1],
29+
negative_space_threshold: [0, 100, 0, 1],
30+
num_nails: [360, 1440, 360, 360/4],
31+
progress_frequency: [200, 500, 200, 50],
32+
};
33+
34+
const [val, setVal] = useState("0");
35+
const dontRender = !index || !Object.keys(minMaxVals).includes(index as string)
36+
const [min, max, start, step] = dontRender? [0,0,0,0] : minMaxVals[index as keyof typeof minMaxVals];
37+
38+
useEffect(() => {
39+
setVal(String(start))
40+
}, [start])
41+
42+
if (dontRender) return null;
43+
if (!settingsRef?.current) return null;
44+
return (
45+
<div key={index}>
46+
<input
47+
defaultValue={start}
48+
type="range"
49+
id="volume"
50+
name="volume"
51+
min={min}
52+
max={max}
53+
step={step}
54+
onChange={({ target }) => {
55+
(settingsRef.current[index] as unknown) = target.value;
56+
setVal(target.value);
57+
}}
58+
/>
59+
<label htmlFor="volume">{title} ({val})</label>
60+
</div>
61+
);
62+
};
63+
64+
const StringArtConfigSection = ({
65+
settings,
66+
}: {
67+
settings: RefObject<StringArtConfig>;
68+
}) => {
69+
if (!settings.current) {
70+
return null;
71+
}
72+
return (
73+
<section>
74+
<h2>String Art Configuration</h2>
75+
{(Object.keys(settings.current) as (keyof StringArtConfig)[]).map(
76+
(key) => {
77+
return (
78+
<Slider
79+
key={key}
80+
index={key}
81+
title={key.split("_").join(" ")}
82+
settingsRef={settings}
83+
/>
84+
);
85+
}
86+
)}
87+
</section>
88+
);
89+
};
90+
91+
export default StringArtConfigSection;

string-art-demo/src/components/StringArtConfig/index.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)