diff --git a/src/App.jsx b/src/App.jsx
index 6dfe075..b50e7e3 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -34,6 +34,7 @@ import ListResources from "./pages/ResourceHub/resourcehub/ListResources";
import Base64Url from "./pages/DevUtilities/devutilities/Base64Url";
import BcryptGenerator from "./pages/DevUtilities/devutilities/BcryptGenerator";
import BorderImageGenerator from "./pages/DevUtilities/devutilities/BorderImageGenerator";
+import FancyBorderRadiusGenerator from "./pages/DevUtilities/devutilities/FancyBorderRadiusGenerator";
import ChmodCalculator from "./pages/DevUtilities/devutilities/ChmodCalculator";
import ClipPathMaker from "./pages/DevUtilities/devutilities/ClipPathMaker";
import CodeSandbox from "./pages/DevUtilities/devutilities/CodeSandbox";
@@ -529,6 +530,10 @@ function AppInner({ toggleHUD, hudVisible }) {
path="/devutilities/border-image"
element={}
/>
+ }
+ />
}
diff --git a/src/config/sidebarSections.js b/src/config/sidebarSections.js
index 43df62e..4e64ed7 100644
--- a/src/config/sidebarSections.js
+++ b/src/config/sidebarSections.js
@@ -295,6 +295,12 @@ const SIDEBAR_SECTIONS = [
"Design custom sliced image borders and generate copy-ready CSS properties visually.",
path: "/devutilities/border-image",
},
+ {
+ label: "CSS Custom Blob & Fancy Border Radius",
+ description:
+ "Design organic blob shapes using visual handles and advanced 8-point border-radius.",
+ path: "/devutilities/fancy-border-radius",
+ },
{
label: "Glassmorphism Playground",
description: "Design glassmorphism effects with shadows and blur",
diff --git a/src/pages/DevUtilities/DevUtilities.jsx b/src/pages/DevUtilities/DevUtilities.jsx
index 580ec6c..91616f0 100644
--- a/src/pages/DevUtilities/DevUtilities.jsx
+++ b/src/pages/DevUtilities/DevUtilities.jsx
@@ -1091,6 +1091,27 @@ const DevUtilities = () => {
),
},
+ {
+ title: "CSS Custom Blob & Fancy Border Radius",
+ description:
+ "Design organic blob shapes using visual handles and advanced 8-point border-radius.",
+ path: "/devutilities/fancy-border-radius",
+ icon: (
+
+ ),
+ },
{
title: "Password Generator",
description:
diff --git a/src/pages/DevUtilities/devutilities/FancyBorderRadiusGenerator.jsx b/src/pages/DevUtilities/devutilities/FancyBorderRadiusGenerator.jsx
new file mode 100644
index 0000000..5bc8207
--- /dev/null
+++ b/src/pages/DevUtilities/devutilities/FancyBorderRadiusGenerator.jsx
@@ -0,0 +1,424 @@
+import { useCallback, useEffect, useId, useRef, useState } from "react";
+import { Link } from "react-router-dom";
+import { toast } from "sonner";
+import {
+ FaArrowLeft,
+ FaCopy,
+ FaPlay,
+ FaStop,
+ FaSyncAlt,
+ FaUndo,
+} from "react-icons/fa";
+import { useTheme } from "../../../context/ThemeContext";
+
+// Corner order used by the CSS border-radius shorthand: TL, TR, BR, BL
+const CORNER_KEYS = ["tl", "tr", "br", "bl"];
+
+const DEFAULT_RADII = {
+ tlH: 30, trH: 70, brH: 70, blH: 30,
+ tlV: 30, trV: 30, brV: 70, blV: 70,
+};
+
+const PRESETS = [
+ { name: "Perfect Circle", radii: { tlH: 50, trH: 50, brH: 50, blH: 50, tlV: 50, trV: 50, brV: 50, blV: 50 } },
+ { name: "Organic Blob", radii: { tlH: 30, trH: 70, brH: 70, blH: 30, tlV: 30, trV: 30, brV: 70, blV: 70 } },
+ { name: "Liquid Button", radii: { tlH: 60, trH: 40, brH: 30, blH: 70, tlV: 60, trV: 30, brV: 70, blV: 40 } },
+ { name: "Egg", radii: { tlH: 50, trH: 50, brH: 50, blH: 50, tlV: 60, trV: 60, brV: 40, blV: 40 } },
+ { name: "Speech Bubble", radii: { tlH: 35, trH: 35, brH: 35, blH: 5, tlV: 35, trV: 35, brV: 10, blV: 35 } },
+];
+
+const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
+const round1 = (n) => Math.round(n * 10) / 10;
+
+const radiiToCss = (r) =>
+ `${round1(r.tlH)}% ${round1(r.trH)}% ${round1(r.brH)}% ${round1(r.blH)}% / ${round1(r.tlV)}% ${round1(r.trV)}% ${round1(r.brV)}% ${round1(r.blV)}%`;
+
+// Cyclically rotates the 8 values so the morph target still looks organic
+// and always stays within the same 0-100 range as the source shape.
+const rotateRadii = (r) => ({
+ tlH: r.trH, trH: r.brH, brH: r.blH, blH: r.tlH,
+ tlV: r.trV, trV: r.brV, brV: r.blV, blV: r.tlV,
+});
+
+export default function FancyBorderRadiusGenerator() {
+ const { dark } = useTheme();
+ const containerRef = useRef(null);
+ const draggingHandle = useRef(null);
+ const rawId = useId().replace(/[^a-zA-Z0-9]/g, "");
+
+ const [radii, setRadii] = useState({ ...DEFAULT_RADII });
+ const [activePreset, setActivePreset] = useState("Organic Blob");
+ const [width, setWidth] = useState(280);
+ const [height, setHeight] = useState(280);
+ const [morphing, setMorphing] = useState(false);
+ const [duration, setDuration] = useState(4);
+ const [tailwindMode, setTailwindMode] = useState(false);
+
+ const theme = {
+ light: {
+ wrapper: "bg-[#F8F9FA] text-zinc-900",
+ heading: "text-zinc-900",
+ subtext: "text-zinc-500",
+ card: "bg-white border-zinc-200/85 shadow-sm",
+ input:
+ "bg-zinc-50 border-zinc-200 text-zinc-900 focus:border-zinc-400 focus:outline-none",
+ button: "bg-zinc-900 text-white hover:bg-zinc-800 transition-all duration-200 shadow-sm",
+ secondaryBtn:
+ "bg-white text-zinc-800 border-zinc-200 hover:bg-zinc-50 transition-all duration-200",
+ activeBtn: "bg-zinc-900 text-white border-zinc-900",
+ backLink:
+ "bg-white border-neutral-200 text-neutral-600 hover:text-black hover:border-neutral-350",
+ codeBox: "bg-zinc-900 text-zinc-100 border-zinc-800",
+ subtle: "border-zinc-100",
+ canvasBorder: "border-zinc-200",
+ blob: "bg-gradient-to-br from-zinc-800 to-zinc-500",
+ },
+ dark: {
+ wrapper: "bg-[#090A0F] text-zinc-100",
+ heading: "text-zinc-100",
+ subtext: "text-zinc-500",
+ card: "bg-zinc-900/50 border-zinc-800/85 backdrop-blur-md shadow-md",
+ input:
+ "bg-zinc-900 border-zinc-700 text-zinc-100 focus:border-zinc-500 focus:outline-none",
+ button: "bg-white text-zinc-900 hover:bg-zinc-100 transition-all duration-200 shadow-sm",
+ secondaryBtn:
+ "bg-zinc-800/50 text-zinc-300 border-zinc-700 hover:bg-zinc-700/50 transition-all duration-200",
+ activeBtn: "bg-white text-zinc-900 border-white",
+ backLink:
+ "bg-zinc-800/80 border-zinc-700 text-zinc-300 hover:text-white hover:border-zinc-600",
+ codeBox: "bg-black/40 text-emerald-400 border-zinc-800/80 font-mono",
+ subtle: "border-zinc-800/60",
+ canvasBorder: "border-zinc-800",
+ blob: "bg-gradient-to-br from-zinc-200 to-zinc-500",
+ },
+ };
+ const t = dark ? theme.dark : theme.light;
+
+ const primaryRadius = radiiToCss(radii);
+ const secondaryRadii = rotateRadii(radii);
+ const secondaryRadius = radiiToCss(secondaryRadii);
+
+ const cssOutput = `.fancy-blob {\n width: ${width}px;\n height: ${height}px;\n border-radius: ${primaryRadius};\n}`;
+ const tailwindOutput = `w-[${width}px] h-[${height}px] rounded-[${primaryRadius.replace(/\s+/g, "_")}]`;
+
+ const keyframeName = `blobMorph${rawId}`;
+
+ const getRelativePoint = useCallback((clientX, clientY) => {
+ const rect = containerRef.current.getBoundingClientRect();
+ const x = clamp(((clientX - rect.left) / rect.width) * 100, 0, 100);
+ const y = clamp(((clientY - rect.top) / rect.height) * 100, 0, 100);
+ return [x, y];
+ }, []);
+
+ const handlePointerDown = (handleKey) => (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ draggingHandle.current = handleKey;
+ };
+
+ useEffect(() => {
+ const handleMove = (e) => {
+ const handle = draggingHandle.current;
+ if (!handle || !containerRef.current) return;
+ const [x, y] = getRelativePoint(e.clientX, e.clientY);
+
+ setRadii((prev) => {
+ const next = { ...prev };
+ switch (handle) {
+ case "tlH":
+ next.tlH = clamp(round1(x), 0, 100);
+ break;
+ case "trH":
+ next.trH = clamp(round1(100 - x), 0, 100);
+ break;
+ case "brH":
+ next.brH = clamp(round1(100 - x), 0, 100);
+ break;
+ case "blH":
+ next.blH = clamp(round1(x), 0, 100);
+ break;
+ case "tlV":
+ next.tlV = clamp(round1(y), 0, 100);
+ break;
+ case "trV":
+ next.trV = clamp(round1(y), 0, 100);
+ break;
+ case "brV":
+ next.brV = clamp(round1(100 - y), 0, 100);
+ break;
+ case "blV":
+ next.blV = clamp(round1(100 - y), 0, 100);
+ break;
+ default:
+ break;
+ }
+ return next;
+ });
+ setActivePreset(null);
+ };
+ const handleUp = () => {
+ draggingHandle.current = null;
+ };
+ window.addEventListener("pointermove", handleMove);
+ window.addEventListener("pointerup", handleUp);
+ return () => {
+ window.removeEventListener("pointermove", handleMove);
+ window.removeEventListener("pointerup", handleUp);
+ };
+ }, [getRelativePoint]);
+
+ const applyPreset = (preset) => {
+ setRadii({ ...preset.radii });
+ setActivePreset(preset.name);
+ toast.success(`Applied ${preset.name} preset`);
+ };
+
+ const resetShape = () => {
+ setRadii({ ...DEFAULT_RADII });
+ setActivePreset("Organic Blob");
+ setMorphing(false);
+ toast.success("Shape reset");
+ };
+
+ const copyCss = () => {
+ navigator.clipboard.writeText(tailwindMode ? tailwindOutput : cssOutput);
+ toast.success(tailwindMode ? "Copied Tailwind classes!" : "Copied CSS to clipboard!");
+ };
+
+ // Handle anchor positions, expressed as {x%, y%} on the preview box.
+ const handlePositions = {
+ tlH: { x: radii.tlH, y: 0 },
+ trH: { x: 100 - radii.trH, y: 0 },
+ brH: { x: 100 - radii.brH, y: 100 },
+ blH: { x: radii.blH, y: 100 },
+ tlV: { x: 0, y: radii.tlV },
+ trV: { x: 100, y: radii.trV },
+ brV: { x: 100, y: 100 - radii.brV },
+ blV: { x: 0, y: 100 - radii.blV },
+ };
+
+ const handleLabels = {
+ tlH: "Top-left horizontal",
+ trH: "Top-right horizontal",
+ brH: "Bottom-right horizontal",
+ blH: "Bottom-left horizontal",
+ tlV: "Top-left vertical",
+ trV: "Top-right vertical",
+ brV: "Bottom-right vertical",
+ blV: "Bottom-left vertical",
+ };
+
+ return (
+
+
CSS Custom Blob & Fancy Border Radius Generator — DevTasks
+
+
+ {morphing && (
+
+ )}
+
+
+ {/* Header */}
+
+
+
+
+
+
+ CSS Custom Blob & Fancy Border Radius Generator
+
+
+ Drag the 8 handles to morph the shape, layer in animation, and export ready-to-use CSS. Fully offline.
+
+
+
+
+
+ {/* Left: Preview canvas */}
+
+
+
+
Preview
+
+
+
+
+
+
+ {/* Draggable handles, positioned relative to the container box */}
+
+
+ {CORNER_KEYS.flatMap((corner) => [`${corner}H`, `${corner}V`]).map((key) => {
+ const pos = handlePositions[key];
+ return (
+
+ );
+ })}
+
+
+
+
+
+ Each corner has a horizontal and a vertical handle — drag them along the box edges to shape the blob.
+
+
+
+ {/* Dimensions */}
+
+
+
+ {/* Right: Presets, animation, output */}
+
+ {/* Presets */}
+
+
Shape Presets
+
+ {PRESETS.map((preset) => (
+
+ ))}
+
+
+
+ {/* Animation */}
+
+
+
Morph Animation
+
+
+
+ Speed
+ setDuration(Number(e.target.value))}
+ className="flex-1 accent-indigo-500 h-1.5 bg-zinc-200 dark:bg-zinc-800 rounded-lg cursor-pointer"
+ />
+ {duration}s
+
+
+ Morphs between the current shape and an auto-generated offset for a fluid, liquid motion.
+
+
+
+ {/* CSS Output */}
+
+
+
CSS Output
+
+
+
+
+ {tailwindMode ? tailwindOutput : cssOutput}
+
+
+
+
+
+
+
+
+ );
+}