科赫雪花(Koch Snowflake)是经典的分形几何图形,由 Helge von Koch 在 1904 年提出。它通过不断递归生成边上的“锯齿”,形成复杂而美丽的雪花形状。本文将介绍如何用 函数式编程封装科赫雪花生成与绘制,方便在 Canvas 上渲染,同时可封装成可复用的模块。
1. 算法原理
科赫雪花的生成规则非常直观:
- 从一个等边三角形开始。
- 将每条边分为三等份,并在中间段向外凸出一个等边小三角形。
- 对生成的新边重复步骤 2,递归指定层数。
- 最终得到雪花状的复杂轮廓。
特点:
- 每增加一层递归,边数呈指数增长。
- 轮廓越来越复杂,但数学公式可精确描述。
2. 函数式封装设计
在传统实现中,可能使用类或对象存储状态。函数式封装的思路是 只依赖纯函数和数组操作,保证雪花点集可重复使用,同时避免副作用。
核心函数
koch(p1, p2, iter)
:递归生成科赫曲线上的点createKochSnowflake(size, iterations)
:生成雪花点集和绘制函数draw(ctx, x, y, strokeStyle, lineWidth)
:将雪花绘制到 Canvas
3. 完整函数实现
export const createKochSnowflake = function(size = 200, iterations = 4) {
// 参数校验
if (typeof size !== "number" || size <= 0) {
throw new Error("[KochSnowflake] 参数错误: size 必须是大于 0 的数字");
}
if (!Number.isInteger(iterations) || iterations < 0) {
throw new Error("[KochSnowflake] 参数错误: iterations 必须是大于等于 0 的整数");
}
// 科赫递归函数
const koch = (p1, p2, iter) => iter === 0
? [p1, p2]
: (() => {
const dx = (p2.x - p1.x) / 3;
const dy = (p2.y - p1.y) / 3;
const pa = {x: p1.x + dx, y: p1.y + dy};
const pb = {x: p1.x + 2*dx, y: p1.y + 2*dy};
const angle = Math.PI / 3;
const peak = {
x: pa.x + Math.cos(angle) * (pb.x - pa.x) - Math.sin(angle) * (pb.y - pa.y),
y: pa.y + Math.sin(angle) * (pb.x - pa.x) + Math.cos(angle) * (pb.y - pa.y)
};
return [
...koch(p1, pa, iter - 1).slice(0, -1),
...koch(pa, peak, iter - 1).slice(0, -1),
...koch(peak, pb, iter - 1).slice(0, -1),
...koch(pb, p2, iter - 1)
];
})();
// 初始等边三角形(以原点为中心)
const h = size * Math.sqrt(3) / 2;
const triangle = [
{x: -size/2, y: h/3},
{x: size/2, y: h/3},
{x: 0, y: -2*h/3}
];
// 生成雪花点集
const points = triangle.flatMap((p, i) =>
koch(p, triangle[(i+1)%3], iterations).slice(0, -1)
).concat([triangle[0]]);
// 返回雪花句柄
return {
points,
/**
* 绘制雪花
* @param {CanvasRenderingContext2D} ctx - Canvas 上下文
* @param {number} x - 中心 X 坐标
* @param {number} y - 中心 Y 坐标
* @param {string} strokeStyle - 线条颜色
* @param {number} lineWidth - 线宽
*/
draw(ctx, x, y, strokeStyle = "white", lineWidth = 2) {
if (!ctx || typeof ctx.moveTo !== "function") {
throw new Error("[KochSnowflake] ctx 必须是 CanvasRenderingContext2D");
}
if (typeof x !== "number" || typeof y !== "number") {
throw new Error("[KochSnowflake] x, y 必须是数字");
}
if (typeof strokeStyle !== "string") {
throw new Error("[KochSnowflake] strokeStyle 必须是字符串");
}
if (typeof lineWidth !== "number" || lineWidth <= 0) {
throw new Error("[KochSnowflake] lineWidth 必须大于 0");
}
ctx.beginPath();
ctx.moveTo(points[0].x + x, points[0].y + y);
points.slice(1).forEach(p => ctx.lineTo(p.x + x, p.y + y));
ctx.closePath();
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
};
};
4. 使用示例
<canvas id="canvas" width="500" height="500"></canvas>
<script type="module">
import { createKochSnowflake } from './koch-snowflake.js';
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// 创建雪花句柄
const snowflake = createKochSnowflake(150, 4);
// 绘制在不同位置
snowflake.draw(ctx, 150, 150, "cyan", 2);
snowflake.draw(ctx, 350, 150, "yellow", 2);
snowflake.draw(ctx, 250, 350, "white", 2);
</script>
5. 优势与扩展
- 函数式封装:避免副作用,易于测试
- 性能优化:雪花点只计算一次,可多次绘制
- 参数验证:减少调用错误
- 可扩展性:未来可增加旋转、缩放、3D 投影或动画
6. 总结
通过函数式封装科赫雪花,我们实现了:
- 简洁、可维护的代码结构
- 高性能绘制,可重复使用
- 易于封装成 NPM 模块或在前端项目中复用
这种方法不仅适用于科赫雪花,也适合其他递归分形图形的封装,实现算法与渲染解耦,方便扩展和复用。