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 ? `
Create organic SVG shapes.
SVG Code
{getSvgString()}