Files
novalon-website/docs/superpowers/plans/2026-05-03-ink-wash-enhancement.md
T
2026-05-03 19:19:50 +08:00

786 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 墨韵增强实现计划
> **面向 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 使用