feat: use original particle scatter banner effect
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-21 11:22:56 +08:00
parent f83230d7e9
commit ff4f48bffc
3 changed files with 308 additions and 190 deletions

View File

@ -1,197 +1,14 @@
"use client";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
type AgentWorkflowParticleScatterProps = {
className?: string;
density?: "low" | "medium" | "high";
};
type Particle = {
x: number;
y: number;
radius: number;
speed: number;
drift: number;
alpha: number;
hue: number;
phase: number;
swing: number;
depth: number;
};
const PARTICLE_DENSITY_MULTIPLIER = {
low: 0.75,
medium: 1,
high: 1.35,
} as const;
function randomBetween(min: number, max: number) {
return min + Math.random() * (max - min);
}
export function AgentWorkflowParticleScatter({
className,
density = "medium",
}: AgentWorkflowParticleScatterProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const context = canvas.getContext("2d");
if (!context) {
return;
}
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
let width = 0;
let height = 0;
let animationFrameId = 0;
let particles: Particle[] = [];
const buildParticle = (): Particle => {
const depth = randomBetween(0.65, 1.15);
const spread = width * randomBetween(0.16, 0.48);
const originX = width / 2 + randomBetween(-spread, spread);
const originY = height * randomBetween(0.56, 1.06);
return {
x: originX,
y: originY,
radius: randomBetween(0.9, 2.8) * depth,
speed: randomBetween(0.08, 0.34) * depth,
drift: randomBetween(0.12, 0.38) * (Math.random() > 0.5 ? 1 : -1),
alpha: randomBetween(0.16, 0.84),
hue: Math.random() > 0.7 ? 0 : 160,
phase: randomBetween(0, Math.PI * 2),
swing: randomBetween(10, 42),
depth,
};
};
const resetParticle = (particle: Particle, respawnNearBottom = false) => {
const next = buildParticle();
particle.x = next.x;
particle.y = respawnNearBottom ? height * randomBetween(0.72, 1.04) : next.y;
particle.radius = next.radius;
particle.speed = next.speed;
particle.drift = next.drift;
particle.alpha = next.alpha;
particle.hue = next.hue;
particle.phase = next.phase;
particle.swing = next.swing;
particle.depth = next.depth;
};
const resize = () => {
const bounds = canvas.getBoundingClientRect();
const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
width = Math.max(1, bounds.width);
height = Math.max(1, bounds.height);
canvas.width = Math.round(width * pixelRatio);
canvas.height = Math.round(height * pixelRatio);
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
const baseCount = Math.round((width / 12) * PARTICLE_DENSITY_MULTIPLIER[density]);
particles = Array.from(
{ length: Math.max(56, Math.min(220, baseCount)) },
() => buildParticle()
);
};
const drawGlow = (x: number, y: number, radius: number, alpha: number, hue: number) => {
const gradient = context.createRadialGradient(x, y, 0, x, y, radius * 5.5);
if (hue === 160) {
gradient.addColorStop(0, `rgba(110, 255, 217, ${alpha})`);
gradient.addColorStop(0.35, `rgba(26, 214, 168, ${alpha * 0.7})`);
gradient.addColorStop(1, "rgba(26, 214, 168, 0)");
} else {
gradient.addColorStop(0, `rgba(255, 255, 255, ${alpha})`);
gradient.addColorStop(0.42, `rgba(209, 255, 244, ${alpha * 0.45})`);
gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
}
context.fillStyle = gradient;
context.beginPath();
context.arc(x, y, radius * 5.5, 0, Math.PI * 2);
context.fill();
};
const render = (time: number) => {
context.clearRect(0, 0, width, height);
const bottomGlow = context.createRadialGradient(
width / 2,
height * 0.92,
0,
width / 2,
height * 0.92,
width * 0.42
);
bottomGlow.addColorStop(0, "rgba(19, 221, 179, 0.18)");
bottomGlow.addColorStop(0.5, "rgba(19, 221, 179, 0.08)");
bottomGlow.addColorStop(1, "rgba(19, 221, 179, 0)");
context.fillStyle = bottomGlow;
context.fillRect(0, 0, width, height);
particles.forEach((particle) => {
particle.phase += 0.012;
particle.y -= particle.speed;
particle.x += Math.sin(time * 0.00032 + particle.phase) * particle.drift;
const waveOffset =
Math.sin(time * 0.001 + particle.phase * 2.4) * particle.swing * 0.018;
const pulseAlpha =
particle.alpha * (0.7 + 0.3 * Math.sin(time * 0.0016 + particle.phase));
const renderX = particle.x + waveOffset;
const renderY = particle.y;
if (renderY < -24 || renderX < -32 || renderX > width + 32) {
resetParticle(particle, true);
}
drawGlow(renderX, renderY, particle.radius, pulseAlpha * 0.68, particle.hue);
context.beginPath();
context.fillStyle =
particle.hue === 160
? `rgba(155, 255, 230, ${pulseAlpha})`
: `rgba(255, 255, 255, ${pulseAlpha})`;
context.arc(renderX, renderY, particle.radius, 0, Math.PI * 2);
context.fill();
});
if (!reducedMotionQuery.matches) {
animationFrameId = window.requestAnimationFrame(render);
}
};
resize();
const handleResize = () => {
resize();
};
window.addEventListener("resize", handleResize);
if (reducedMotionQuery.matches) {
render(0);
} else {
animationFrameId = window.requestAnimationFrame(render);
}
return () => {
window.removeEventListener("resize", handleResize);
if (animationFrameId) {
window.cancelAnimationFrame(animationFrameId);
}
};
}, [density]);
return (
<div
className={cn(
@ -199,9 +16,13 @@ export function AgentWorkflowParticleScatter({
className
)}
>
<canvas
ref={canvasRef}
className="h-full w-full [mask-image:linear-gradient(to_top,black_58%,transparent_100%)]"
<iframe
aria-hidden="true"
className="h-full w-full border-0 bg-transparent [mask-image:linear-gradient(to_top,black_72%,transparent_100%)]"
loading="eager"
src="/particle-scatter-effect.html"
tabIndex={-1}
title="Particle Scatter Background"
/>
</div>
);

View File

@ -44,9 +44,8 @@ export function AgentWorkflowShowcaseBanner({
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<AgentWorkflowParticleScatter className="h-[48%]" density="high" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[44%] bg-[radial-gradient(circle_at_50%_100%,rgba(16,185,129,0.22),transparent_52%)]" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[32%] bg-gradient-to-t from-black via-black/35 to-transparent" />
<AgentWorkflowParticleScatter className="top-0 h-full opacity-95" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[34%] bg-gradient-to-t from-black via-black/28 to-transparent" />
<div className="relative z-10">
<h1 className="mb-3 text-3xl font-bold text-white md:text-4xl">{resolved.title}</h1>
@ -116,7 +115,6 @@ export function AgentWorkflowShowcaseBanner({
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(16,185,129,0.12),transparent_42%)]" />
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/70 via-black/10 to-black/35" />
<AgentWorkflowParticleScatter className="h-[42%] opacity-60" density="medium" />
{resolved.overlayText ? (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center px-8 text-center">

File diff suppressed because one or more lines are too long