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 { Button } from "@/components/Button";
|
||||||
import { Spotlight } from "@/components/ui/spotlight";
|
import { Spotlight } from "@/components/ui/spotlight";
|
||||||
import { GridBlink } from "@/components/ui/GridBlink";
|
|
||||||
import { H1, H4, H5 } from "@/components/Texts";
|
import { H1, H4, H5 } from "@/components/Texts";
|
||||||
|
import { HomeHeadline } from "@/components/HomeHeadline";
|
||||||
|
|
||||||
export function HomeBlink({ onGetStartedClick }: { onGetStartedClick: () => void }) {
|
export function HomeBlink({ onGetStartedClick }: { onGetStartedClick: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div className="px-4">
|
<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">
|
<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 */}
|
{/* ✅ Cyan Radial Glow */}
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 1024 1024"
|
viewBox="0 0 1024 1024"
|
||||||
aria-hidden="true"
|
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
|
<circle
|
||||||
r={512}
|
r={512}
|
||||||
cx={512}
|
cx={512}
|
||||||
cy={512}
|
cy={512}
|
||||||
fill="url(#mycelium-cyan-glow)"
|
fill="url(#mycelium-cyan-glow)"
|
||||||
fillOpacity="0.1"
|
fillOpacity="0.2"
|
||||||
/>
|
/>
|
||||||
<defs>
|
<defs>
|
||||||
<radialGradient id="mycelium-cyan-glow">
|
<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" />
|
<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">
|
<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">
|
<HomeHeadline />
|
||||||
MYCELIUM
|
<H4 className="text-center mt-8">The Living Network of the Next Internet</H4>
|
||||||
</H1>
|
|
||||||
<H4 className="text-center mt-4">The Living Network of the Next Internet</H4>
|
|
||||||
|
|
||||||
<H5 className="mx-auto mt-6 max-w-4xl text-center font-normal text-neutral-500">
|
<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.
|
A new internet is emerging — private, distributed, and self-sovereign.
|
||||||
|
|||||||
Reference in New Issue
Block a user