# Hero 数字水墨动画实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 为 Hero 区域实现"数字水墨"Canvas 2D 粒子动画,传达"传统智慧 → 数字化转型"的品牌叙事。 **架构:** 新建 `InkDataMorph` Canvas 2D 粒子组件,三阶段状态机驱动(扩散 → 沉淀 → 数据化),集成到 `hero-section-v2.tsx` 中作为背景层,文字内容层在 Canvas 之上。 **技术栈:** React 18 + Canvas 2D API + requestAnimationFrame + Jest **规格文档:** `docs/superpowers/specs/2026-05-03-hero-ink-data-morph-design.md` --- ## 文件结构 | 操作 | 文件 | 职责 | |------|------|------| | 新建 | `src/components/effects/ink-data-morph.tsx` | 数字水墨粒子引擎组件 | | 新建 | `src/components/effects/ink-data-morph.test.tsx` | 组件单元测试 | | 修改 | `src/components/effects/index.ts` | 导出新组件 | | 修改 | `src/components/sections/hero-section-v2.tsx` | 集成 InkDataMorph + 调整背景色/延迟 | --- ### 任务 1:InkDataMorph 组件 — 粒子数据结构与初始化 **文件:** - 创建:`src/components/effects/ink-data-morph.tsx` - [ ] **步骤 1:创建组件骨架与粒子类型定义** 创建 `src/components/effects/ink-data-morph.tsx`,包含完整的类型定义和组件骨架: ```tsx 'use client'; import { useEffect, useRef, useCallback } from 'react'; import { useReducedMotion } from '@/hooks/use-reduced-motion'; interface InkDataMorphProps { particleCount?: number; primaryColor?: string; accentColor?: string; connectionColor?: string; connectionDistance?: number; className?: string; } interface Particle { x: number; y: number; vx: number; vy: number; radius: number; initialRadius: number; dataRadius: number; opacity: number; isAccent: boolean; phase: 'spreading' | 'settling' | 'morphing' | 'complete'; spreadTime: number; maxSpreadTime: number; settleTime: number; morphProgress: number; targetX: number; targetY: number; delay: number; age: number; } const DEFAULTS = { particleCount: 150, primaryColor: '#1C1C1C', accentColor: '#C41E3A', connectionColor: '#C41E3A', connectionDistance: 80, } as const; function easeOutQuart(t: number): number { return 1 - Math.pow(1 - t, 4); } function easeInOutCubic(t: number): number { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } function lerp(a: number, b: number, t: number): number { return a + (b - a) * t; } function hexToRgb(hex: string): string { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `${r}, ${g}, ${b}`; } export function InkDataMorph({ particleCount = DEFAULTS.particleCount, primaryColor = DEFAULTS.primaryColor, accentColor = DEFAULTS.accentColor, connectionColor = DEFAULTS.connectionColor, connectionDistance = DEFAULTS.connectionDistance, className = '', }: InkDataMorphProps) { const canvasRef = useRef(null); const particlesRef = useRef([]); const animationRef = useRef(undefined); const startTimeRef = useRef(0); const shouldReduceMotion = useReducedMotion(); const createParticle = useCallback(( cx: number, cy: number, W: number, H: number, delay: number, ): Particle => { const angle = Math.random() * Math.PI * 2; const dist = Math.random() * 10; const speed = 0.3 + Math.random() * 1.5; return { x: cx + Math.cos(angle) * dist, y: cy + Math.sin(angle) * dist, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, radius: 1.5 + Math.random() * 4, initialRadius: 1.5 + Math.random() * 4, dataRadius: 1 + Math.random() * 2, opacity: 0.4 + Math.random() * 0.4, isAccent: Math.random() > 0.75, phase: 'spreading', spreadTime: 0, maxSpreadTime: 60 + Math.random() * 80, settleTime: 0, morphProgress: 0, targetX: W * (0.55 + Math.random() * 0.35), targetY: H * (0.15 + Math.random() * 0.6), delay, age: 0, }; }, []); const initParticles = useCallback((W: number, H: number) => { const center1 = { x: W * 0.7, y: H * 0.35 }; const center2 = { x: W * 0.25, y: H * 0.65 }; const count1 = Math.floor(particleCount * 0.67); const count2 = particleCount - count1; const particles: Particle[] = []; for (let i = 0; i < count1; i++) { particles.push(createParticle(center1.x, center1.y, W, H, 0)); } for (let i = 0; i < count2; i++) { particles.push(createParticle(center2.x, center2.y, W, H, 30)); } particlesRef.current = particles; }, [particleCount, createParticle]); return (