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

22 KiB
Raw Permalink Blame History

墨韵增强实现计划

面向 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 接口中添加:

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 接口中添加:

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)

inkLayers: 3,
inkRadiusScale: 1.2,
inkStringThreshold: 0.35,
showFeibai: false,
paperTextureSize: 128,
wobbleDetail: 16,

平板端 (768-1023)

inkLayers: 3,
inkRadiusScale: 1.1,
inkStringThreshold: 0.4,
showFeibai: true,
paperTextureSize: 128,
wobbleDetail: 20,

桌面端 (≥1024)

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 返回对象中添加:

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
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 函数之后添加:

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 之后添加:

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 函数:

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 占位函数
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
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 阶段开头添加速度记录:

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 函数

替换占位函数为完整实现:

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
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 函数之后添加:

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 之后、粒子绘制循环之前,添加:

drawInkStrings(ctx, particles, config);
  • 步骤 3:运行类型检查

运行:npx tsc --noEmit --pretty 2>&1 | head -30 预期:无类型错误

  • 步骤 4Commit
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 函数之后添加:

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 函数
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 一起添加:

const textureRef = useRef<HTMLCanvasElement | null>(null);
  • 步骤 4:在 initCanvas 中生成纹理

在 initCanvas 函数中,const config = getResponsiveConfig(W); 之后添加:

textureRef.current = generatePaperTexture(config.paperTextureSize);
  • 步骤 5:在 animate 循环末尾调用 drawPaperTexture

在 animate 函数中,drawConnections 之后添加:

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
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 类中添加:

closePath = jest.fn();
globalCompositeOperation = 'source-over';
createPattern = jest.fn(() => ({}));
createLinearGradient = jest.fn(() => ({
  addColorStop: jest.fn(),
}));
  • 步骤 2:添加晕染层数相关测试

在 describe('Responsive behavior') 块中添加:

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
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(如有微调)
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 使用