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 */} +
+

Dimensions

+
+
+ Width + setWidth(Number(e.target.value))} + className="flex-1 accent-indigo-500 h-1.5 bg-zinc-200 dark:bg-zinc-800 rounded-lg cursor-pointer" + /> + {width}px +
+
+ Height + setHeight(Number(e.target.value))} + className="flex-1 accent-indigo-500 h-1.5 bg-zinc-200 dark:bg-zinc-800 rounded-lg cursor-pointer" + /> + {height}px +
+
+
+
+ + {/* 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}
+                
+ +
+
+
+
+
+
+ ); +}