import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Copy, RefreshCw, Download, Sliders, Palette, Code, Check } from 'lucide-react'; // --- MATH UTILS --- /** * Converts polar coordinates to cartesian */ const toCartesian = (radius, angle) => { const rad = (angle * Math.PI) / 180; return { x: radius * Math.cos(rad), y: radius * Math.sin(rad), }; }; /** * Catmull-Rom to Cubic Bezier conversion. * Calculates control points for a smooth curve passing through points. */ const getControlPoints = (p0, p1, p2, p3, tension = 0.5) => { const d1 = Math.sqrt(Math.pow(p1.x - p0.x, 2) + Math.pow(p1.y - p0.y, 2)); const d2 = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); const d3 = Math.sqrt(Math.pow(p3.x - p2.x, 2) + Math.pow(p3.y - p2.y, 2)); // Scaling factors can be used here for non-uniform knot vectors, // but for a blob generator, uniform or simple tension works well enough. // Using simplified derivative approach for uniform spacing approximation: const cp1 = { x: p1.x + (p2.x - p0.x) / 6 * tension, y: p1.y + (p2.y - p0.y) / 6 * tension, }; const cp2 = { x: p2.x - (p3.x - p1.x) / 6 * tension, y: p2.y - (p3.y - p1.y) / 6 * tension, }; return { cp1, cp2 }; }; /** * Generates the SVG Path string from a set of points using Spline interpolation */ const generateBlobPath = (points) => { if (points.length === 0) return ""; // Duplicate points to handle the closed loop smoothing // We need context of previous and next points for the start/end indices const len = points.length; const loopPoints = [ points[len - 1], ...points, points[0], points[1] ]; let path = `M ${points[0].x} ${points[0].y}`; // We iterate from the first actual point to the last actual point for (let i = 1; i <= len; i++) { const p0 = loopPoints[i - 1]; // Prev const p1 = loopPoints[i]; // Start const p2 = loopPoints[i + 1]; // End const p3 = loopPoints[i + 2]; // Next const { cp1, cp2 } = getControlPoints(p0, p1, p2, p3, 1); // Tension 1 for roundness path += ` C ${cp1.x.toFixed(2)} ${cp1.y.toFixed(2)}, ${cp2.x.toFixed(2)} ${cp2.y.toFixed(2)}, ${p2.x.toFixed(2)} ${p2.y.toFixed(2)}`; } return path + " Z"; }; const App = () => { // --- STATE --- const [complexity, setComplexity] = useState(8); // Number of points const [contrast, setContrast] = useState(6); // Randomness (Difference between min/max radius) const [size, setSize] = useState(300); // Canvas size const [fillColor, setFillColor] = useState('#6366f1'); const [gradient, setGradient] = useState(true); const [secondaryColor, setSecondaryColor] = useState('#a855f7'); const [blobPath, setBlobPath] = useState(''); const [seed, setSeed] = useState(Date.now()); const [copied, setCopied] = useState(false); // --- LOGIC --- const generateBlob = useCallback(() => { const numPoints = complexity; const minRadius = (size / 2) * 0.5; // Ensure blob doesn't get too small const maxVariation = (size / 2) * (contrast / 10) * 0.5; const points = []; const angleStep = 360 / numPoints; for (let i = 0; i < numPoints; i++) { const angle = (i * angleStep); // Add some random noise to the angle to make it less perfectly radial // const angleNoise = (Math.random() - 0.5) * (angleStep * 0.5); const randomRadius = minRadius + (Math.random() * maxVariation); const pos = toCartesian(randomRadius, angle); // Offset to center of viewBox points.push({ x: pos.x + size / 2, y: pos.y + size / 2 }); } setBlobPath(generateBlobPath(points)); }, [complexity, contrast, size, seed]); // Regenerate when params change useEffect(() => { generateBlob(); }, [generateBlob]); // --- HANDLERS --- const handleCopy = () => { const svgString = getSvgString(); navigator.clipboard.writeText(svgString); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const handleDownload = () => { const svgString = getSvgString(); const blob = new Blob([svgString], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `blob-${Date.now()}.svg`; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const getSvgString = () => { const gradId = "blobGradient"; const defs = gradient ? ` ` : ''; const fillAttr = gradient ? `url(#${gradId})` : fillColor; return ` ${defs} `; }; return (
{/* --- LEFT COLUMN: CONTROLS --- */}

Blob Generator

Create organic SVG shapes.

{/* Complexity Slider */}
{complexity} points
setComplexity(parseInt(e.target.value))} className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-indigo-600" />
{/* Contrast Slider */}
{contrast}
setContrast(parseInt(e.target.value))} className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-purple-600" />
{/* Color Pickers */}
setFillColor(e.target.value)} className="w-8 h-8 rounded cursor-pointer border-none bg-transparent" /> {fillColor}
{gradient && (
setSecondaryColor(e.target.value)} className="w-8 h-8 rounded cursor-pointer border-none bg-transparent" /> {secondaryColor}
)}
{/* Generate Button */}
{/* --- RIGHT COLUMN: PREVIEW --- */}
{/* Canvas Area */}
{/* Checkerboard background for transparency */}
{/* Export Actions */}
SVG Code
{/* Quick Code Preview (Collapsed) */}
                {getSvgString()}
             
); }; export default App;