docs: 添加墨韵增强实现计划

This commit is contained in:
张翔
2026-05-03 19:19:50 +08:00
parent b900abbe1d
commit 8f70f7021c
@@ -0,0 +1,785 @@
# 墨韵增强实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 升级 InkDataMorph 粒子束动画,实现4层墨韵晕染、不规则墨迹边缘、墨丝效果、宣纸纹理和飞白笔触,同时优化移动端表现。
**架构:** 在现有 Canvas 2D 粒子系统基础上,重构绘制管线——将单层径向渐变替换为4层晕染叠加,用极坐标噪声路径替代椭圆绘制,新增墨丝/飞白/宣纸纹理三个绘制层。响应式配置系统扩展新增字段以控制各效果的参数。
**技术栈:** React 19, Canvas 2D API, TypeScript, Jest
---
## 文件结构
| 文件 | 职责 | 操作 |
|------|------|------|
| `src/components/effects/ink-data-morph.tsx` | 核心动画组件,包含所有粒子逻辑和绘制 | 修改 |
| `src/components/effects/ink-data-morph.test.tsx` | 单元测试 | 修改 |
---
### 任务 1:扩展数据模型和响应式配置
**文件:**
- 修改:`src/components/effects/ink-data-morph.tsx:18-34` (Particle 接口)
- 修改:`src/components/effects/ink-data-morph.tsx:36-52` (ResponsiveConfig 接口)
- 修改:`src/components/effects/ink-data-morph.tsx:54-120` (getResponsiveConfig 函数)
- 修改:`src/components/effects/ink-data-morph.tsx:375-418` (createParticle 函数)
- [ ] **步骤 1:扩展 Particle 接口,新增6个字段**
在 Particle 接口中添加:
```typescript
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
radius: number;
initialRadius: number;
dataRadius: number;
opacity: number;
toneIndex: number;
rotation: number;
scaleX: number;
scaleY: number;
isSplash: boolean;
phase: 'spreading' | 'settling' | 'morphing' | 'complete' | 'fading';
spreadTime: number;
maxSpreadTime: number;
settleTime: number;
morphProgress: number;
targetX: number;
targetY: number;
delay: number;
age: number;
trail: TrailPoint[];
seed1: number;
seed2: number;
wobbleFactor: number;
prevVx: number;
prevVy: number;
inkLayerCount: number;
}
```
- [ ] **步骤 2:扩展 ResponsiveConfig 接口,新增6个字段**
在 ResponsiveConfig 接口中添加:
```typescript
interface ResponsiveConfig {
particleCount: number;
centers: { x: number; y: number }[];
centerDistribution: number[];
targetXRange: [number, number];
targetYRange: [number, number];
connectionDistance: number;
glowScale: number;
gradientLayers: number;
maxTrail: number;
splashRatio: number;
bgOrbScale: number;
inkLayers: number;
inkRadiusScale: number;
inkStringThreshold: number;
showFeibai: boolean;
paperTextureSize: number;
wobbleDetail: number;
}
```
- [ ] **步骤 3:更新 getResponsiveConfig 函数,为三个断点添加新字段**
移动端 (<768)
```typescript
inkLayers: 3,
inkRadiusScale: 1.2,
inkStringThreshold: 0.35,
showFeibai: false,
paperTextureSize: 128,
wobbleDetail: 16,
```
平板端 (768-1023)
```typescript
inkLayers: 3,
inkRadiusScale: 1.1,
inkStringThreshold: 0.4,
showFeibai: true,
paperTextureSize: 128,
wobbleDetail: 20,
```
桌面端 (≥1024)
```typescript
inkLayers: 4,
inkRadiusScale: 1.0,
inkStringThreshold: 0.5,
showFeibai: true,
paperTextureSize: 256,
wobbleDetail: 24,
```
同时更新三个断点的 particleCount:桌面 180、平板 120、移动 70。更新 maxTrail:桌面 6、平板 4、移动 2。
- [ ] **步骤 4:更新 createParticle 函数,初始化新字段**
在 createParticle 返回对象中添加:
```typescript
seed1: Math.random() * Math.PI * 2,
seed2: Math.random() * Math.PI * 2,
wobbleFactor: 0.3,
prevVx: 0,
prevVy: 0,
inkLayerCount: config.inkLayers,
```
- [ ] **步骤 5:运行 TypeScript 类型检查**
运行:`npx tsc --noEmit --pretty 2>&1 | head -30`
预期:无类型错误(新增字段在所有使用 Particle 的地方都需要初始化)
- [ ] **步骤 6Commit**
```bash
git add src/components/effects/ink-data-morph.tsx
git commit -m "refactor: 扩展 Particle 和 ResponsiveConfig 数据模型,新增墨韵增强字段"
```
---
### 任务 2:实现4层晕染绘制系统
**文件:**
- 修改:`src/components/effects/ink-data-morph.tsx` (drawParticle 函数,约 L260-340)
- [ ] **步骤 1:新增 drawInkBlob 辅助函数(极坐标噪声路径绘制)**
在 drawInkDot 函数之后添加:
```typescript
function drawInkBlob(
ctx: CanvasRenderingContext2D,
cx: number,
cy: number,
baseR: number,
seed1: number,
seed2: number,
wobble: number,
detail: number,
rgb: string,
opacity: number,
): void {
ctx.beginPath();
for (let i = 0; i <= detail; i++) {
const angle = (i / detail) * Math.PI * 2;
const noise = Math.sin(angle * 3 + seed1) * 0.3 + Math.cos(angle * 5 + seed2) * 0.2;
const r = Math.max(0.5, baseR * (1 + noise * wobble));
const x = cx + Math.cos(angle) * r;
const y = cy + Math.sin(angle) * r;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.fillStyle = `rgba(${rgb}, ${opacity})`;
ctx.fill();
}
```
- [ ] **步骤 2:新增 drawInkLayers 函数(4层晕染叠加)**
在 drawInkBlob 之后添加:
```typescript
function drawInkLayers(
ctx: CanvasRenderingContext2D,
p: Particle,
config: ResponsiveConfig,
): void {
const tone = TONES[p.toneIndex];
if (!tone) {
return;
}
const isAccent = ACCENT_TONES.includes(p.toneIndex);
const deepRgb = isAccent ? tone.rgb : (p.toneIndex <= 1 ? tone.rgb : TONES[0].rgb);
const lightRgb = isAccent ? TONES[1].rgb : (p.toneIndex >= 2 ? tone.rgb : TONES[2].rgb);
const r = Math.max(0.5, p.radius) * config.inkRadiusScale;
const op = p.opacity;
const detail = config.wobbleDetail;
const layers: Array<{ radiusMul: number; opacityMul: number; rgb: string; wobble: number; useRect: boolean }> = [
{ radiusMul: 5, opacityMul: 0.04, rgb: lightRgb, wobble: 0.25, useRect: true },
{ radiusMul: 3, opacityMul: 0.12, rgb: lightRgb, wobble: 0.2, useRect: true },
{ radiusMul: 1.5, opacityMul: 0.4, rgb: deepRgb, wobble: 0.12, useRect: false },
{ radiusMul: 1, opacityMul: 0.9, rgb: deepRgb, wobble: 0.06, useRect: false },
];
const layerCount = p.inkLayerCount;
const startIdx = layers.length - layerCount;
for (let i = startIdx; i < layers.length; i++) {
const layer = layers[i];
if (!layer) {
continue;
}
const layerR = r * layer.radiusMul;
const layerOp = op * layer.opacityMul;
if (layer.useRect) {
const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, layerR);
grad.addColorStop(0, `rgba(${layer.rgb}, ${layerOp})`);
grad.addColorStop(0.5, `rgba(${layer.rgb}, ${layerOp * 0.3})`);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.fillRect(p.x - layerR, p.y - layerR, layerR * 2, layerR * 2);
} else {
const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, layerR);
grad.addColorStop(0, `rgba(${layer.rgb}, ${layerOp})`);
grad.addColorStop(0.6, `rgba(${layer.rgb}, ${layerOp * 0.5})`);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(p.x, p.y, layerR, 0, Math.PI * 2);
ctx.fill();
drawInkBlob(ctx, p.x, p.y, layerR * 0.7, p.seed1, p.seed2, layer.wobble, detail, layer.rgb, layerOp * 0.6);
}
}
}
```
- [ ] **步骤 3:重构 drawParticle 函数,使用 drawInkLayers 替代旧的绘制逻辑**
将 drawParticle 函数中 spreading 和 settling 阶段的绘制逻辑替换为调用 drawInkLayers。保留 drawTrail 调用。fading 和 complete/morphing 阶段保持不变。
新的 drawParticle 函数:
```typescript
function drawParticle(
ctx: CanvasRenderingContext2D,
p: Particle,
time: number,
config: ResponsiveConfig,
): void {
if (p.age < p.delay || p.opacity <= 0) {
return;
}
const tone = TONES[p.toneIndex];
if (!tone) {
return;
}
const rgb = tone.rgb;
if (p.phase === 'spreading') {
drawTrail(ctx, p, rgb);
drawInkLayers(ctx, p, config);
} else if (p.phase === 'settling') {
drawInkLayers(ctx, p, config);
if (config.showFeibai && !p.isSplash) {
drawFeibai(ctx, p, rgb);
}
} else if (p.phase === 'fading') {
const r = Math.max(0.5, p.radius);
const grad1 = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r * 1.5);
grad1.addColorStop(0, `rgba(${rgb}, ${p.opacity * 0.6})`);
grad1.addColorStop(1, 'transparent');
ctx.fillStyle = grad1;
ctx.beginPath();
ctx.arc(p.x, p.y, r * 1.5, 0, Math.PI * 2);
ctx.fill();
} else if (p.phase === 'morphing') {
drawInkLayers(ctx, p, config);
} else {
const r = Math.max(0.5, p.radius);
const pulse = 1 + Math.sin(time * 0.002 + p.targetX * 0.01) * 0.15;
const glowR = r * 3 * pulse * config.glowScale;
const grad2 = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowR);
grad2.addColorStop(0, `rgba(${rgb}, ${p.opacity * 0.12})`);
grad2.addColorStop(0.4, `rgba(${rgb}, ${p.opacity * 0.04})`);
grad2.addColorStop(1, 'transparent');
ctx.fillStyle = grad2;
ctx.fillRect(p.x - glowR, p.y - glowR, glowR * 2, glowR * 2);
drawInkBlob(ctx, p.x, p.y, r, p.seed1, p.seed2, 0.08, config.wobbleDetail, rgb, p.opacity);
}
}
```
注意:此步骤中 `drawFeibai` 函数尚未实现,先声明一个空函数占位,任务3中实现。
- [ ] **步骤 4:添加 drawFeibai 占位函数**
```typescript
function drawFeibai(
ctx: CanvasRenderingContext2D,
p: Particle,
rgb: string,
): void {
}
```
- [ ] **步骤 5:删除不再使用的 drawInkDot 函数**
删除整个 drawInkDot 函数,因为已被 drawInkBlob 替代。
- [ ] **步骤 6:运行类型检查和 lint**
运行:`npx tsc --noEmit --pretty 2>&1 | head -30`
预期:无类型错误
运行:`npx eslint src/components/effects/ink-data-morph.tsx 2>&1 | head -20`
预期:无 lint 错误
- [ ] **步骤 7Commit**
```bash
git add src/components/effects/ink-data-morph.tsx
git commit -m "feat: 实现4层墨韵晕染系统和极坐标噪声墨迹边缘"
```
---
### 任务 3:实现飞白笔触效果
**文件:**
- 修改:`src/components/effects/ink-data-morph.tsx` (drawFeibai 函数 + updateParticle 函数)
- [ ] **步骤 1:更新 updateParticle 函数,记录 prevVx/prevVy 并更新 wobbleFactor**
在 updateParticle 函数的 spreading 阶段开头添加速度记录:
```typescript
if (p.phase === 'spreading') {
p.prevVx = p.vx;
p.prevVy = p.vy;
const spreadRatio = p.spreadTime / p.maxSpreadTime;
p.wobbleFactor = lerp(0.3, 0.15, spreadRatio);
// ... 其余 spreading 逻辑不变
}
```
- [ ] **步骤 2:实现 drawFeibai 函数**
替换占位函数为完整实现:
```typescript
function drawFeibai(
ctx: CanvasRenderingContext2D,
p: Particle,
rgb: string,
): void {
const speed = Math.sqrt(p.prevVx * p.prevVx + p.prevVy * p.prevVy);
if (speed < 0.1) {
return;
}
const dirX = -p.prevVx / speed;
const dirY = -p.prevVy / speed;
const length = p.radius * (3 + Math.random() * 2);
const endX = p.x + dirX * length;
const endY = p.y + dirY * length;
const perpX = -dirY;
const perpY = dirX;
const curvature = (Math.random() - 0.5) * length * 0.15;
const cpX = (p.x + endX) / 2 + perpX * curvature;
const cpY = (p.y + endY) / 2 + perpY * curvature;
const grad = ctx.createLinearGradient(p.x, p.y, endX, endY);
grad.addColorStop(0, `rgba(${rgb}, ${p.opacity * 0.25})`);
grad.addColorStop(1, 'transparent');
ctx.save();
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.quadraticCurveTo(cpX, cpY, endX, endY);
ctx.strokeStyle = grad;
ctx.lineWidth = Math.max(0.3, p.radius * 0.6);
ctx.stroke();
ctx.restore();
}
```
- [ ] **步骤 3:运行类型检查**
运行:`npx tsc --noEmit --pretty 2>&1 | head -30`
预期:无类型错误
- [ ] **步骤 4Commit**
```bash
git add src/components/effects/ink-data-morph.tsx
git commit -m "feat: 实现飞白笔触效果,settling阶段沿运动反方向绘制渐变笔触"
```
---
### 任务 4:实现墨丝效果
**文件:**
- 修改:`src/components/effects/ink-data-morph.tsx` (新增 drawInkStrings 函数 + animate 循环)
- [ ] **步骤 1:新增 drawInkStrings 函数**
在 drawConnections 函数之后添加:
```typescript
function drawInkStrings(
ctx: CanvasRenderingContext2D,
particles: Particle[],
config: ResponsiveConfig,
): void {
const spreading = particles.filter(
p => p.phase === 'spreading' && p.age >= p.delay
);
const threshold = config.connectionDistance * config.inkStringThreshold;
ctx.save();
ctx.lineCap = 'round';
for (let i = 0; i < spreading.length; i++) {
const a = spreading[i];
if (!a) {
continue;
}
const toneA = TONES[a.toneIndex];
if (!toneA) {
continue;
}
for (let j = i + 1; j < spreading.length; j++) {
const b = spreading[j];
if (!b) {
continue;
}
const toneB = TONES[b.toneIndex];
if (!toneB) {
continue;
}
const dx = a.x - b.x;
const dy = a.y - b.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < threshold && dist > 0) {
const distRatio = 1 - dist / threshold;
const alpha = distRatio * 0.15 * Math.min(a.opacity, b.opacity);
const midWidth = 0.5 + distRatio * 1.5;
const mx = (a.x + b.x) / 2;
const my = (a.y + b.y) / 2;
const offset = dist * 0.08;
const cpx = mx + (dy / dist) * offset;
const cpy = my - (dx / dist) * offset;
const grad = ctx.createLinearGradient(a.x, a.y, b.x, b.y);
grad.addColorStop(0, `rgba(${toneA.rgb}, ${alpha * 0.3})`);
grad.addColorStop(0.5, `rgba(${toneA.rgb}, ${alpha})`);
grad.addColorStop(1, `rgba(${toneB.rgb}, ${alpha * 0.3})`);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.quadraticCurveTo(cpx, cpy, b.x, b.y);
ctx.strokeStyle = grad;
ctx.lineWidth = midWidth;
ctx.stroke();
}
}
}
ctx.restore();
}
```
- [ ] **步骤 2:在 animate 循环中调用 drawInkStrings**
在 animate 函数中,`drawBgOrbs` 之后、粒子绘制循环之前,添加:
```typescript
drawInkStrings(ctx, particles, config);
```
- [ ] **步骤 3:运行类型检查**
运行:`npx tsc --noEmit --pretty 2>&1 | head -30`
预期:无类型错误
- [ ] **步骤 4Commit**
```bash
git add src/components/effects/ink-data-morph.tsx
git commit -m "feat: 实现墨丝效果,spreading阶段近距离粒子间绘制渐变墨丝"
```
---
### 任务 5:实现宣纸纹理叠加
**文件:**
- 修改:`src/components/effects/ink-data-morph.tsx` (新增 generatePaperTexture 函数 + 组件内 textureRef + animate 循环)
- [ ] **步骤 1:新增 generatePaperTexture 函数**
在 drawBgOrbs 函数之后添加:
```typescript
function generatePaperTexture(size: number): HTMLCanvasElement {
const offscreen = document.createElement('canvas');
offscreen.width = size;
offscreen.height = size;
const octx = offscreen.getContext('2d');
if (!octx) {
return offscreen;
}
octx.fillStyle = '#FAFAF5';
octx.fillRect(0, 0, size, size);
for (let i = 0; i < size * size * 0.15; i++) {
const x = Math.random() * size;
const y = Math.random() * size;
const gray = 200 + Math.floor(Math.random() * 40);
const alpha = 0.02 + Math.random() * 0.04;
octx.fillStyle = `rgba(${gray}, ${gray}, ${gray}, ${alpha})`;
octx.fillRect(x, y, 1, 1);
}
octx.strokeStyle = 'rgba(180, 175, 168, 0.015)';
octx.lineWidth = 0.5;
for (let i = 0; i < size * 0.3; i++) {
const y = Math.random() * size;
const startX = Math.random() * size * 0.3;
const endX = startX + size * (0.1 + Math.random() * 0.4);
octx.beginPath();
octx.moveTo(startX, y);
octx.lineTo(Math.min(endX, size), y + (Math.random() - 0.5) * 2);
octx.stroke();
}
return offscreen;
}
```
- [ ] **步骤 2:新增 drawPaperTexture 函数**
```typescript
function drawPaperTexture(
ctx: CanvasRenderingContext2D,
W: number,
H: number,
texture: HTMLCanvasElement,
): void {
ctx.save();
ctx.globalCompositeOperation = 'overlay';
const pattern = ctx.createPattern(texture, 'repeat');
if (pattern) {
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, W, H);
}
ctx.globalCompositeOperation = 'source-over';
ctx.restore();
}
```
- [ ] **步骤 3:在组件中添加 textureRef**
在组件内部,与其他 ref 一起添加:
```typescript
const textureRef = useRef<HTMLCanvasElement | null>(null);
```
- [ ] **步骤 4:在 initCanvas 中生成纹理**
在 initCanvas 函数中,`const config = getResponsiveConfig(W);` 之后添加:
```typescript
textureRef.current = generatePaperTexture(config.paperTextureSize);
```
- [ ] **步骤 5:在 animate 循环末尾调用 drawPaperTexture**
在 animate 函数中,`drawConnections` 之后添加:
```typescript
if (textureRef.current) {
drawPaperTexture(ctx, W, H, textureRef.current);
}
```
- [ ] **步骤 6:在 drawStaticFinal 中也调用 drawPaperTexture**
在 drawStaticFinal 函数中,`drawConnections` 之后添加同样的纹理绘制代码。
- [ ] **步骤 7:运行类型检查和 lint**
运行:`npx tsc --noEmit --pretty 2>&1 | head -30`
预期:无类型错误
- [ ] **步骤 8Commit**
```bash
git add src/components/effects/ink-data-morph.tsx
git commit -m "feat: 实现宣纸纹理叠加,使用OffscreenCanvas生成一次性噪声纹理"
```
---
### 任务 6:更新单元测试
**文件:**
- 修改:`src/components/effects/ink-data-morph.test.tsx`
- [ ] **步骤 1:更新 MockCanvasContext,添加缺失的 mock 方法**
在 MockCanvasContext 类中添加:
```typescript
closePath = jest.fn();
globalCompositeOperation = 'source-over';
createPattern = jest.fn(() => ({}));
createLinearGradient = jest.fn(() => ({
addColorStop: jest.fn(),
}));
```
- [ ] **步骤 2:添加晕染层数相关测试**
在 describe('Responsive behavior') 块中添加:
```typescript
it('should render on mobile viewport with reduced effects', () => {
const { container } = render(
<div style={{ width: 375, height: 667 }}>
<InkDataMorph />
</div>
);
const canvas = container.querySelector('canvas');
expect(canvas).toBeInTheDocument();
});
it('should render on tablet viewport', () => {
const { container } = render(
<div style={{ width: 900, height: 600 }}>
<InkDataMorph />
</div>
);
const canvas = container.querySelector('canvas');
expect(canvas).toBeInTheDocument();
});
it('should render on desktop viewport with full effects', () => {
const { container } = render(
<div style={{ width: 1440, height: 900 }}>
<InkDataMorph />
</div>
);
const canvas = container.querySelector('canvas');
expect(canvas).toBeInTheDocument();
});
```
- [ ] **步骤 3:运行测试**
运行:`npx jest src/components/effects/ink-data-morph.test.tsx --no-coverage 2>&1`
预期:所有测试通过
- [ ] **步骤 4Commit**
```bash
git add src/components/effects/ink-data-morph.test.tsx
git commit -m "test: 更新InkDataMorph单元测试,适配墨韵增强后的组件"
```
---
### 任务 7:视觉验证与性能检查
**文件:** 无代码修改
- [ ] **步骤 1:启动开发服务器**
运行:`npm run dev`
- [ ] **步骤 2:桌面端视觉验证**
在浏览器中打开 http://localhost:3000,观察:
- 粒子是否呈现4层墨韵晕染效果(核心→浓墨→淡墨→水渍)
- 粒子边缘是否不规则(墨迹形态而非圆点)
- spreading 阶段是否有墨丝连接
- 背景是否有宣纸纹理质感
- settling 阶段是否有飞白笔触
- morphing → complete 阶段过渡是否自然
- [ ] **步骤 3:移动端视觉验证**
使用浏览器开发者工具切换到移动端视口(375×667),观察:
- 粒子虽少但视觉表现力是否充足
- 晕染半径是否比桌面端更大(1.2倍系数)
- 墨丝效果是否保留
- 无飞白效果(移动端 showFeibai: false
- 宣纸纹理是否可见
- [ ] **步骤 4:性能检查**
在桌面端和移动端分别打开 Chrome DevTools Performance 面板,录制5秒动画:
- 桌面端帧率 ≥ 50fps
- 移动端帧率 ≥ 30fps
如果帧率不达标,需要优化:
- 减少 wobbleDetail(桌面 20、移动 12
- 减少 inkLayers(移动端降为 2
- 增大 inkStringThreshold(减少墨丝绘制数量)
- [ ] **步骤 5:截图对比**
桌面端截图保存到 /tmp/hero-ink-wash-desktop.png
移动端截图保存到 /tmp/hero-ink-wash-mobile.png
- [ ] **步骤 6:最终 Commit(如有微调)**
```bash
git add -A
git commit -m "fix: 墨韵增强视觉微调与性能优化"
```
---
## 自检
### 规格覆盖度
| 规格需求 | 对应任务 |
|---------|---------|
| 4层晕染系统 | 任务2 (drawInkLayers) |
| 不规则墨迹边缘 | 任务2 (drawInkBlob) |
| 墨丝效果 | 任务4 (drawInkStrings) |
| 宣纸纹理叠加 | 任务5 (generatePaperTexture + drawPaperTexture) |
| 飞白笔触 | 任务3 (drawFeibai) |
| 移动端适配 | 任务1 (ResponsiveConfig 新字段) |
| Particle 新增字段 | 任务1 |
| ResponsiveConfig 新增字段 | 任务1 |
| 性能考量 | 任务7 (验证步骤) |
| 测试更新 | 任务6 |
### 占位符扫描
无 TODO/待定/后续实现。所有函数都有完整实现代码。
### 类型一致性
- Particle.seed1/seed2/wobbleFactor/prevVx/prevVy/inkLayerCount 在任务1定义,任务2/3/4使用
- ResponsiveConfig.inkLayers/inkRadiusScale/inkStringThreshold/showFeibai/paperTextureSize/wobbleDetail 在任务1定义,任务2/3/4/5使用
- drawInkBlob 在任务2定义,任务2和任务2的 drawParticle 使用
- drawInkLayers 在任务2定义,任务2的 drawParticle 使用
- drawFeibai 在任务3定义,任务2的 drawParticle 使用
- drawInkStrings 在任务4定义,任务4的 animate 循环使用
- generatePaperTexture/drawPaperTexture 在任务5定义,任务5的 initCanvas/animate 使用