design
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
||||||
import { motion, useScroll, useTransform, useSpring } from 'framer-motion';
|
import { motion, useScroll, useTransform, useSpring, useAnimationFrame } from 'framer-motion';
|
||||||
|
|
||||||
interface FlowingPathProps {
|
interface FlowingPathProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -22,66 +22,82 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
|
|||||||
const pathRef = useRef<SVGPathElement>(null);
|
const pathRef = useRef<SVGPathElement>(null);
|
||||||
|
|
||||||
const { scrollYProgress } = useScroll();
|
const { scrollYProgress } = useScroll();
|
||||||
|
const [vh, setVh] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setVh(window.innerHeight);
|
||||||
|
const handleResize = () => setVh(window.innerHeight);
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Calculate the progress needed to keep the dot at the center of the viewport
|
||||||
|
const centeredProgress = useTransform(
|
||||||
|
scrollYProgress,
|
||||||
|
(latest) => {
|
||||||
|
if (dimensions.height === 0 || vh === 0) return latest;
|
||||||
|
const scrollRange = dimensions.height - vh;
|
||||||
|
const currentScrollY = latest * scrollRange;
|
||||||
|
const targetY = currentScrollY + vh / 2;
|
||||||
|
return Math.max(0, Math.min(1, targetY / dimensions.height));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Smooth spring animation for the path drawing
|
// Smooth spring animation for the path drawing
|
||||||
const smoothProgress = useSpring(scrollYProgress, {
|
const smoothProgress = useSpring(centeredProgress, {
|
||||||
stiffness: 50,
|
stiffness: 40,
|
||||||
damping: 20,
|
damping: 25,
|
||||||
restDelta: 0.001,
|
restDelta: 0.0001,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Transform scroll progress to path drawing
|
// Transform scroll progress to path drawing
|
||||||
const pathDrawn = useTransform(smoothProgress, [0, 1], [0, 1]);
|
const pathDrawn = useTransform(smoothProgress, [0, 1], [0, 1]);
|
||||||
|
|
||||||
// Accent path transform
|
// Accent path transform
|
||||||
const accentPathDrawn = useTransform(smoothProgress, [0, 1], [0, 0.95]);
|
const accentPathDrawn = useTransform(smoothProgress, [0, 1], [0, 0.98]);
|
||||||
|
|
||||||
// Dot transforms
|
// Dot transforms
|
||||||
const dot1Distance = useTransform(smoothProgress, [0, 1], ['0%', '100%']);
|
const dot1Distance = useTransform(smoothProgress, [0, 1], ['0%', '100%']);
|
||||||
const dot2Distance = useTransform(smoothProgress, [0, 1], ['0%', '95%']);
|
const dot2Distance = useTransform(smoothProgress, [0, 1], ['0%', '98%']);
|
||||||
|
|
||||||
// Generate a flowing path that wraps around content sections
|
// Generate a flowing path that wraps around content sections
|
||||||
const generatePath = useCallback((width: number, height: number): string => {
|
const generatePath = useCallback((width: number, height: number): string => {
|
||||||
const margin = 60;
|
const margin = width < 768 ? 20 : 80;
|
||||||
const contentWidth = Math.min(width * 0.85, 900);
|
const contentWidth = Math.min(width * 0.9, 1200);
|
||||||
const leftEdge = (width - contentWidth) / 2 - margin;
|
const leftEdge = (width - contentWidth) / 2 - margin;
|
||||||
const rightEdge = (width + contentWidth) / 2 + margin;
|
const rightEdge = (width + contentWidth) / 2 + margin;
|
||||||
const centerX = width / 2;
|
const centerX = width / 2;
|
||||||
|
|
||||||
// Define key vertical positions (approximate section boundaries)
|
const sectionHeight = height / 8;
|
||||||
const sectionHeight = height / 6;
|
|
||||||
|
|
||||||
const nodes: PathNode[] = [
|
const nodes: PathNode[] = [
|
||||||
// Start from top left, flowing down
|
{ x: centerX - 100, y: 0, type: 'line' },
|
||||||
{ x: leftEdge, y: 0, type: 'line' },
|
{ x: centerX - 100, y: sectionHeight * 0.2, type: 'curve' },
|
||||||
{ x: leftEdge, y: sectionHeight * 0.3, type: 'curve' },
|
|
||||||
|
|
||||||
// Curve around hero section (right side)
|
// Hero section - wide sweep
|
||||||
{ x: rightEdge, y: sectionHeight * 0.6, type: 'curve' },
|
{ x: rightEdge, y: sectionHeight * 0.8, type: 'curve' },
|
||||||
{ x: rightEdge, y: sectionHeight * 1.2, type: 'curve' },
|
{ x: rightEdge - 50, y: sectionHeight * 1.5, type: 'curve' },
|
||||||
|
|
||||||
// Flow to left for section 02
|
// Section 2 - tight curve to left
|
||||||
{ x: leftEdge, y: sectionHeight * 1.5, type: 'curve' },
|
{ x: leftEdge + 50, y: sectionHeight * 2.2, type: 'curve' },
|
||||||
{ x: leftEdge, y: sectionHeight * 2, type: 'curve' },
|
{ x: leftEdge, y: sectionHeight * 3.0, type: 'curve' },
|
||||||
|
|
||||||
// Cross to right for section 03
|
// Section 3 - middle flow
|
||||||
{ x: rightEdge, y: sectionHeight * 2.3, type: 'curve' },
|
{ x: centerX + 150, y: sectionHeight * 3.8, type: 'curve' },
|
||||||
{ x: rightEdge, y: sectionHeight * 2.8, type: 'curve' },
|
{ x: centerX - 150, y: sectionHeight * 4.6, type: 'curve' },
|
||||||
|
|
||||||
// Back to left for section 04
|
// Section 4 - right side
|
||||||
{ x: leftEdge, y: sectionHeight * 3.2, type: 'curve' },
|
{ x: rightEdge - 20, y: sectionHeight * 5.4, type: 'curve' },
|
||||||
{ x: leftEdge, y: sectionHeight * 3.8, type: 'curve' },
|
{ x: rightEdge - 100, y: sectionHeight * 6.2, type: 'curve' },
|
||||||
|
|
||||||
// Right side for section 05
|
// Section 5 - back to left
|
||||||
{ x: rightEdge, y: sectionHeight * 4.2, type: 'curve' },
|
{ x: leftEdge + 100, y: sectionHeight * 7.0, type: 'curve' },
|
||||||
{ x: rightEdge, y: sectionHeight * 4.8, type: 'curve' },
|
|
||||||
|
|
||||||
// Final section - converge to center
|
// Final - center
|
||||||
{ x: centerX, y: sectionHeight * 5.5, type: 'curve' },
|
{ x: centerX, y: sectionHeight * 7.6, type: 'curve' },
|
||||||
{ x: centerX, y: height, type: 'line' },
|
{ x: centerX, y: height, type: 'line' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Build SVG path with smooth bezier curves
|
|
||||||
let path = `M ${nodes[0].x} ${nodes[0].y}`;
|
let path = `M ${nodes[0].x} ${nodes[0].y}`;
|
||||||
|
|
||||||
for (let i = 1; i < nodes.length; i++) {
|
for (let i = 1; i < nodes.length; i++) {
|
||||||
@@ -89,14 +105,9 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
|
|||||||
const curr = nodes[i];
|
const curr = nodes[i];
|
||||||
|
|
||||||
if (curr.type === 'curve') {
|
if (curr.type === 'curve') {
|
||||||
// Calculate control points for smooth S-curves
|
const cp1y = prev.y + (curr.y - prev.y) * 0.5;
|
||||||
const midY = (prev.y + curr.y) / 2;
|
const cp2y = prev.y + (curr.y - prev.y) * 0.5;
|
||||||
const controlX1 = prev.x;
|
path += ` C ${prev.x} ${cp1y}, ${curr.x} ${cp2y}, ${curr.x} ${curr.y}`;
|
||||||
const controlY1 = midY;
|
|
||||||
const controlX2 = curr.x;
|
|
||||||
const controlY2 = midY;
|
|
||||||
|
|
||||||
path += ` C ${controlX1} ${controlY1}, ${controlX2} ${controlY2}, ${curr.x} ${curr.y}`;
|
|
||||||
} else {
|
} else {
|
||||||
path += ` L ${curr.x} ${curr.y}`;
|
path += ` L ${curr.x} ${curr.y}`;
|
||||||
}
|
}
|
||||||
@@ -107,31 +118,29 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
|
|||||||
|
|
||||||
// Generate secondary decorative paths
|
// Generate secondary decorative paths
|
||||||
const generateSecondaryPath = useCallback((width: number, height: number, offset: number): string => {
|
const generateSecondaryPath = useCallback((width: number, height: number, offset: number): string => {
|
||||||
const margin = 40 + offset;
|
const margin = width < 768 ? 10 : 40 + offset;
|
||||||
const contentWidth = Math.min(width * 0.85, 900);
|
const contentWidth = Math.min(width * 0.85, 1000);
|
||||||
const leftEdge = (width - contentWidth) / 2 - margin;
|
const leftEdge = (width - contentWidth) / 2 - margin;
|
||||||
const rightEdge = (width + contentWidth) / 2 + margin;
|
const rightEdge = (width + contentWidth) / 2 + margin;
|
||||||
|
|
||||||
const sectionHeight = height / 6;
|
const sectionHeight = height / 8;
|
||||||
|
|
||||||
let path = `M ${leftEdge + offset * 2} ${-50}`;
|
let path = `M ${width / 2 + offset} ${-100}`;
|
||||||
|
|
||||||
// Simpler parallel path
|
|
||||||
const points = [
|
const points = [
|
||||||
{ x: leftEdge, y: sectionHeight * 0.5 },
|
{ x: leftEdge + offset, y: sectionHeight * 1.2 },
|
||||||
{ x: rightEdge, y: sectionHeight * 1.0 },
|
{ x: rightEdge - offset, y: sectionHeight * 2.5 },
|
||||||
{ x: leftEdge, y: sectionHeight * 1.8 },
|
{ x: leftEdge - offset, y: sectionHeight * 4.0 },
|
||||||
{ x: rightEdge, y: sectionHeight * 2.5 },
|
{ x: rightEdge + offset, y: sectionHeight * 5.5 },
|
||||||
{ x: leftEdge, y: sectionHeight * 3.5 },
|
{ x: width / 2 + offset, y: height + 100 },
|
||||||
{ x: rightEdge, y: sectionHeight * 4.5 },
|
|
||||||
{ x: width / 2, y: height + 50 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < points.length; i++) {
|
for (let i = 0; i < points.length; i++) {
|
||||||
const curr = points[i];
|
const curr = points[i];
|
||||||
const prev = i > 0 ? points[i - 1] : { x: leftEdge + offset * 2, y: -50 };
|
const prev = i > 0 ? points[i - 1] : { x: width / 2 + offset, y: -100 };
|
||||||
const midY = (prev.y + curr.y) / 2;
|
const cp1y = prev.y + (curr.y - prev.y) * 0.5;
|
||||||
path += ` C ${prev.x} ${midY}, ${curr.x} ${midY}, ${curr.x} ${curr.y}`;
|
const cp2y = prev.y + (curr.y - prev.y) * 0.5;
|
||||||
|
path += ` C ${prev.x} ${cp1y}, ${curr.x} ${cp2y}, ${curr.x} ${curr.y}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
@@ -206,28 +215,39 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
|
|||||||
style={{ position: 'absolute', top: 0, left: 0 }}
|
style={{ position: 'absolute', top: 0, left: 0 }}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
<feGaussianBlur stdDeviation="2" result="blur" />
|
<feGaussianBlur stdDeviation="4" result="blur" />
|
||||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||||
</filter>
|
</filter>
|
||||||
|
<filter id="dotGlow" x="-100%" y="-100%" width="300%" height="300%">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="blur" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.58 0 0 0 0 0.64 0 0 0 0 0.72 0 0 0 1 0" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
<stop offset="0%" stopColor="rgba(148, 163, 184, 0.1)" />
|
<stop offset="0%" stopColor="rgba(148, 163, 184, 0.05)" />
|
||||||
<stop offset="50%" stopColor="rgba(148, 163, 184, 0.5)" />
|
<stop offset="20%" stopColor="rgba(148, 163, 184, 0.3)" />
|
||||||
<stop offset="100%" stopColor="rgba(148, 163, 184, 0.1)" />
|
<stop offset="50%" stopColor="rgba(148, 163, 184, 0.6)" />
|
||||||
|
<stop offset="80%" stopColor="rgba(148, 163, 184, 0.3)" />
|
||||||
|
<stop offset="100%" stopColor="rgba(148, 163, 184, 0.05)" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
{/* Background decorative paths - static, very subtle */}
|
{/* Background decorative paths - static, very subtle */}
|
||||||
<path
|
<path
|
||||||
d={generateSecondaryPath(dimensions.width, dimensions.height, 20)}
|
d={generateSecondaryPath(dimensions.width, dimensions.height, 40)}
|
||||||
stroke="rgba(226, 232, 240, 0.4)"
|
stroke="rgba(148, 163, 184, 0.15)"
|
||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
strokeDasharray="5,10"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d={generateSecondaryPath(dimensions.width, dimensions.height, -15)}
|
d={generateSecondaryPath(dimensions.width, dimensions.height, -30)}
|
||||||
stroke="rgba(226, 232, 240, 0.3)"
|
stroke="rgba(148, 163, 184, 0.1)"
|
||||||
strokeWidth="0.5"
|
strokeWidth="0.5"
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
@@ -261,10 +281,18 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Particle System */}
|
||||||
|
<ParticleSystem
|
||||||
|
pathRef={pathRef}
|
||||||
|
progress={smoothProgress}
|
||||||
|
pathLength={pathLength}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Animated dot that travels along the path */}
|
{/* Animated dot that travels along the path */}
|
||||||
<motion.circle
|
<motion.circle
|
||||||
r="3"
|
r="4"
|
||||||
fill="rgba(15, 23, 42, 0.8)"
|
fill="rgba(30, 41, 59, 1)"
|
||||||
|
filter="url(#dotGlow)"
|
||||||
style={{
|
style={{
|
||||||
offsetPath: `path("${pathData}")`,
|
offsetPath: `path("${pathData}")`,
|
||||||
offsetDistance: dot1Distance,
|
offsetDistance: dot1Distance,
|
||||||
@@ -273,8 +301,8 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
|
|||||||
|
|
||||||
{/* Secondary traveling dot */}
|
{/* Secondary traveling dot */}
|
||||||
<motion.circle
|
<motion.circle
|
||||||
r="1.5"
|
r="2"
|
||||||
fill="rgba(148, 163, 184, 0.4)"
|
fill="rgba(148, 163, 184, 0.6)"
|
||||||
style={{
|
style={{
|
||||||
offsetPath: `path("${pathData}")`,
|
offsetPath: `path("${pathData}")`,
|
||||||
offsetDistance: dot2Distance,
|
offsetDistance: dot2Distance,
|
||||||
@@ -282,7 +310,7 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Node markers at key positions */}
|
{/* Node markers at key positions */}
|
||||||
{[0.15, 0.33, 0.5, 0.67, 0.85].map((pos, i) => (
|
{[0.12, 0.28, 0.45, 0.62, 0.82].map((pos, i) => (
|
||||||
<NodeMarker
|
<NodeMarker
|
||||||
key={i}
|
key={i}
|
||||||
pos={pos}
|
pos={pos}
|
||||||
@@ -302,31 +330,134 @@ interface NodeMarkerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NodeMarker: React.FC<NodeMarkerProps> = ({ pos, pathData, progress }) => {
|
const NodeMarker: React.FC<NodeMarkerProps> = ({ pos, pathData, progress }) => {
|
||||||
const opacity = useTransform(
|
const isActive = useTransform(
|
||||||
progress,
|
progress,
|
||||||
[pos - 0.1, pos, pos + 0.05],
|
[pos - 0.05, pos, pos + 0.05],
|
||||||
[0, 1, 1]
|
[0, 1, 1]
|
||||||
);
|
);
|
||||||
|
|
||||||
const scale = useTransform(
|
const scale = useTransform(
|
||||||
progress,
|
progress,
|
||||||
[pos - 0.05, pos, pos + 0.1],
|
[pos - 0.05, pos, pos + 0.05],
|
||||||
[0.5, 1.2, 1]
|
[0.8, 1.4, 1]
|
||||||
|
);
|
||||||
|
|
||||||
|
const glowOpacity = useTransform(
|
||||||
|
progress,
|
||||||
|
[pos - 0.02, pos, pos + 0.02],
|
||||||
|
[0, 0.6, 0]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.circle
|
<g>
|
||||||
r="6"
|
<motion.circle
|
||||||
fill="white"
|
r="12"
|
||||||
stroke="rgba(148, 163, 184, 0.5)"
|
fill="rgba(148, 163, 184, 0.2)"
|
||||||
strokeWidth="2"
|
style={{
|
||||||
style={{
|
offsetPath: `path("${pathData}")`,
|
||||||
offsetPath: `path("${pathData}")`,
|
offsetDistance: `${pos * 100}%`,
|
||||||
offsetDistance: `${pos * 100}%`,
|
opacity: glowOpacity,
|
||||||
opacity,
|
scale: 2,
|
||||||
scale,
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<motion.circle
|
||||||
|
r="6"
|
||||||
|
fill="white"
|
||||||
|
stroke="rgba(148, 163, 184, 0.8)"
|
||||||
|
strokeWidth="2"
|
||||||
|
style={{
|
||||||
|
offsetPath: `path("${pathData}")`,
|
||||||
|
offsetDistance: `${pos * 100}%`,
|
||||||
|
opacity: isActive,
|
||||||
|
scale,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<motion.circle
|
||||||
|
r="2"
|
||||||
|
fill="rgba(148, 163, 184, 1)"
|
||||||
|
style={{
|
||||||
|
offsetPath: `path("${pathData}")`,
|
||||||
|
offsetDistance: `${pos * 100}%`,
|
||||||
|
opacity: isActive,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
id: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
life: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ParticleSystem: React.FC<{
|
||||||
|
pathRef: React.RefObject<SVGPathElement>;
|
||||||
|
progress: any;
|
||||||
|
pathLength: number;
|
||||||
|
}> = ({ pathRef, progress, pathLength }) => {
|
||||||
|
const [particles, setParticles] = useState<Particle[]>([]);
|
||||||
|
const lastPos = useRef({ x: 0, y: 0 });
|
||||||
|
const particleId = useRef(0);
|
||||||
|
|
||||||
|
useAnimationFrame((time, delta) => {
|
||||||
|
if (!pathRef.current || pathLength === 0) return;
|
||||||
|
|
||||||
|
// Get current position of the dot
|
||||||
|
const currentProgress = progress.get();
|
||||||
|
const point = pathRef.current.getPointAtLength(currentProgress * pathLength);
|
||||||
|
|
||||||
|
// Update and emit particles in one go
|
||||||
|
setParticles(prev => {
|
||||||
|
const updated = prev
|
||||||
|
.map(p => ({
|
||||||
|
...p,
|
||||||
|
x: p.x + p.vx,
|
||||||
|
y: p.y + p.vy,
|
||||||
|
life: p.life - 0.02,
|
||||||
|
}))
|
||||||
|
.filter(p => p.life > 0);
|
||||||
|
|
||||||
|
const dist = Math.hypot(point.x - lastPos.current.x, point.y - lastPos.current.y);
|
||||||
|
if (dist > 1) {
|
||||||
|
const newParticles: Particle[] = [];
|
||||||
|
const count = Math.min(3, Math.floor(dist / 2));
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
newParticles.push({
|
||||||
|
id: particleId.current++,
|
||||||
|
x: point.x,
|
||||||
|
y: point.y,
|
||||||
|
vx: (Math.random() - 0.5) * 2,
|
||||||
|
vy: (Math.random() - 0.5) * 2,
|
||||||
|
life: 1.0,
|
||||||
|
size: Math.random() * 3 + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
lastPos.current = { x: point.x, y: point.y };
|
||||||
|
return [...updated, ...newParticles].slice(-60);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
{particles.map(p => (
|
||||||
|
<circle
|
||||||
|
key={p.id}
|
||||||
|
cx={p.x}
|
||||||
|
cy={p.y}
|
||||||
|
r={p.size}
|
||||||
|
fill={`rgba(148, 163, 184, ${p.life * 0.5})`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user