97 lines
3 KiB
TypeScript
97 lines
3 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* PillMark — the brand imagotype as a living system.
|
|
*
|
|
* Reconstructs the 4-bar capsule grid (violet→blue gradient bar / ink bar +
|
|
* blue square / emerald bar) as inline SVG so each pill can be animated
|
|
* independently: it assembles on mount, then breathes. Used at hero scale and
|
|
* as a compact mark in dividers / footer.
|
|
*
|
|
* Decorative by default (aria-hidden). Pass `title` to expose it as an image.
|
|
*/
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import { gsap } from "gsap";
|
|
|
|
type Props = {
|
|
className?: string;
|
|
/** Animate assembly on mount (hero). */
|
|
animate?: boolean;
|
|
/** Continuous breathing after assembly. */
|
|
breathe?: boolean;
|
|
title?: string;
|
|
};
|
|
|
|
export default function PillMark({
|
|
className = "",
|
|
animate = false,
|
|
breathe = false,
|
|
title,
|
|
}: Props) {
|
|
const ref = useRef<SVGSVGElement>(null);
|
|
|
|
useEffect(() => {
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
|
|
const ctx = gsap.context(() => {
|
|
const bars = gsap.utils.toArray<SVGElement>(".pillmark__bar");
|
|
|
|
if (animate) {
|
|
gsap.set(bars, { transformOrigin: "center center" });
|
|
gsap.from(bars, {
|
|
scaleX: 0,
|
|
scaleY: 0.2,
|
|
opacity: 0,
|
|
duration: 1.1,
|
|
ease: "elastic.out(1, 0.75)",
|
|
stagger: { each: 0.09, from: "start" },
|
|
delay: 0.2,
|
|
});
|
|
}
|
|
|
|
if (breathe) {
|
|
bars.forEach((bar, i) => {
|
|
gsap.to(bar, {
|
|
y: i % 2 === 0 ? "+=6" : "-=6",
|
|
duration: 2.6 + i * 0.25,
|
|
ease: "sine.inOut",
|
|
repeat: -1,
|
|
yoyo: true,
|
|
delay: 1.2 + i * 0.1,
|
|
});
|
|
});
|
|
}
|
|
}, el);
|
|
|
|
return () => ctx.revert();
|
|
}, [animate, breathe]);
|
|
|
|
return (
|
|
<svg
|
|
ref={ref}
|
|
className={`pillmark ${className}`}
|
|
viewBox="0 0 1192 1287"
|
|
role={title ? "img" : "presentation"}
|
|
aria-hidden={title ? undefined : "true"}
|
|
aria-label={title}
|
|
>
|
|
{title ? <title>{title}</title> : null}
|
|
<defs>
|
|
<linearGradient id="pm-grad" x1="0" y1="0" x2="1192" y2="0" gradientUnits="userSpaceOnUse">
|
|
<stop offset="0" stopColor="#8b5cf6" />
|
|
<stop offset="1" stopColor="#3b82f6" />
|
|
</linearGradient>
|
|
</defs>
|
|
{/* top: full-width violet→blue gradient capsule */}
|
|
<rect className="pillmark__bar" x="0" y="0" width="1192" height="348.64" rx="174.32" fill="url(#pm-grad)" />
|
|
{/* middle row: ink wide capsule + blue square capsule */}
|
|
<rect className="pillmark__bar" x="0.02" y="480.64" width="725.02" height="348.64" rx="174.32" fill="#111827" />
|
|
<rect className="pillmark__bar" x="843" y="480.28" width="349.04" height="348.98" rx="174.32" fill="#3b82f6" />
|
|
{/* bottom: full-width emerald capsule */}
|
|
<rect className="pillmark__bar" x="0.04" y="938.21" width="1191.97" height="348.64" rx="174.32" fill="#10b981" />
|
|
</svg>
|
|
);
|
|
}
|