Skip to content

Commit 2775c46

Browse files
zipping up, copyright, defaults
1 parent ca08912 commit 2775c46

1 file changed

Lines changed: 229 additions & 22 deletions

File tree

seeded_svg_splotch_generator_single_page_app.tsx

Lines changed: 229 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import {
2020
Droplets,
2121
Info,
2222
Dices,
23+
Package,
2324
} from "lucide-react";
25+
import JSZip from "jszip";
2426

2527
/**
2628
* Seeded SVG Splotch Generator
@@ -515,10 +517,13 @@ function simulate(params: Params) {
515517
// inside the huge workingW buffer.
516518
const physW = displayW;
517519

518-
// For spray geometry, offset the center opposite to spray direction
520+
// For spray and line geometry, offset the center opposite to spray direction
519521
// to compensate for the drift caused by spray magnitude
520522
let center = { x: workingW / 2, y: workingW / 2 };
521-
if (params.geometry === "spray" && params.sprayMagnitude > 0) {
523+
if (
524+
(params.geometry === "spray" || params.geometry === "line") &&
525+
params.sprayMagnitude > 0
526+
) {
522527
const ang = (params.sprayAngleDeg * Math.PI) / 180;
523528
// Offset opposite to spray direction, scaled by magnitude and physics size
524529
const offsetAmount = params.sprayMagnitude * physW * 0.35;
@@ -542,13 +547,21 @@ function simulate(params: Params) {
542547
}
543548

544549
if (params.geometry === "line") {
550+
const ang = (params.sprayAngleDeg * Math.PI) / 180;
551+
const dir = { x: Math.cos(ang), y: Math.sin(ang) };
552+
const perp = { x: -dir.y, y: dir.x };
553+
545554
const t = rng.range(-0.5, 0.5);
546-
const lineLen = physW * 0.22;
547-
const p = add(center, {
548-
x: t * lineLen,
549-
y: rng.normal() * (physW * 0.02),
550-
});
551-
const v = { x: speed * rng.range(0.6, 1.3), y: rng.normal() * 0.2 };
555+
// Magnitude controls line length (0 to 1.6 maps to 0 to ~0.35 of physW)
556+
const lineLen = physW * (0.05 + params.sprayMagnitude * 0.19);
557+
const alongOffset = t * lineLen;
558+
const perpOffset = rng.normal() * (physW * 0.02);
559+
const p = add(center, add(mul(dir, alongOffset), mul(perp, perpOffset)));
560+
561+
// Add directional velocity bias based on magnitude
562+
const drift = params.sprayMagnitude * 1.8;
563+
const baseSpeed = speed * rng.range(0.6, 1.3);
564+
const v = add(mul(dir, drift + baseSpeed), mul(perp, rng.normal() * 0.2));
552565
return { p, v, vz };
553566
}
554567

@@ -998,7 +1011,7 @@ const DEFAULT: Params = {
9981011
sprayMagnitude: 0.0,
9991012
sprayCovariance: 0.0,
10001013

1001-
strokes: 4,
1014+
strokes: 1,
10021015
flingPower: 22,
10031016
directionality: 0.78,
10041017
anisotropy: 4.6,
@@ -1231,7 +1244,16 @@ if (typeof window !== "undefined") {
12311244
}
12321245

12331246
export default function App() {
1234-
const [p, setP] = useState<Params>(DEFAULT);
1247+
const [p, setP] = useState<Params>(() => {
1248+
// Set magnitude to 1 for line geometry by default
1249+
const initial = { ...DEFAULT };
1250+
if (initial.geometry === "line") {
1251+
initial.sprayMagnitude = 1.0;
1252+
}
1253+
return initial;
1254+
});
1255+
const [variations, setVariations] = useState<number>(1);
1256+
const [isGeneratingVariations, setIsGeneratingVariations] = useState(false);
12351257
const dp = useDebounced(p, 250);
12361258

12371259
// Check if we're waiting for debounce (params changed but not yet processed)
@@ -1305,6 +1327,126 @@ export default function App() {
13051327
return `splotch_${safe}_${p.geometry}.svg`;
13061328
}, [p.seed, p.geometry]);
13071329

1330+
// Helper function to generate SVG text for a given seed offset
1331+
const generateSvgForSeed = (seedOffset: number): string => {
1332+
const modifiedParams: Params = {
1333+
...p,
1334+
seed: `${p.seed}-${seedOffset}`,
1335+
};
1336+
1337+
try {
1338+
const simResult = simulate(modifiedParams);
1339+
const size = p.svgSize;
1340+
const d = simResult.d;
1341+
1342+
const meta = {
1343+
seed: modifiedParams.seed,
1344+
geometry: modifiedParams.geometry,
1345+
packets: modifiedParams.packets,
1346+
fieldSize: modifiedParams.fieldSize,
1347+
threshold: modifiedParams.threshold,
1348+
smooth: modifiedParams.smooth,
1349+
blur: modifiedParams.blur,
1350+
viscosity: modifiedParams.viscosity,
1351+
restitution: modifiedParams.restitution,
1352+
drag: modifiedParams.drag,
1353+
impactSpread: modifiedParams.impactSpread,
1354+
smear: modifiedParams.smear,
1355+
noise: modifiedParams.noise,
1356+
baseRadius: modifiedParams.baseRadius,
1357+
radiusJitter: modifiedParams.radiusJitter,
1358+
sprayAngleDeg: modifiedParams.sprayAngleDeg,
1359+
sprayMagnitude: modifiedParams.sprayMagnitude,
1360+
sprayCovariance: modifiedParams.sprayCovariance,
1361+
strokes: modifiedParams.strokes,
1362+
flingPower: modifiedParams.flingPower,
1363+
directionality: modifiedParams.directionality,
1364+
anisotropy: modifiedParams.anisotropy,
1365+
tail: modifiedParams.tail,
1366+
tailDroplets: modifiedParams.tailDroplets,
1367+
panX: modifiedParams.panX,
1368+
panY: modifiedParams.panY,
1369+
userScale: modifiedParams.userScale,
1370+
};
1371+
1372+
const desc = `Generated by Seeded SVG Splotch Generator. Params: ${JSON.stringify(meta)}`;
1373+
1374+
const flip = p.invertY
1375+
? ` transform="translate(0 ${size}) scale(1 -1)"`
1376+
: "";
1377+
1378+
return (
1379+
`<?xml version="1.0" encoding="UTF-8"?>\n` +
1380+
`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">\n` +
1381+
` <desc>${desc.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</desc>\n` +
1382+
` <g${flip}>\n` +
1383+
` <path d="${d}" fill="#000" fill-rule="evenodd"/>\n` +
1384+
` </g>\n` +
1385+
`</svg>\n`
1386+
);
1387+
} catch {
1388+
return "";
1389+
}
1390+
};
1391+
1392+
// Generate filename for a variation
1393+
const getVariationFilename = (index: number): string => {
1394+
const safe =
1395+
p.seed
1396+
.trim()
1397+
.replace(/[^a-zA-Z0-9_-]+/g, "-")
1398+
.slice(0, 40) || "splotch";
1399+
return `splotch_${safe}_${p.geometry}_v${index + 1}.svg`;
1400+
};
1401+
1402+
// Generate and download variations as zip
1403+
const handleDownloadVariations = async () => {
1404+
if (variations < 1 || variations > 100) {
1405+
alert("Please enter a number between 1 and 100");
1406+
return;
1407+
}
1408+
1409+
setIsGeneratingVariations(true);
1410+
1411+
try {
1412+
const zip = new JSZip();
1413+
1414+
// Generate all variations (offset seed by 1, 2, 3, etc.)
1415+
for (let i = 0; i < variations; i++) {
1416+
const svgText = generateSvgForSeed(i + 1);
1417+
if (svgText) {
1418+
const filename = getVariationFilename(i);
1419+
zip.file(filename, svgText);
1420+
}
1421+
}
1422+
1423+
// Generate zip file
1424+
const zipBlob = await zip.generateAsync({ type: "blob" });
1425+
1426+
// Download zip
1427+
const safe =
1428+
p.seed
1429+
.trim()
1430+
.replace(/[^a-zA-Z0-9_-]+/g, "-")
1431+
.slice(0, 40) || "splotch";
1432+
const zipFilename = `splotch_${safe}_${p.geometry}_variations.zip`;
1433+
1434+
const url = URL.createObjectURL(zipBlob);
1435+
const a = document.createElement("a");
1436+
a.href = url;
1437+
a.download = zipFilename;
1438+
document.body.appendChild(a);
1439+
a.click();
1440+
a.remove();
1441+
URL.revokeObjectURL(url);
1442+
} catch (error) {
1443+
console.error("Error generating variations:", error);
1444+
alert("Error generating variations. Please try again.");
1445+
} finally {
1446+
setIsGeneratingVariations(false);
1447+
}
1448+
};
1449+
13081450
return (
13091451
<div className="min-h-screen bg-gradient-to-b from-zinc-50 to-white">
13101452
<div className="max-w-6xl mx-auto px-4 py-6">
@@ -1404,11 +1546,16 @@ export default function App() {
14041546
geometry: g,
14051547
packets: Math.min(Math.max(p.packets, 800), 1200),
14061548
fieldSize: Math.min(p.fieldSize, 220),
1407-
strokes: Math.min(Math.max(p.strokes, 3), 6),
1549+
strokes: 1,
14081550
});
14091551
return;
14101552
}
1411-
setP({ ...p, geometry: g });
1553+
setP({
1554+
...p,
1555+
geometry: g,
1556+
// Set magnitude to 1 for line geometry by default
1557+
sprayMagnitude: g === "line" ? 1.0 : p.sprayMagnitude,
1558+
});
14121559
}}
14131560
>
14141561
<SelectTrigger className="rounded-xl">
@@ -1464,11 +1611,11 @@ export default function App() {
14641611

14651612
<div className="pt-2 border-t" />
14661613

1467-
{p.geometry === "spray" && (
1614+
{(p.geometry === "spray" || p.geometry === "line") && (
14681615
<>
14691616
<Row
1470-
label={`Spray direction (${Math.round(p.sprayAngleDeg)}°)`}
1471-
help="Direction of the spray bias (adds a dominant direction without expensive fling simulation)."
1617+
label={`Direction (${Math.round(p.sprayAngleDeg)}°)`}
1618+
help="Direction of the bias (adds a dominant direction without expensive fling simulation)."
14721619
>
14731620
<Slider
14741621
value={[p.sprayAngleDeg]}
@@ -1482,8 +1629,12 @@ export default function App() {
14821629
</Row>
14831630

14841631
<Row
1485-
label={`Spray magnitude (${p.sprayMagnitude.toFixed(2)})`}
1486-
help="Strength of the directional drift and mean shift. Higher = more fling-like throw."
1632+
label={`Magnitude (${p.sprayMagnitude.toFixed(2)})`}
1633+
help={
1634+
p.geometry === "line"
1635+
? "Controls the length of the line. Higher = longer line."
1636+
: "Strength of the directional drift and mean shift. Higher = more fling-like throw."
1637+
}
14871638
>
14881639
<Slider
14891640
value={[p.sprayMagnitude]}
@@ -1495,7 +1646,11 @@ export default function App() {
14951646
}
14961647
/>
14971648
</Row>
1649+
</>
1650+
)}
14981651

1652+
{p.geometry === "spray" && (
1653+
<>
14991654
<Row
15001655
label={`Spray covariance (${p.sprayCovariance.toFixed(2)})`}
15011656
help="Anisotropy of the spray cloud. Higher = stretched distribution along the spray direction."
@@ -1702,9 +1857,10 @@ export default function App() {
17021857
Reset all
17031858
</Button>
17041859
<div className="text-xs text-muted-foreground leading-relaxed">
1705-
Tips: If you get &quot;no contour&quot;, lower <b>Threshold</b> or
1706-
increase <b>Packets</b>/<b>Base radius</b>. Higher{" "}
1707-
<b>Field res</b> yields more detail.
1860+
Tips: If you get &quot;no contour&quot;, lower{" "}
1861+
<b>Threshold</b> or increase <b>Packets</b>/
1862+
<b>Base radius</b>. Higher <b>Field res</b> yields more
1863+
detail.
17081864
</div>
17091865
</div>
17101866
</CardContent>
@@ -1767,8 +1923,8 @@ export default function App() {
17671923
</Row>
17681924

17691925
<div className="text-xs text-muted-foreground">
1770-
Manual placement is always on. It&apos;s OK if the blob goes out of
1771-
frame — adjust Pan/Scale to compose it.
1926+
Manual placement is always on. It&apos;s OK if the blob goes
1927+
out of frame — adjust Pan/Scale to compose it.
17721928
</div>
17731929
</CardContent>
17741930
</Card>
@@ -1876,6 +2032,43 @@ export default function App() {
18762032
</pre>
18772033
</details>
18782034

2035+
<div className="flex items-center gap-3 pt-2 border-t">
2036+
<div className="flex-1">
2037+
<Label htmlFor="variations" className="text-xs">
2038+
Number of variations
2039+
</Label>
2040+
<Input
2041+
id="variations"
2042+
type="number"
2043+
min="1"
2044+
max="100"
2045+
value={variations}
2046+
onChange={(e) => {
2047+
const val = parseInt(e.target.value, 10);
2048+
if (!isNaN(val) && val >= 1 && val <= 100) {
2049+
setVariations(val);
2050+
}
2051+
}}
2052+
className="mt-1"
2053+
disabled={isGeneratingVariations}
2054+
/>
2055+
</div>
2056+
<div className="flex items-end">
2057+
<Button
2058+
onClick={handleDownloadVariations}
2059+
className="rounded-2xl"
2060+
disabled={
2061+
!result.d || isGeneratingVariations || variations < 1
2062+
}
2063+
>
2064+
<Package className="h-4 w-4 mr-2" />
2065+
{isGeneratingVariations
2066+
? "Generating..."
2067+
: "Download ZIP"}
2068+
</Button>
2069+
</div>
2070+
</div>
2071+
18792072
<div className="text-xs text-muted-foreground">
18802073
The export is a single <code>&lt;path&gt;</code> using{" "}
18812074
<code>fill-rule=&quot;evenodd&quot;</code>.
@@ -1897,6 +2090,20 @@ export default function App() {
18972090
Changing any slider changes output deterministically.
18982091
</motion.div>
18992092
</AnimatePresence>
2093+
2094+
<footer className="mt-12 pt-6 border-t text-center text-xs text-muted-foreground space-y-1">
2095+
<div>© 2026 Mind the Math LLC</div>
2096+
<div>
2097+
<a
2098+
href="https://github.com/mindthemath/splotch"
2099+
target="_blank"
2100+
rel="noopener noreferrer"
2101+
className="hover:text-foreground transition-colors underline"
2102+
>
2103+
github.com/mindthemath/splotch
2104+
</a>
2105+
</div>
2106+
</footer>
19002107
</div>
19012108
</div>
19022109
);

0 commit comments

Comments
 (0)