山形动画组件设计
程序生成的山形动画组件,使用 Web Worker + OffscreenCanvas 实现高性能渲染,支持高 DPR 设备的清晰显示。
🎯 概述与设计目标
组件定位
山形动画组件是一个高性能、可定制的程序化山形生成器,主要用于网页背景装饰或视觉效果展示。它基于分形噪声算法生成自然的山形轮廓,并通过滚动交互实现 3D 旋转效果。
核心问题
在实现过程中,需要解决以下关键挑战:
- 性能瓶颈:大量线段的实时渲染会阻塞主线程,导致页面卡顿
- 高分辨率适配:在 Retina/4K 屏幕上保持清晰显示
- 滚动流畅性:滚动时实时计算旋转角度,需要极低延迟
- 内存管理:缓存策略需要平衡内存占用和渲染性能
设计目标
| 目标 | 解决方案 |
| 不阻塞主线程 | Web Worker + OffscreenCanvas |
| 高分辨率清晰 | 自动 DPR 检测 + ctx.scale 变换 |
| 滚动即时响应 | ImageBitmap 预渲染缓存,O(1) 切换 |
| 代码可维护 | MVC 架构,职责分离 |
📁 文件结构
mountain/
├── README.md # 架构说明(本文件)
├── index.ts # 统一导出
│
├── canvas-mountain.tsx # React 组件(主线程)
├── mountain.worker.ts # Worker 调度器
│
├── mountain.ts # Mountain 数据模型类 ⭐️
├── mountain-renderer.ts # MountainRenderer 渲染器类 ⭐️
│
├── config.ts # 配置常量
├── types.ts # 类型定义
└── renderer.ts # Canvas 渲染函数特性一览
- ✅ 高性能渲染:Web Worker + OffscreenCanvas,不阻塞主线程
- ✅ 高 DPR 支持:自动检测设备像素比,在高分辨率屏幕上保持清晰
- ✅ 智能缓存:预渲染 41 个旋转角度的 ImageBitmap,滚动时 O(1) 切换
- ✅ 流畅动画:逐点增量渲染的初始动画,60 FPS 流畅体验
- ✅ 程序生成:基于 fBm 噪声的随机山形,每个 seed 生成独特图案
- ✅ 响应式设计:自动适配不同尺寸,保持 16:9 比例不变形
🏗️ 架构设计
核心理念
MVC 模式 + 关注点分离: - Model (Mountain 类):数据模型,负责数据生成和存储 - View (MountainRenderer 类):视图渲染器,负责渲染和动画 - Controller (Worker 调度器):控制器,负责消息路由
三层架构
设计思想
- MVC 模式:Model (Mountain) + View (MountainRenderer) + Controller (Worker)
- 单一职责:每个类只做一件事
- 数据与视图分离:Mountain 负责数据,MountainRenderer 负责渲染
- 面向对象:用类封装复杂的状态和行为
- 智能缓存:根据需要决定重新计算
- 性能优先:Worker + OffscreenCanvas + ImageBitmap
- 可维护性:清晰的文件结构和导出
架构演进
从最初的函数式编程到现在的 MVC 模式,架构经历了三个主要阶段的演进。
各阶段特点:
- 阶段 1 函数式:✅ 简单直观 ❌ 职责不清、难维护、Worker 臃肿
- 阶段 2 单一类:✅ 代码组织改善 ❌ 数据渲染混合、测试困难
- 阶段 3 MVC 模式:✅ 职责清晰、易测试、可复用、易扩展
📦 Mountain 类(数据模型)
核心方法
class Mountain {
// 构造函数
constructor(config: MountainConfig)
// 生成数据(异步)
async generate(): Promise<void>
// 获取数据(只读)
getPoints(): readonly Point[]
getLines(): readonly PrecomputedLine[][]
getDrawArea(): { drawWidth, drawHeight, offsetX, offsetY }
getRotationConstants(): RotationConstants | null
getPreRenderState(): PreRenderState | null
// 清理资源
dispose(): void
}数据生成流程
Mountain 类的 generate() 方法执行完整的数据生成流程,所有数据在这一步生成完毕。
详细步骤说明:
- 计算绘制区域:确保山形始终以 16:9 的比例显示,避免变形。
const aspectRatio = width / height; if (aspectRatio > 16 / 9) { // 宽度过大,裁剪左右 drawHeight = height; drawWidth = height * (16 / 9); offsetX = (width - drawWidth) / 2; } else { // 高度过大,裁剪上下 drawWidth = width; drawHeight = width / (16 / 9); offsetY = (height - drawHeight) / 2; }
性能特征:
| 步骤 | 典型耗时 | 主要开销 |
| 计算绘制区域 | < 1ms | 简单数学计算 |
| 生成 fBm 噪声 | < 1ms | 创建函数闭包 |
| 生成点数据 | 5-10ms | 噪声采样、数组操作 |
| 预计算线段 | 10-20ms | 大量线段计算、随机数生成 |
| 初始化预渲染 | < 1ms | 创建对象 |
| 总计 | 20-30ms | 一次性开销 |
📦 MountainRenderer 类(渲染器)
核心方法
class MountainRenderer {
// 静态方法:判断是否需要重新生成
static needsRegeneration(
oldConfig: MountainRendererConfig,
newConfig: MountainRendererConfig
): boolean
// 构造函数
constructor(
ctx: OffscreenCanvasRenderingContext2D,
canvasWidth: number,
canvasHeight: number,
config: MountainRendererConfig
)
// 初始化(创建 Mountain 并生成数据)
async initialize(): Promise<void>
// 开始动画
startAnimation(): void
// 停止动画
stopAnimation(): void
// 更新滚动进度
updateScrollProgress(progress: number): void
// 清理资源
dispose(): void
// 获取 Mountain 实例(调试用)
getMountain(): Mountain | null
}渲染模式
// 初始动画模式:逐点增量渲染
renderIncremental() {
drawMountainIncremental(ctx, lines, renderedPointsCount, ...)
}
// 高性能模式:使用 ImageBitmap 缓存
renderWithBitmapCache() {
const bitmap = preRenderState.bitmaps.get(nearestStep);
if (bitmap) {
ctx.drawImage(bitmap, 0, 0); // O(1) 操作
}
}🔄 消息流程
1. 初始化流程
当组件首次挂载时,主线程向 Worker 发送初始化消息。
2. 参数更新与数据生成
当组件的 props 发生变化时(如 seed、尺寸、偏移量),触发数据重新生成。
3. 滚动进度更新
用户滚动页面时,主线程实时同步滚动进度到 Worker。
4. 预渲染流程(后台)
初始动画完成后,Worker 在空闲时逐步预渲染各个旋转角度的 ImageBitmap。
🎨 渲染流程
初始动画(逐点渲染)
组件首次加载时,通过 RAF 循环逐点渲染,营造山形”生长”的动画效果。
关键代码片段:
private animationLoop = (): void => {
// 1. 增加已渲染点数
this.animationState.renderedPointsCount += this.animationState.animationSpeed;
// 2. 判断是否完成
if (this.animationState.renderedPointsCount >= totalPoints) {
this.animationState.renderedPointsCount = totalPoints;
this.animationState.isAnimating = false;
// 3. 渲染最后一帧
this.renderFrame();
this.logAnimationStats(currentTime, totalPoints);
self.postMessage({ type: "animationComplete" });
return;
}
// 4. 继续渲染
this.renderFrame();
this.animationState.animationRafId = requestAnimationFrame(this.animationLoop);
};渲染模式决策
根据当前状态(初始动画 vs 滚动)和缓存可用性,智能选择最优的渲染模式。
关键代码片段:
private renderFrame(): void {
const isFullyRendered = renderedPointsCount >= totalPoints;
// 模式 1: 高性能缓存模式(最快)
if (isFullyRendered && preRenderState?.bitmaps.size > 0) {
this.renderWithBitmapCache();
}
// 模式 2: 增量渲染模式(初始动画)
else {
this.renderIncremental();
}
}
private renderWithBitmapCache(): void {
const stepSize = 1 / ROTATION_CONFIG.CACHE_STEPS;
const nearestStep = Math.round(currentScrollProgress / stepSize) * stepSize;
const bitmap = preRenderState.bitmaps.get(nearestStep);
if (bitmap) {
// O(1) 操作:直接绘制 GPU 纹理
this.ctx.drawImage(bitmap, 0, 0);
} else {
// 降级到实时渲染,并优先预渲染这个角度
this.renderFull();
prioritizeAngle(nearestStep, preRenderState);
}
// 继续预渲染剩余角度
if (!preRenderState.isPreRendering && preRenderState.queue.length > 0) {
scheduleNextPreRender(preRenderState, this.createDrawMountainCallback());
}
}🖥️ 高 DPR 支持
为了在高分辨率屏幕(Retina、4K 等)上保持清晰的显示效果,组件实现了完整的 DPR(Device Pixel Ratio)支持。
DPR 处理原理
三层 DPR 处理
关键实现细节
- 主线程:Canvas 尺寸设置
// canvas-mountain.tsx const dpr = window.devicePixelRatio || 1; // 设置实际像素尺寸(高分辨率) canvas.width = width * dpr; canvas.height = height * dpr; // 设置 CSS 显示尺寸(保持界面大小) canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; // 传递给 Worker worker.postMessage({ type: "init", canvas: offscreen, dpr });
DPR 变化处理
支持动态 DPR 变化(例如拖动窗口到不同 DPI 的显示器):
// canvas-mountain.tsx - 参数更新时
useEffect(() => {
const dpr = window.devicePixelRatio || 1;
// 发送新的 DPR 给 Worker
worker.postMessage({
type: "updateParams",
width,
height,
dpr, // 可能变化的 DPR
...
});
}, [width, height, ...]);
// mountain.worker.ts - 处理 DPR 变化
if (newDpr !== dpr) {
dpr = newDpr;
// 调整 canvas 尺寸
canvas.width = width * dpr;
canvas.height = height * dpr;
// 重新应用缩放(调整尺寸会重置 transform)
ctx.scale(dpr, dpr);
}性能影响
| DPR | Canvas 像素数 | 内存占用 | 渲染性能 | 视觉效果 |
| 1.0 | 1× (基准) | 1× | 最快 | 普通 |
| 2.0 | 4× | 4× | 稍慢 (~10%) | 清晰 |
| 3.0 | 9× | 9× | 较慢 (~30%) | 极清晰 |
优化策略:
- ✅ 使用 ImageBitmap 缓存,缓存命中时 DPR 对性能影响极小
- ✅ GPU 加速的 drawImage 操作,即使是高分辨率也能快速绘制
- ✅ 初始动画完成后预渲染,避免实时渲染大量像素
🚀 性能优化
优化策略总览
1. 数据层面优化
点距离过滤:生成点时跳过距离过近的点,减少冗余数据。
动态线条数量:根据点的深度(y 坐标)动态调整线条数量,远处少、近处多,符合透视效果。
预计算:所有复杂计算在数据生成阶段完成,渲染时只需简单绘制。
- 线段起点、终点、颜色、缩放比例全部预计算
- 旋转系数(
MULTIPLIER_FACTOR,BASE_FACTOR)预计算 - 绘制区域(16:9 裁剪、居中偏移)预计算
2. 渲染层面优化
ImageBitmap 缓存:预渲染不同旋转角度为 GPU 纹理,滚动时直接复用。
性能对比:
| 渲染方式 | 时间复杂度 | 典型耗时 | 适用场景 |
| 实时渲染 | O(n) | 5-15ms | 初始动画、缓存未命中 |
| ImageBitmap 缓存 | O(1) | < 1ms | 滚动浏览(缓存命中) |
增量渲染:初始动画时,每帧只绘制新增的点,而不是重绘全部。
3. 架构层面优化
Web Worker + OffscreenCanvas:所有渲染在后台线程运行,不阻塞主线程用户交互。
智能重新生成:通过 needsRegeneration 判断是否需要重新生成数据,仅 speed/debug 变化时复用现有实例。
资源管理:自动清理 ImageBitmap,防止内存泄漏。
🔧 配置与调试
配置常量
所有配置常量在 config.ts 中:
NOISE_CONFIG: 噪声参数SCALE_CONFIG: 透视缩放SHADING_CONFIG: 阴影和颜色OPTIMIZATION_CONFIG: 性能优化POINT_GENERATION_CONFIG: 点生成ROTATION_CONFIG: 旋转效果
调试模式
启用 debug 模式可以看到详细的性能统计:
<CanvasMountain debug={true} ... />输出包括:点生成统计、线段数量统计、初始渲染性能(FPS、最长帧)、预渲染进度和性能。