- Created a new utility script `lottie-to-svg.ts` to convert Lottie animations to SVG format using `bodymovin-to-smil`. - Added a new Lottie animation file `zipper.json` for the zipper animation. - Generated the corresponding SVG file `zipper.svg` from the Lottie animation. - Updated `deno.json` to include new dependencies and references for the zipper animation. - Modified `main.css` to introduce new color variables and styles for the zipper animation.
178 lines
6.4 KiB
TypeScript
178 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useId, useRef, useState } from "react";
|
|
|
|
export const ZipperSVG: React.FC<{ className?: string }> = ({ className }) => {
|
|
const zipperTeethDefId = useId();
|
|
const zipperPullId = useId();
|
|
const hoverTriggerId = useId().replace("»", "x").replace("«", "z");
|
|
|
|
const animatedZipperPathValues = [
|
|
"M -355 -450 C -354.6 -65.1 -355 368.5 -1.35 370 C 5.3 370 0 370 0 370 H -355 V -450",
|
|
"M -355 -450 C -115 -161 -1.53 68.83 -1.35 370 C -1.06 295.48 0 370 0 370 H -355 V -450",
|
|
"M-262.6 -450 C-90.08,-229.72 -1.53,-81.25 -1.35,4.92 C-1.06,145.39 0,370 0,370 H -355 V -450",
|
|
"M-166.48 -450 C-61.44,-299.83 -1.53,-231.33 -1.35,-145.16 C-1.06,-4.69 0,370 0,370 H -355 V -450",
|
|
"M-89.81 -450 C-32.97,-354.91 -1.53,-338.53 -1.35,-252.36 C-1.06,-111.89 0,370 0,370 H -355 V -450",
|
|
"M-45.9 -450 C-17.98,-385.45 -1.53,-402.85 -1.35,-316.68 C-1.06,-176.2 0,370 0,370 H -355 V -450",
|
|
"M-18.3 -450 C-9.66,-401.82 -1.53,-445.73 -1.35,-359.56 C-1.06,-219.09 0,370 0,370 H -355 V -450",
|
|
"M-1 -450 C-1,-405.25 -1.03,-401.38 -0.98,-380.87 C-0.62,-239.38 0,370 0,370 H -355 V -450",
|
|
];
|
|
|
|
const zippedPath = animatedZipperPathValues.at(-1)!;
|
|
|
|
const animationDuration = "0.8s";
|
|
const animationEasing = "0.4 0 0.2 1"; // A standard ease-in-out curve
|
|
|
|
const unzipTeethRef = useRef<SVGAnimateElement>(null);
|
|
const zipTeethRef = useRef<SVGAnimateElement>(null);
|
|
const unzipPullRef = useRef<SVGAnimateTransformElement>(null);
|
|
const zipPullRef = useRef<SVGAnimateTransformElement>(null);
|
|
const [isZipped, setIsZipped] = useState(true);
|
|
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
|
useEffect(() => {
|
|
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
|
}, []);
|
|
|
|
const handleToggle = () => {
|
|
if (!isTouchDevice) return;
|
|
|
|
if (isZipped) {
|
|
// Trigger the "unzip" animations
|
|
unzipTeethRef.current?.beginElement();
|
|
unzipPullRef.current?.beginElement();
|
|
} else {
|
|
// Trigger the "zip" animations
|
|
zipTeethRef.current?.beginElement();
|
|
zipPullRef.current?.beginElement();
|
|
}
|
|
setIsZipped(!isZipped); // Toggle the state
|
|
};
|
|
|
|
return (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
preserveAspectRatio="none"
|
|
viewBox="150 100 700 500"
|
|
className={className}
|
|
>
|
|
<defs>
|
|
{/* --- Path for the Zipper Teeth (starts in the zipped-up state) --- */}
|
|
<path
|
|
id={zipperTeethDefId}
|
|
stroke="var(--primary)"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
fill="var(--background)"
|
|
vectorEffect="non-scaling-stroke"
|
|
strokeWidth="1"
|
|
strokeOpacity="1"
|
|
d={zippedPath} // Start with the final, "zipped up" path
|
|
/>
|
|
|
|
{/* --- HOVER ANIMATIONS (Unzipping) --- */}
|
|
<animate
|
|
ref={unzipTeethRef}
|
|
xlinkHref={`#${zipperTeethDefId}`}
|
|
begin={isTouchDevice ? "indefinite" : `${hoverTriggerId}.mouseover`}
|
|
dur={animationDuration}
|
|
fill="freeze"
|
|
attributeName="d"
|
|
// Animate from zipped to unzipped
|
|
values={animatedZipperPathValues.toReversed().join(";")}
|
|
calcMode="spline"
|
|
keySplines={`${animationEasing};`.repeat(
|
|
animatedZipperPathValues.length - 1,
|
|
).slice(0, -1)}
|
|
/>
|
|
<animateTransform
|
|
ref={unzipPullRef}
|
|
xlinkHref={`#${zipperPullId}`}
|
|
begin={isTouchDevice ? "indefinite" : `${hoverTriggerId}.mouseover`}
|
|
dur={animationDuration}
|
|
fill="freeze"
|
|
attributeName="transform"
|
|
type="translate"
|
|
additive="replace" // Use replace to override the initial transform
|
|
// Animate from top (zipped) to bottom (unzipped)
|
|
values="8 -1220; 8 160.329"
|
|
calcMode="spline"
|
|
keySplines={animationEasing}
|
|
/>
|
|
|
|
{/* --- MOUSEOUT ANIMATIONS (Zipping back up) --- */}
|
|
<animate
|
|
ref={zipTeethRef}
|
|
xlinkHref={`#${zipperTeethDefId}`}
|
|
begin={isTouchDevice ? "indefinite" : `${hoverTriggerId}.mouseout`}
|
|
dur={animationDuration}
|
|
fill="freeze"
|
|
attributeName="d"
|
|
// Animate from unzipped back to zipped
|
|
values={animatedZipperPathValues.join(";")}
|
|
calcMode="spline"
|
|
keySplines={`${animationEasing};`.repeat(
|
|
animatedZipperPathValues.length - 1,
|
|
).slice(0, -1)}
|
|
/>
|
|
<animateTransform
|
|
ref={zipPullRef}
|
|
xlinkHref={`#${zipperPullId}`}
|
|
begin={isTouchDevice ? "indefinite" : `${hoverTriggerId}.mouseout`}
|
|
dur={animationDuration}
|
|
fill="freeze"
|
|
attributeName="transform"
|
|
type="translate"
|
|
additive="replace"
|
|
// Animate from bottom (unzipped) back to top (zipped)
|
|
values="8 160.329; 8 -1220"
|
|
calcMode="spline"
|
|
keySplines={animationEasing}
|
|
/>
|
|
</defs>
|
|
|
|
<g id="_R_G">
|
|
{/* Zipper Teeth (Left and Right) */}
|
|
<g transform="translate(494.7, 537)">
|
|
<use xlinkHref={`#${zipperTeethDefId}`} />
|
|
</g>
|
|
<g transform="scale(-1, 1) translate(-1000, 0)">
|
|
<g transform="translate(494.7, 537)">
|
|
<use xlinkHref={`#${zipperTeethDefId}`} />
|
|
</g>
|
|
</g>
|
|
|
|
{/* Zipper Pull - wrapped in two groups to handle transforms cleanly */}
|
|
<g transform="translate(494.7, 537) scale(0.38 0.38)">
|
|
<g id={zipperPullId} transform="translate(8 -1220)">
|
|
<path
|
|
fill="var(--accent-foreground)"
|
|
stroke="#fff"
|
|
d="M52 156H-54l-10 242c-1 33 21 46 39 49s42 0 56-1c15-2 33-22 32-42L52 156z"
|
|
/>
|
|
<path
|
|
fill="var(--background)"
|
|
d="M41 191v42c0 11-9 20-20 20h-45c-11 0-20-9-20-20v-42c0-12 9-20 20-20h45c11 0 20 8 20 20zM-1 346c23 0 43 18 43 40s-20 40-43 40c-24 0-43-18-43-40s19-40 43-40z"
|
|
/>
|
|
<path
|
|
fill="var(--accent-foreground)"
|
|
d="M29 1v197c0 6-5 11-11 11h-38c-6 0-10-5-10-11V1c0-6 4-10 10-10h38c6 0 11 4 11 10z"
|
|
/>
|
|
</g>
|
|
</g>
|
|
</g>
|
|
|
|
{/* INVISIBLE HOVER AREA */}
|
|
<rect
|
|
id={hoverTriggerId}
|
|
x="150"
|
|
y="100"
|
|
width="700"
|
|
height="500"
|
|
fill="transparent"
|
|
cursor="pointer"
|
|
onClick={handleToggle}
|
|
/>
|
|
</svg>
|
|
);
|
|
};
|