feat: add dotted glow background and enhance stack section animations
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"registries": {
|
"registries": {
|
||||||
"@magicui": "https://magicui.design/r/{name}.json"
|
"@magicui": "https://magicui.design/r/{name}.json",
|
||||||
|
"@aceternity": "https://ui.aceternity.com/registry/{name}.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,72 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
import { StackedCubesLight } from "@/components/ui/StackedCubesLight";
|
import { StackedCubesLight } from "@/components/ui/StackedCubesLight";
|
||||||
import { H1, H2, P } from '@/components/Texts';
|
import { H2, P } from "@/components/Texts";
|
||||||
import { FadeIn } from "./FadeIn";
|
import { FadeIn } from "./FadeIn";
|
||||||
|
import { DottedGlowBackground } from '@/components/ui/dotted-glow-background';
|
||||||
|
|
||||||
export function StackSectionLight() {
|
export function StackSectionLight() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full bg-white lg:px-0 py-12 lg:py-24 px-6 relative lg:pt-32">
|
<section className="relative w-full overflow-hidden py-24 lg:py-40">
|
||||||
<div className="mx-auto max-w-7xl">
|
{/* === Background Layer === */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-16 items-center lg:items-center">
|
<div className="absolute inset-0 -z-10 bg-white">
|
||||||
<div
|
{/* Dotted Glow Background */}
|
||||||
aria-hidden="true"
|
<DottedGlowBackground
|
||||||
className="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]"
|
gap={15}
|
||||||
>
|
radius={2}
|
||||||
<div
|
color="rgba(0,0,0,0.4)"
|
||||||
style={{
|
glowColor="rgba(0,170,255,0.85)"
|
||||||
clipPath:
|
opacity={0.2}
|
||||||
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
|
|
||||||
}}
|
|
||||||
className="relative left-[calc(50%+3rem)] aspect-1155/678 w-144.5 -translate-x-1/2 bg-linear-to-tr from-blue-300 to-blue-600 opacity-20 sm:left-[calc(50%+36rem)] sm:w-288.75"
|
|
||||||
/>
|
/>
|
||||||
|
{/* Faint 3D grid floor */}
|
||||||
|
<div className="absolute inset-0 flex items-end justify-center overflow-hidden">
|
||||||
|
<div className="w-[200vw] h-[200vh] bg-[linear-gradient(to_right,rgba(0,0,0,0.03)_1px,transparent_1px),linear-gradient(to_bottom,rgba(0,0,0,0.03)_1px,transparent_1px)] bg-[size:60px_60px] [transform:perspective(800px)_rotateX(70deg)] origin-bottom opacity-50" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
{/* === Content === */}
|
||||||
aria-hidden="true"
|
<div className="relative mx-auto max-w-7xl px-6 lg:px-8 grid grid-cols-1 lg:grid-cols-3 gap-16 items-center">
|
||||||
className="absolute inset-x-0 bottom-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:bottom-[calc(100%-30rem)]"
|
{/* Left Column - Text */}
|
||||||
>
|
<div className="text-center lg:text-left">
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
clipPath:
|
|
||||||
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
|
|
||||||
}}
|
|
||||||
className="relative left-[calc(30%-3rem)] aspect-1155/678 w-144.5 -translate-x-1/2 bg-linear-to-tr from-blue-200 to-blue-400 opacity-15 sm:left-[calc(50%-36rem)] sm:w-288.75"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Left Column - Text (1/3 width) */}
|
|
||||||
<div className="text-center lg:text-left lg:col-span-1 order-1 lg:order-1 pt-12">
|
|
||||||
<FadeIn>
|
<FadeIn>
|
||||||
<H2 className="" color="dark">
|
<H2 color="dark" className="text-4xl sm:text-5xl font-semibold">
|
||||||
The Mycelium Stack
|
The Mycelium Stack
|
||||||
</H2>
|
</H2>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
|
|
||||||
<FadeIn>
|
<FadeIn>
|
||||||
<P className="mx-auto mt-8 max-w-3xl" color="dark">
|
<P color="dark" className="mt-6 text-lg leading-relaxed text-gray-600">
|
||||||
Built with Mycelium technology, our AI infrastructure ensures unbreakable networks, complete data sovereignty, ultra-secure agent-human communication, and unhackable data storage systems.
|
Built with Mycelium technology, our AI infrastructure ensures
|
||||||
|
unbreakable networks, complete data sovereignty, ultra-secure
|
||||||
|
agent-human communication, and unhackable data storage systems.
|
||||||
</P>
|
</P>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
</div>
|
</div>
|
||||||
{/* Right Column - Stacked Cubes (2/3 width) */}
|
|
||||||
<div className="lg:col-span-2 flex items-center justify-center lg:justify-start order-2 lg:order-2 mt-8 lg:mt-0">
|
{/* Right Column - Animated Stack */}
|
||||||
<FadeIn>
|
<div className="lg:col-span-2 flex items-center justify-center lg:justify-start relative">
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: 30, opacity: 0 }}
|
||||||
|
whileInView={{ y: 0, opacity: 1 }}
|
||||||
|
transition={{ duration: 1.2, ease: "easeOut" }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
y: [0, -10, 0],
|
||||||
|
rotateZ: [0, 0.5, -0.5, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 6,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
<StackedCubesLight />
|
<StackedCubesLight />
|
||||||
</FadeIn>
|
</motion.div>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
308
src/components/ui/dotted-glow-background.tsx
Normal file
308
src/components/ui/dotted-glow-background.tsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
type DottedGlowBackgroundProps = {
|
||||||
|
className?: string;
|
||||||
|
/** distance between dot centers in pixels */
|
||||||
|
gap?: number;
|
||||||
|
/** base radius of each dot in CSS px */
|
||||||
|
radius?: number;
|
||||||
|
/** dot color (will pulse by alpha) */
|
||||||
|
color?: string;
|
||||||
|
/** optional dot color for dark mode */
|
||||||
|
darkColor?: string;
|
||||||
|
/** shadow/glow color for bright dots */
|
||||||
|
glowColor?: string;
|
||||||
|
/** optional glow color for dark mode */
|
||||||
|
darkGlowColor?: string;
|
||||||
|
/** optional CSS variable name for light dot color (e.g. --color-zinc-900) */
|
||||||
|
colorLightVar?: string;
|
||||||
|
/** optional CSS variable name for dark dot color (e.g. --color-zinc-100) */
|
||||||
|
colorDarkVar?: string;
|
||||||
|
/** optional CSS variable name for light glow color */
|
||||||
|
glowColorLightVar?: string;
|
||||||
|
/** optional CSS variable name for dark glow color */
|
||||||
|
glowColorDarkVar?: string;
|
||||||
|
/** global opacity for the whole layer */
|
||||||
|
opacity?: number;
|
||||||
|
/** background radial fade opacity (0 = transparent background) */
|
||||||
|
backgroundOpacity?: number;
|
||||||
|
/** minimum per-dot speed in rad/s */
|
||||||
|
speedMin?: number;
|
||||||
|
/** maximum per-dot speed in rad/s */
|
||||||
|
speedMax?: number;
|
||||||
|
/** global speed multiplier for all dots */
|
||||||
|
speedScale?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canvas-based dotted background that randomly glows and dims.
|
||||||
|
* - Uses a stable grid of dots.
|
||||||
|
* - Each dot gets its own phase + speed producing organic shimmering.
|
||||||
|
* - Handles high-DPI and resizes via ResizeObserver.
|
||||||
|
*/
|
||||||
|
export function DottedGlowBackground({
|
||||||
|
className,
|
||||||
|
gap = 12,
|
||||||
|
radius = 2,
|
||||||
|
color = "rgba(0,0,0,0.7)",
|
||||||
|
darkColor,
|
||||||
|
glowColor = "rgba(0, 170, 255, 0.85)",
|
||||||
|
darkGlowColor,
|
||||||
|
colorLightVar,
|
||||||
|
colorDarkVar,
|
||||||
|
glowColorLightVar,
|
||||||
|
glowColorDarkVar,
|
||||||
|
opacity = 0.6,
|
||||||
|
backgroundOpacity = 0,
|
||||||
|
speedMin = 0.4,
|
||||||
|
speedMax = 1.3,
|
||||||
|
speedScale = 1,
|
||||||
|
}: DottedGlowBackgroundProps) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [resolvedColor, setResolvedColor] = useState<string>(color);
|
||||||
|
const [resolvedGlowColor, setResolvedGlowColor] = useState<string>(glowColor);
|
||||||
|
|
||||||
|
// Resolve CSS variable value from the container or root
|
||||||
|
const resolveCssVariable = (
|
||||||
|
el: Element,
|
||||||
|
variableName?: string,
|
||||||
|
): string | null => {
|
||||||
|
if (!variableName) return null;
|
||||||
|
const normalized = variableName.startsWith("--")
|
||||||
|
? variableName
|
||||||
|
: `--${variableName}`;
|
||||||
|
const fromEl = getComputedStyle(el as Element)
|
||||||
|
.getPropertyValue(normalized)
|
||||||
|
.trim();
|
||||||
|
if (fromEl) return fromEl;
|
||||||
|
const root = document.documentElement;
|
||||||
|
const fromRoot = getComputedStyle(root).getPropertyValue(normalized).trim();
|
||||||
|
return fromRoot || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectDarkMode = (): boolean => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (root.classList.contains("dark")) return true;
|
||||||
|
if (root.classList.contains("light")) return false;
|
||||||
|
return (
|
||||||
|
window.matchMedia &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep resolved colors in sync with theme changes and prop updates
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current ?? document.documentElement;
|
||||||
|
|
||||||
|
const compute = () => {
|
||||||
|
const isDark = detectDarkMode();
|
||||||
|
|
||||||
|
let nextColor: string = color;
|
||||||
|
let nextGlow: string = glowColor;
|
||||||
|
|
||||||
|
if (isDark) {
|
||||||
|
const varDot = resolveCssVariable(container, colorDarkVar);
|
||||||
|
const varGlow = resolveCssVariable(container, glowColorDarkVar);
|
||||||
|
nextColor = varDot || darkColor || nextColor;
|
||||||
|
nextGlow = varGlow || darkGlowColor || nextGlow;
|
||||||
|
} else {
|
||||||
|
const varDot = resolveCssVariable(container, colorLightVar);
|
||||||
|
const varGlow = resolveCssVariable(container, glowColorLightVar);
|
||||||
|
nextColor = varDot || nextColor;
|
||||||
|
nextGlow = varGlow || nextGlow;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResolvedColor(nextColor);
|
||||||
|
setResolvedGlowColor(nextGlow);
|
||||||
|
};
|
||||||
|
|
||||||
|
compute();
|
||||||
|
|
||||||
|
const mql = window.matchMedia
|
||||||
|
? window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
: null;
|
||||||
|
const handleMql = () => compute();
|
||||||
|
mql?.addEventListener?.("change", handleMql);
|
||||||
|
|
||||||
|
const mo = new MutationObserver(() => compute());
|
||||||
|
mo.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class", "style"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mql?.removeEventListener?.("change", handleMql);
|
||||||
|
mo.disconnect();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
color,
|
||||||
|
darkColor,
|
||||||
|
glowColor,
|
||||||
|
darkGlowColor,
|
||||||
|
colorLightVar,
|
||||||
|
colorDarkVar,
|
||||||
|
glowColorLightVar,
|
||||||
|
glowColorDarkVar,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = canvasRef.current;
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!el || !container) return;
|
||||||
|
|
||||||
|
const ctx = el.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
let raf = 0;
|
||||||
|
let stopped = false;
|
||||||
|
|
||||||
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
const { width, height } = container.getBoundingClientRect();
|
||||||
|
el.width = Math.max(1, Math.floor(width * dpr));
|
||||||
|
el.height = Math.max(1, Math.floor(height * dpr));
|
||||||
|
el.style.width = `${Math.floor(width)}px`;
|
||||||
|
el.style.height = `${Math.floor(height)}px`;
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(resize);
|
||||||
|
ro.observe(container);
|
||||||
|
resize();
|
||||||
|
|
||||||
|
// Precompute dot metadata for a medium-sized grid and regenerate on resize
|
||||||
|
let dots: { x: number; y: number; phase: number; speed: number }[] = [];
|
||||||
|
|
||||||
|
const regenDots = () => {
|
||||||
|
dots = [];
|
||||||
|
const { width, height } = container.getBoundingClientRect();
|
||||||
|
const cols = Math.ceil(width / gap) + 2;
|
||||||
|
const rows = Math.ceil(height / gap) + 2;
|
||||||
|
const min = Math.min(speedMin, speedMax);
|
||||||
|
const max = Math.max(speedMin, speedMax);
|
||||||
|
for (let i = -1; i < cols; i++) {
|
||||||
|
for (let j = -1; j < rows; j++) {
|
||||||
|
const x = i * gap + (j % 2 === 0 ? 0 : gap * 0.5); // offset every other row
|
||||||
|
const y = j * gap;
|
||||||
|
// Randomize phase and speed slightly per dot
|
||||||
|
const phase = Math.random() * Math.PI * 2;
|
||||||
|
const span = Math.max(max - min, 0);
|
||||||
|
const speed = min + Math.random() * span; // configurable rad/s
|
||||||
|
dots.push({ x, y, phase, speed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const regenThrottled = () => {
|
||||||
|
regenDots();
|
||||||
|
};
|
||||||
|
|
||||||
|
regenDots();
|
||||||
|
|
||||||
|
let last = performance.now();
|
||||||
|
|
||||||
|
const draw = (now: number) => {
|
||||||
|
if (stopped) return;
|
||||||
|
const dt = (now - last) / 1000; // seconds
|
||||||
|
last = now;
|
||||||
|
const { width, height } = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, el.width, el.height);
|
||||||
|
ctx.globalAlpha = opacity;
|
||||||
|
|
||||||
|
// optional subtle background fade for depth (defaults to 0 = transparent)
|
||||||
|
if (backgroundOpacity > 0) {
|
||||||
|
const grad = ctx.createRadialGradient(
|
||||||
|
width * 0.5,
|
||||||
|
height * 0.4,
|
||||||
|
Math.min(width, height) * 0.1,
|
||||||
|
width * 0.5,
|
||||||
|
height * 0.5,
|
||||||
|
Math.max(width, height) * 0.7,
|
||||||
|
);
|
||||||
|
grad.addColorStop(0, "rgba(0,0,0,0)");
|
||||||
|
grad.addColorStop(
|
||||||
|
1,
|
||||||
|
`rgba(0,0,0,${Math.min(Math.max(backgroundOpacity, 0), 1)})`,
|
||||||
|
);
|
||||||
|
ctx.fillStyle = grad as unknown as CanvasGradient;
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// animate dots
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = resolvedColor;
|
||||||
|
|
||||||
|
const time = (now / 1000) * Math.max(speedScale, 0);
|
||||||
|
for (let i = 0; i < dots.length; i++) {
|
||||||
|
const d = dots[i];
|
||||||
|
// Linear triangle wave 0..1..0 for linear glow/dim
|
||||||
|
const mod = (time * d.speed + d.phase) % 2;
|
||||||
|
const lin = mod < 1 ? mod : 2 - mod; // 0..1..0
|
||||||
|
const a = 0.25 + 0.55 * lin; // 0.25..0.8 linearly
|
||||||
|
|
||||||
|
// draw glow when bright
|
||||||
|
if (a > 0.6) {
|
||||||
|
const glow = (a - 0.6) / 0.4; // 0..1
|
||||||
|
ctx.shadowColor = resolvedGlowColor;
|
||||||
|
ctx.shadowBlur = 6 * glow;
|
||||||
|
} else {
|
||||||
|
ctx.shadowColor = "transparent";
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = a * opacity;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(d.x, d.y, radius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
raf = requestAnimationFrame(draw);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
resize();
|
||||||
|
regenThrottled();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
raf = requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopped = true;
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
ro.disconnect();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
gap,
|
||||||
|
radius,
|
||||||
|
resolvedColor,
|
||||||
|
resolvedGlowColor,
|
||||||
|
opacity,
|
||||||
|
backgroundOpacity,
|
||||||
|
speedMin,
|
||||||
|
speedMax,
|
||||||
|
speedScale,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={className}
|
||||||
|
style={{ position: "absolute", inset: 0 }}
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{ display: "block", width: "100%", height: "100%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DottedGlowBackground;
|
||||||
@@ -24,5 +24,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"],
|
||||||
|
"extend": {
|
||||||
|
"animation": {
|
||||||
|
"pulse-slow": "pulse 6s ease-in-out infinite"
|
||||||
|
},
|
||||||
|
"keyframes": {
|
||||||
|
"pulse": {
|
||||||
|
"0%, 100%": { "opacity": "1" },
|
||||||
|
"50%": { "opacity": "0.6" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user