forked from emre/www_projectmycelium_com
feat: add animated text hover effect to home headline
- Created TextHoverEffect component with animated gradient mask and stroke drawing - Replaced static H1 title with interactive HomeHeadline component featuring auto-looping animation - Enhanced visual appeal with cyan gradient glow and smooth reveal effects
This commit is contained in:
10
src/components/HomeHeadline.tsx
Normal file
10
src/components/HomeHeadline.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
|
||||
|
||||
export function HomeHeadline() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-auto max-h-[200px]">
|
||||
<TextHoverEffect text="MYCELIUM" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
141
src/components/ui/text-hover-effect.tsx
Normal file
141
src/components/ui/text-hover-effect.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { motion, useAnimation } from "motion/react";
|
||||
|
||||
export const TextHoverEffect = ({
|
||||
text,
|
||||
duration = 6, // loop duration
|
||||
}: {
|
||||
text: string;
|
||||
duration?: number;
|
||||
}) => {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const controls = useAnimation();
|
||||
const [cursor, setCursor] = useState({ x: 0, y: 0 });
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
// ✅ Animate mask looping automatically
|
||||
useEffect(() => {
|
||||
const loop = async () => {
|
||||
while (true) {
|
||||
await controls.start({
|
||||
cx: ["20%", "80%", "50%"],
|
||||
cy: ["20%", "80%", "50%"],
|
||||
transition: {
|
||||
duration,
|
||||
ease: "easeInOut",
|
||||
repeat: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
loop();
|
||||
}, [controls, duration]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 300 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onMouseMove={(e) => setCursor({ x: e.clientX, y: e.clientY })}
|
||||
className="select-none"
|
||||
>
|
||||
<defs>
|
||||
{/* ✅ Softer cyan gradient */}
|
||||
<linearGradient id="textGradient" gradientUnits="userSpaceOnUse">
|
||||
{hovered ? (
|
||||
<>
|
||||
<stop offset="0%" stopColor="#7df3ff" />
|
||||
<stop offset="40%" stopColor="#4adffa" />
|
||||
<stop offset="70%" stopColor="#18c5e8" />
|
||||
<stop offset="100%" stopColor="#0aaecb" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<stop offset="0%" stopColor="#7df3ff33" />
|
||||
<stop offset="100%" stopColor="#0aaecb33" />
|
||||
</>
|
||||
)}
|
||||
</linearGradient>
|
||||
|
||||
{/* ✅ Mask with autoplay motion */}
|
||||
<motion.radialGradient
|
||||
id="revealMask"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
r="45%"
|
||||
animate={controls}
|
||||
initial={{ cx: "50%", cy: "50%" }}
|
||||
>
|
||||
<stop offset="0%" stopColor="white" />
|
||||
<stop offset="100%" stopColor="black" />
|
||||
</motion.radialGradient>
|
||||
|
||||
{/* ✅ Glow */}
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="3.2" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<mask id="textMask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="url(#revealMask)" />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
{/* ✅ Background faint stroke */}
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
strokeWidth="0.15"
|
||||
className="fill-transparent stroke-neutral-300 dark:stroke-neutral-800 font-[helvetica] text-7xl font-bold"
|
||||
style={{ opacity: hovered ? 0.25 : 0.1 }}
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
|
||||
{/* ✅ Line drawing animation always plays too */}
|
||||
<motion.text
|
||||
x="50%"
|
||||
y="50%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
strokeWidth="0.25"
|
||||
className="fill-transparent stroke-cyan-300 font-[helvetica] text-7xl font-bold"
|
||||
initial={{ strokeDashoffset: 600, strokeDasharray: 600 }}
|
||||
animate={{
|
||||
strokeDashoffset: 0,
|
||||
strokeDasharray: 600,
|
||||
}}
|
||||
transition={{
|
||||
duration: 2.2,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</motion.text>
|
||||
|
||||
{/* ✅ Final filled glowing cyan text (mask reveals it) */}
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
stroke="url(#textGradient)"
|
||||
strokeWidth="1.1"
|
||||
mask="url(#textMask)"
|
||||
filter="url(#glow)"
|
||||
className="font-[helvetica] text-7xl font-bold fill-[url(#textGradient)]"
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -2,30 +2,26 @@
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Spotlight } from "@/components/ui/spotlight";
|
||||
import { GridBlink } from "@/components/ui/GridBlink";
|
||||
import { H1, H4, H5 } from "@/components/Texts";
|
||||
import { HomeHeadline } from "@/components/HomeHeadline";
|
||||
|
||||
export function HomeBlink({ onGetStartedClick }: { onGetStartedClick: () => void }) {
|
||||
return (
|
||||
<div className="px-4">
|
||||
<div className="relative mx-auto max-w-7xl border border-t-0 border-gray-100 bg-white overflow-hidden py-24 lg:py-32">
|
||||
|
||||
{/* ✅ Animated blinking grid */}
|
||||
<GridBlink />
|
||||
|
||||
|
||||
{/* ✅ Cyan Radial Glow */}
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
aria-hidden="true"
|
||||
className="absolute top-full left-1/2 w-7xl h-520 -translate-x-1/2 -translate-y-1/2 mask-image mask-[radial-gradient(circle,white,transparent)]"
|
||||
className="absolute top-full left-1/2 w-7xl h-720 -translate-x-1/2 -translate-y-1/2 mask-image mask-[radial-gradient(circle,white,transparent)]"
|
||||
>
|
||||
<circle
|
||||
r={512}
|
||||
cx={512}
|
||||
cy={512}
|
||||
fill="url(#mycelium-cyan-glow)"
|
||||
fillOpacity="0.1"
|
||||
fillOpacity="0.2"
|
||||
/>
|
||||
<defs>
|
||||
<radialGradient id="mycelium-cyan-glow">
|
||||
@@ -39,10 +35,8 @@ export function HomeBlink({ onGetStartedClick }: { onGetStartedClick: () => void
|
||||
<Spotlight className="-top-40 left-0 md:-top-20 md:left-60" />
|
||||
|
||||
<div className="relative z-10 mx-auto w-full max-w-7xl p-4 md:pt-0">
|
||||
<H1 className="text-black text-center font-bold tracking-wide">
|
||||
MYCELIUM
|
||||
</H1>
|
||||
<H4 className="text-center mt-4">The Living Network of the Next Internet</H4>
|
||||
<HomeHeadline />
|
||||
<H4 className="text-center mt-8">The Living Network of the Next Internet</H4>
|
||||
|
||||
<H5 className="mx-auto mt-6 max-w-4xl text-center font-normal text-neutral-500">
|
||||
A new internet is emerging — private, distributed, and self-sovereign.
|
||||
|
||||
Reference in New Issue
Block a user