feat(branding): implement hand-drawn Copic marker, strikethrough, and pen circle effects; redesign guarantee section with animated signature
This commit is contained in:
124
apps/web/src/components/PenCircle.tsx
Normal file
124
apps/web/src/components/PenCircle.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user