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%)',
 | 
					        />
 | 
				
			||||||
            }}
 | 
					        {/* Faint 3D grid floor */}
 | 
				
			||||||
            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"
 | 
					        <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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {/* === Content === */}
 | 
				
			||||||
 | 
					      <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">
 | 
				
			||||||
 | 
					        {/* Left Column - Text */}
 | 
				
			||||||
 | 
					        <div className="text-center lg:text-left">
 | 
				
			||||||
 | 
					          <FadeIn>
 | 
				
			||||||
 | 
					            <H2 color="dark" className="text-4xl sm:text-5xl font-semibold">
 | 
				
			||||||
 | 
					              The Mycelium Stack
 | 
				
			||||||
 | 
					            </H2>
 | 
				
			||||||
 | 
					          </FadeIn>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <FadeIn>
 | 
				
			||||||
 | 
					            <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.
 | 
				
			||||||
 | 
					            </P>
 | 
				
			||||||
 | 
					          </FadeIn>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
         <div
 | 
					        {/* Right Column - Animated Stack */}
 | 
				
			||||||
          aria-hidden="true"
 | 
					        <div className="lg:col-span-2 flex items-center justify-center lg:justify-start relative">
 | 
				
			||||||
          className="absolute inset-x-0 bottom-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:bottom-[calc(100%-30rem)]"
 | 
					          <motion.div
 | 
				
			||||||
        >
 | 
					            initial={{ y: 30, opacity: 0 }}
 | 
				
			||||||
          <div
 | 
					            whileInView={{ y: 0, opacity: 1 }}
 | 
				
			||||||
            style={{
 | 
					            transition={{ duration: 1.2, ease: "easeOut" }}
 | 
				
			||||||
              clipPath:
 | 
					            viewport={{ once: true }}
 | 
				
			||||||
                '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%)',
 | 
					          >
 | 
				
			||||||
            }}
 | 
					            <motion.div
 | 
				
			||||||
            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"
 | 
					              animate={{
 | 
				
			||||||
          />
 | 
					                y: [0, -10, 0],
 | 
				
			||||||
        </div>
 | 
					                rotateZ: [0, 0.5, -0.5, 0],
 | 
				
			||||||
          {/* Left Column - Text (1/3 width) */}
 | 
					              }}
 | 
				
			||||||
            <div className="text-center lg:text-left lg:col-span-1 order-1 lg:order-1 pt-12">
 | 
					              transition={{
 | 
				
			||||||
            <FadeIn>
 | 
					                duration: 6,
 | 
				
			||||||
              <H2 className="" color="dark">
 | 
					                repeat: Infinity,
 | 
				
			||||||
                The Mycelium Stack
 | 
					                ease: "easeInOut",
 | 
				
			||||||
              </H2>
 | 
					              }}
 | 
				
			||||||
            </FadeIn>
 | 
					              className="relative"
 | 
				
			||||||
            
 | 
					            >
 | 
				
			||||||
            <FadeIn>
 | 
					 | 
				
			||||||
              <P className="mx-auto mt-8 max-w-3xl" color="dark">
 | 
					 | 
				
			||||||
                Built with Mycelium technology, our AI infrastructure ensures unbreakable networks, complete data sovereignty, ultra-secure agent-human communication, and unhackable data storage systems.
 | 
					 | 
				
			||||||
              </P>
 | 
					 | 
				
			||||||
            </FadeIn>
 | 
					 | 
				
			||||||
          </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">
 | 
					 | 
				
			||||||
            <FadeIn>
 | 
					 | 
				
			||||||
              <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