Files
mintel.me/apps/web/src/components/PenCircle.tsx

125 lines
3.4 KiB
TypeScript

"use client";
import React, { useId, useMemo } from "react";
import { motion } from "framer-motion";
import { cn } from "../utils/cn";
interface PenCircleProps {
children: React.ReactNode;
delay?: number;
className?: string;
color?: string;
}
/**
* Ballpoint Pen Circle Component
*
* Draws a hand-drawn ellipse around children using SVG.
* Key: uses preserveAspectRatio="none" so the ellipse stretches
* to match the element dimensions regardless of text length.
*/
function createRng(seed: string) {
let s = 0;
for (let i = 0; i < seed.length; i++) {
s = ((s << 5) - s + seed.charCodeAt(i)) | 0;
}
return () => {
s = (s * 16807 + 0) % 2147483647;
return (s & 0x7fffffff) / 0x7fffffff;
};
}
function generateCirclePath(rng: () => number) {
// We draw an ellipse in a 0-100 x 0-100 viewBox.
// preserveAspectRatio="none" will stretch it to fit the element.
const cx = 50;
const cy = 50;
// Radii in viewBox units — will be stretched by the element
const rx = 50;
const ry = 50;
// Wobble for organic feel
const w = () => (rng() - 0.5) * 6;
const k = 0.5522847;
const kx = rx * k;
const ky = ry * k;
// 4 cardinal points with wobble
const top = { x: cx + w(), y: cy - ry + w() * 0.5 };
const right = { x: cx + rx + w() * 0.3, y: cy + w() };
const bottom = { x: cx + w(), y: cy + ry + w() * 0.5 };
const left = { x: cx - rx + w() * 0.3, y: cy + w() };
// End slightly offset from start for imperfect closure
const endOffX = (rng() - 0.5) * 8;
const endOffY = (rng() - 0.5) * 4;
return [
`M ${top.x},${top.y}`,
`C ${top.x + kx + w()},${top.y + w()} ${right.x + w()},${right.y - ky + w()} ${right.x},${right.y}`,
`C ${right.x + w()},${right.y + ky + w()} ${bottom.x + kx + w()},${bottom.y + w()} ${bottom.x},${bottom.y}`,
`C ${bottom.x - kx + w()},${bottom.y + w()} ${left.x + w()},${left.y + ky + w()} ${left.x},${left.y}`,
`C ${left.x + w()},${left.y - ky + w()} ${top.x - kx + w()},${top.y + w()} ${top.x + endOffX},${top.y + endOffY}`,
].join(" ");
}
export const PenCircle: React.FC<PenCircleProps> = ({
children,
delay = 0,
className = "",
color = "rgba(37, 99, 235, 0.65)", // Blue ballpoint pen
}) => {
const id = useId();
const path = useMemo(() => {
const rng = createRng(id);
return generateCirclePath(rng);
}, [id]);
return (
<span className={cn("relative inline-block px-2 py-1", className)}>
<svg
className="absolute pointer-events-none overflow-visible"
preserveAspectRatio="none"
viewBox="-5 -5 110 110"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
style={{
top: "-6px",
left: "-8px",
width: "calc(100% + 16px)",
height: "calc(100% + 12px)",
zIndex: 10,
}}
>
<motion.path
d={path}
initial={{ pathLength: 0, opacity: 0 }}
whileInView={{ pathLength: 1, opacity: 1 }}
viewport={{ once: true, margin: "-5%" }}
transition={{
pathLength: {
duration: 0.7,
delay: delay,
ease: [0.4, 0, 0.2, 1],
},
opacity: {
duration: 0.1,
delay: delay,
},
}}
stroke={color}
strokeWidth="2.5"
strokeLinecap="round"
fill="none"
vectorEffect="non-scaling-stroke"
/>
</svg>
{children}
</span>
);
};