前言
小时候冬天在窗边呵气,玻璃上的霜花总是长成对称的树状图案------一片叶子分出更小的叶子,小叶子再分出更小的,无穷无尽。后来才知道,那种"自己像自己"的结构在数学里叫分形。分形里最出名的一张脸,就是科赫雪花。它的画法简单到三句话就能说清,却能在有限面积里挤出无限长度的边界线,像一桩完美的数学魔术。
我一直想在手机屏幕上亲手把这片雪花"种"出来------不是贴一张静态图片,而是用滑块一层层控制递归深度,看它从三角形变成六角星,再慢慢长出越来越细的毛刺。打开 DevEco Studio 6.1.1 Beta1,对着 Pura X Max 模拟器的屏幕,用 Canvas 递归绘制,一片蓝色的科赫雪花就这么在模拟器里飘起来了。这篇文章会把整个过程掰开讲:科赫曲线怎么从一条线段长出来、递归函数怎么写、坐标和旋转怎么算、Canvas 怎么把几千条线段一帧画完。代码也给全,复制进去就能跑。

一、分形和那条"太长的海岸线"
1967 年,数学家曼德博在《科学》杂志上发了篇论文,标题叫《英国海岸线有多长》。他说:你拿一把一公里长的尺子去量海岸线,得一个数;换一把一百米长的尺子,弯弯绕绕的小海湾就被测出来了,长度飙升。尺子越短,测出的海岸线越长,最后趋向无穷。这条反直觉的结论开启了分形几何的大门。
分形的核心特征就是自相似------整体和局部长得一样。一棵树的主干分出几根枝,每根枝又像一棵小树;山脉的轮廓放大后,和远观的轮廓差不多。科赫曲线就是这种自相似的教科书级案例。
1904 年,瑞典数学家科赫发明了这条曲线。画法很简单:
- 画一条线段。
- 把线段等分成三段。
- 中间那段用一个等边三角形的另外两条边替代(形成一个凸起)。
- 对得到的四条线段,每条都重复这个过程。
无限重复下去,就得到一条处处连续但处处不可导的曲线------它填不满平面,却在有限区域内无限长。把三条科赫曲线头尾相接,围成一个等边三角形,就是科赫雪花。每增加一层递归,雪花的边长就乘以 4/3,面积却始终小于初始三角形外接圆,最终周长趋向无穷,面积却是收敛的。这就是分形最迷人的地方:有限面积里装着无限周长。
二、把线段变成雪花的公式------三等分、旋转、拼接
计算机画科赫曲线,就是把上面的几何步骤翻译成坐标计算。假设有两个点 A 和 B,我们要在它们之间生成深度为 N 的科赫曲线。
先看最基本的一步变换:给定线段 AB,算出它的两个三等分点 C 和 D,再算出凸起顶点 E。
用向量来算最省事。设 A = (x1, y1),B = (x2, y2)。向量 AB = (dx, dy) = (x2 - x1, y2 - y1)。
三等分点:
C = A + (1/3) * AB = (x1 + dx/3, y1 + dy/3)
D = A + (2/3) * AB = (x1 + 2*dx/3, y1 + 2*dy/3)
接下来是 E 点。E 是以 CD 为底边的等边三角形的第三个顶点,凸起方向朝外。从 C 出发,向量 CD = AB/3。将 CD 逆时针旋转 60 度(或者顺时针,取决于想让雪花往外凸还是往里凹。对于逆时针画出的等边三角形,每条边向外凸需要逆时针旋转 60 度)。旋转公式:
(x', y') = (x * cos60° - y * sin60°, x * sin60° + y * cos60°)
其中 cos60° = 0.5,sin60° = √3/2 ≈ 0.8660254。
所以 E = C + 旋转后的向量。
把这些公式写成 TypeScript 就是:
function getKochPoints(ax: number, ay: number, bx: number, by: number): [number, number][] {
const dx = bx - ax;
const dy = by - ay;
const cx = ax + dx / 3;
const cy = ay + dy / 3;
const dx = cx + (dx / 3) * 0.5 - (dy / 3) * Math.sqrt(3) / 2;
const ey = cy + (dx / 3) * Math.sqrt(3) / 2 + (dy / 3) * 0.5;
const dx2 = ax + 2 * dx / 3;
const dy2 = ay + 2 * dy / 3;
return [[cx, cy], [ex, ey], [dx2, dy2]];
}

单层变换后,原来的线段 AB 被替换为四段:A→C、C→E、E→D、D→B。递归地,每一段再做同样的分裂,直到深度降到 0,就直接连接两点。
这就是递归算法的基础。代码实现时,我们可以用递归函数直接把最终的所有顶点按顺序收集到一个数组里,然后一次性画出来。
三、用递归函数收割所有顶点
递归函数的结构可以这样设计:给定起点 A、终点 B 和当前深度,如果深度为 0,就把 A 和 B 加入结果列表;否则计算 C、E、D,然后递归调用处理 A→C、C→E、E→D、D→B,每段深度减一。
为了避免重复点,我们约定:处理一段线段时,只把起点加入列表,终点由下一段或最后的闭合来处理。具体做法:

function kochCurve(ax: number, ay: number, bx: number, by: number, depth: number, points: number[][]): void {
if (depth === 0) {
points.push([ax, ay], [bx, by]);
return;
}
// 计算 C, E, D
// ...
kochCurve(ax, ay, cx, cy, depth - 1, points);
kochCurve(cx, cy, ex, ey, depth - 1, points);
kochCurve(ex, ey, dx, dy, depth - 1, points);
kochCurve(dx, dy, bx, by, depth - 1, points);
}
这样产生的点列表会有重复的内部点(比如 C 既是第一段的终点又是第二段的起点)。画图时这些重复点不影响结果,Canvas 的 lineTo 重叠一下毫无问题。如果想精简,可以在递归里稍作处理,但为了代码清晰,保持简单即可。
初始雪花由等边三角形的三条边组成。先算好三角形的三个顶点 P1(顶)、P2(左下)、P3(右下)。然后对每条边调用 kochCurve,把所有点拼成一个闭合多边形。注意第一条边要把 P1 加入起点,最后一条边结束时要回到 P1 闭合。
得到的点数组可能有几千个点(深度 6 时约 3×4^6 = 12288 个点),对手机的 Canvas 来说完全不是问题,模拟器上也能瞬间画完。
四、在 HarmonyOS 的 Canvas 上把点连成雪花
HarmonyOS 的 Canvas 组件通过 onReady 回调给我们一个 CanvasRenderingContext2D 对象。在 SDK22 下,导入路径是:
import { CanvasRenderingContext2D } from '@ohos.graphics.canvas';
拿到上下文后,我们可以用 ctx.canvas.width 和 ctx.canvas.height 获取画布的实际像素尺寸,然后根据画布大小计算等边三角形的边长和中心坐标,让雪花自适应居中。
绘制步骤:
clearRect清空画布。- 可选:画一个浅色背景或网格,增加视觉舒适度。
- 调用递归函数生成点列表。
- 用
ctx.beginPath()开始一条新路径,moveTo到第一个点,然后循环lineTo连接所有点,最后closePath()闭合。 - 设置
strokeStyle(比如冰蓝色)、lineWidth,然后stroke。
另外,为了提高辨识度,我加了一个填充色:给雪花内部填充一层很淡的蓝色,边框用深一点的蓝线描边。这样雪花看起来更立体,像一块冰晶。
为了让雪花更好看,还可以在递归前判断雪花的方向(确保凸起朝外)。三角形顶点顺序为逆时针,向量旋转 +60 度正好让凸起指向外侧。这点在数学上是自然的:逆时针多边形,每边向左侧旋转 60 度就朝外。所以我们无需额外调整。
五、用滑块控制深度------从三角到雪花
在界面上放一个 Slider,绑定 @State depth: number,最小值 0,最大值 6,步长 1。每次滑块值变化,就重新计算点列表并重绘画布。
深度 0 时,就是原始的等边三角形。深度 1 时,三角形每条边中间凸起一个小三角形,整个图形变成六角星形(大卫之星)。深度 2 时,星星的每条边上又长出更小的凸起,以此类推。深度 6 时,雪花的轮廓已经非常复杂,充满了细密的锯齿,像是霜花在显微镜下的模样。
给用户一些提示:显示当前深度、总共生成的点数(等于 3×4^深度 + 1),让用户直观感受分形的爆炸增长。
整个界面从上到下:标题、画布、深度显示和滑块、底部一句小知识(周长无限,面积有限)。配色选清凉的蓝白色系,贴合雪花主题。
六、完整代码------整片雪花都在一个文件里
以下是能在 DevEco Studio 6.1.1 Beta1 上直接跑的代码。新建 Empty Ability 项目,把 entry/src/main/ets/pages/Index.ets 全选替换即可。不需要权限,不用改 module.json5。

/*
* 科赫雪花分形绘制 --- 递归生成
* 功能:滑块调节递归深度,Canvas 绘制科赫雪花曲线
* 环境:DevEco Studio 6.1.1 Beta1,Pura X Max 模拟器,SDK22
*/
import { CanvasRenderingContext2D } from '@ohos.graphics.canvas';
@Entry
@Component
struct Index {
@State depth: number = 3; // 递归深度,默认3
@State pointCount: number = 0; // 生成的点数量
private ctx: CanvasRenderingContext2D | null = null;
private canvasWidth: number = 0;
private canvasHeight: number = 0;
// Canvas 就绪
private onCanvasReady(ctx: CanvasRenderingContext2D): void {
this.ctx = ctx;
this.canvasWidth = ctx.canvas.width;
this.canvasHeight = ctx.canvas.height;
this.drawSnowflake();
}
// 生成科赫曲线点集(递归)
private kochCurve(
ax: number, ay: number,
bx: number, by: number,
depth: number,
points: number[][]
): void {
if (depth === 0) {
points.push([ax, ay], [bx, by]);
return;
}
const dx = bx - ax;
const dy = by - ay;
const cx = ax + dx / 3;
const cy = ay + dy / 3;
const dx2 = ax + 2 * dx / 3;
const dy2 = ay + 2 * dy / 3;
// 计算凸起顶点 E
const midX = dx / 3;
const midY = dy / 3;
const cos60 = 0.5;
const sin60 = Math.sqrt(3) / 2;
const ex = cx + midX * cos60 - midY * sin60;
const ey = cy + midX * sin60 + midY * cos60;
// 递归四段
this.kochCurve(ax, ay, cx, cy, depth - 1, points);
this.kochCurve(cx, cy, ex, ey, depth - 1, points);
this.kochCurve(ex, ey, dx2, dy2, depth - 1, points);
this.kochCurve(dx2, dy2, bx, by, depth - 1, points);
}
// 生成雪花完整路径点
private generateSnowflakePoints(): number[][] {
const w = this.canvasWidth;
const h = this.canvasHeight;
// 等边三角形中心
const cx = w / 2;
const cy = h / 2;
const side = Math.min(w, h) * 0.75;
const height = side * Math.sqrt(3) / 2;
// 三个顶点(逆时针)
const p1x = cx;
const p1y = cy - height / 2; // 上
const p2x = cx - side / 2;
const p2y = cy + height / 2; // 左下
const p3x = cx + side / 2;
const p3y = cy + height / 2; // 右下
const points: number[][] = [];
// 三边递归
this.kochCurve(p1x, p1y, p2x, p2y, this.depth, points);
this.kochCurve(p2x, p2y, p3x, p3y, this.depth, points);
this.kochCurve(p3x, p3y, p1x, p1y, this.depth, points);
return points;
}
// 绘制雪花
private drawSnowflake(): void {
if (!this.ctx) return;
const ctx = this.ctx;
const w = this.canvasWidth;
const h = this.canvasHeight;
ctx.clearRect(0, 0, w, h);
// 背景(淡蓝)
ctx.fillStyle = '#F0F5FA';
ctx.fillRect(0, 0, w, h);
const points = this.generateSnowflakePoints();
this.pointCount = points.length;
if (points.length < 2) return;
// 绘制路径
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
ctx.closePath();
// 填充(半透明冰蓝)
ctx.fillStyle = 'rgba(173, 216, 230, 0.3)';
ctx.fill();
// 描边
ctx.strokeStyle = '#4682B4';
ctx.lineWidth = 1.8;
ctx.stroke();
}
// 滑块变更深度
private onDepthChange(value: number): void {
this.depth = value;
this.drawSnowflake();
}
build() {
Column() {
Text('科赫雪花')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 4 })
Text('递归分形 · 有限面积,无限周长')
.fontSize(15)
.fontColor('#666')
.margin({ bottom: 12 })
Canvas()
.width('100%')
.height(360)
.backgroundColor('#F0F5FA')
.onReady((event) => {
let ctx = event.context as CanvasRenderingContext2D;
this.onCanvasReady(ctx);
})
Row() {
Text(`深度:${this.depth}`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width(100)
Slider({
value: this.depth,
min: 0,
max: 6,
step: 1,
style: SliderStyle.OutSet
})
.layoutWeight(1)
.onChange((value: number) => {
this.onDepthChange(value);
})
}
.width('90%')
.margin({ top: 14, bottom: 6 })
Text(`顶点数:${this.pointCount}`)
.fontSize(14)
.fontColor('#888')
.margin({ bottom: 10 })
Column() {
Text('❄️ 小知识')
.fontSize(15)
.fontWeight(FontWeight.Medium)
.alignSelf(ItemAlign.Start)
.margin({ bottom: 6 })
Text('科赫雪花是分形几何的经典图形。每增加一层递归,周长变为原来的 4/3 倍,无限递归后周长趋于无穷,但所围面积却是有限的。')
.fontSize(13)
.fontColor('#666')
.lineHeight(20)
.alignSelf(ItemAlign.Start)
}
.width('88%')
.padding(14)
.backgroundColor('#EEF2F8')
.borderRadius(12)
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
}
代码里把递归和 Canvas 绘图完全分开:kochCurve 专心生成点,generateSnowflakePoints 拼出整个雪花,drawSnowflake 负责清屏、填充和描边。滑块变化时只用调用 drawSnowflake,整个流程简洁清晰。
运行效果
把代码粘贴进项目,点 Run,Pura X Max 模拟器上出现一片淡蓝背景,正中央画着一朵冰蓝色的科赫雪花,默认深度 3,轮廓已经相当精细,能清晰看到每一级凸起。上方显示"深度:3",下方标注顶点数 192 左右。拖动滑块调到 0,雪花变成光秃秃的等边三角形;调到 1,三角形每条边鼓起一个尖,变成六角星;调到 6,雪花的边缘已经布满密密麻麻的小锯齿,线条依旧流畅,没有任何卡顿。整个绘制过程在手指拖动滑块的一瞬间完成,Canvas 的响应丝滑利落。



总结
这个科赫雪花小项目用不到 200 行代码,把好几个有趣的知识点揉到了一起:
- 分形与递归思想:用最简单的"替换规则"反复迭代,生成无限复杂的图形,是最优雅的递归教学案例。
- 向量计算与旋转矩阵:三等分、旋转 60 度,用到的只是高中数学,却能画出精密的几何图形。
- Canvas 路径绘制 :从点数组到
moveTo/lineTo,加上填充和描边,一次性渲染数千条线段,展示了 HarmonyOS Canvas 的性能。 - 声明式 UI 驱动图形 :
@State绑定滑块,深度一变立即重绘,数据驱动视图的模式让交互和绘图解耦得干干净净。
如果想继续玩,可以给雪花加颜色渐变,或者把科赫曲线用到其他形状上------比如把正方形的每一边也做科赫变换,会得到"科赫岛"。分形的世界里,一个简单的递归就能翻出无数花样。这个小工具给你开了一扇门,剩下的就看你的好奇心往哪走了。