使用HarmonyOS 一行公式在手机里藏一颗心
前言
事情是这样的。一个朋友快过生日,我想送点不一样的。贺卡太俗,红包太冷,琢磨半天,忽然想起大学高数课上老师随手在黑板上画过一条心形曲线。那条公式我至今记得:x = 16 sin³ t, y = 13 cos t - 5 cos 2t - 2 cos 3t - cos 4t。当年只觉得神奇,现在想想,这不就是现成的“程序员的浪漫”吗?打开 DevEco Studio 6.1.1 Beta1,在 Pura X Max 模拟器上,我花了一晚上把这条公式从数学课本搬进了手机屏幕。这篇文章就是那个晚上的记录。里面不止有代码,还有参数方程怎么变成像素点、Canvas 怎么连点成线、颜色和大小怎么用滑块实时调,以及为什么爱心的形状偏偏长成这样。

一、这条公式是怎么来的
心形曲线在数学上没有唯一的定义,常见的有笛卡尔的 r = a(1 - cosθ) 极坐标方程,也有我们这次用的参数方程。这个 x = 16 sin³ t, y = 13 cos t - 5 cos 2t - 2 cos 3t - cos 4t 其实是基于傅里叶级数的变体,用几个余弦波的叠加来逼近心形的轮廓。t 从 0 扫到 2π,x 和 y 就在平面上走出一条封闭曲线。
仔细观察公式,x 的部分只有 sin³ t,意味着 x 在 t=0 和 π 时为零,在 π/2 时达到最大 16。这决定了心形的左右宽度。y 的部分复杂得多:主项是 13 cos t,保证了一个上下拉长的基本形状;后面的 -5 cos 2t、-2 cos 3t、-cos 4t 都是高阶修正,让顶部凹陷、底部收尖。每个余弦项的频率不同,叠加起来就产生了心形特有的“双峰”和“尖底”。
在代码里,我们不需要理解每一个系数是怎么调出来的,只需要把 t 从 0 到 2π 分成几百等份,逐个算出 (x, y),再用 Canvas 连起来就行。步长越小曲线越光滑,我取了 600 个点,模拟器上画得又快又顺。
二、从数学坐标到手机屏幕
公式算出来的 x 范围大致在 -16 到 16,y 在 -18 到 14 左右(具体可以粗略估算:x 受 16 sin³ t 约束,在 -16 到 16 之间;y 各项的最大最小值叠加,大约在 -18 到 14 之间)。这些数字直接当像素坐标肯定不行——屏幕才几百像素宽,画出来比指甲盖还小。所以必须做坐标变换。
第一步是缩放。我先算出一个缩放比例 scale,让心形在画布上既不太大撑破,也不太小说不清。基本思路是:用画布宽高分别除以心形的理论跨度,取较小的那个比例,再乘上用户调节的大小系数(比如滑块控制 0.5 到 2 倍)。这样心形永远自适应屏幕。
第二步是平移。Canvas 的坐标原点在左上角,而心形的“中心”大约在 (0, 0) 附近(数学坐标)。所以要把原点移到画布中央。代码里就是:
screenX = canvasCenterX + mathX * scale;
screenY = canvasCenterY - mathY * scale; // 注意 y 轴要翻转,因为屏幕 y 向下
翻转 y 轴这个细节,第一次做很容易忘。数学坐标 y 向上增长,屏幕坐标 y 向下增长,不翻转心形就倒过来了。
有了坐标变换,剩下的就是循环 t,算出每一个屏幕点,用 ctx.lineTo 连起来,再 fill 或 stroke。我选择了填充+描边双保险:填充淡粉色,描边深红色,看起来更立体。
三、HSL 色相滑块
纯红的心形看久了有点单调,我想让朋友能自己调颜色。Canvas 支持 HSL 颜色字符串,色相 h 从 0 到 360 可以走一遍彩虹。我加了一个色相滑块,数值直接拼成 hsl(${hue}, 80%, 60%) 作为填充色,描边用 hsl(${hue}, 90%, 40%) 更深一点,突出边缘。

除了色相,我还给了一个“大小”滑块,实际控制的是缩放系数的倍数。这样用户可以拖动滑块看到心形放大缩小,颜色从红变紫变蓝,互动感一下就上来了。
滑块的 onChange 里直接调用重绘函数,Canvas 清屏后重新计算所有屏幕点并绘制。因为只有几百个点,计算量极小,拖动时没有任何延迟,跟手得很。
四、Canvas 的填充和描边顺序
画心形的时候,如果先描边再填充,填充色会把描边盖掉一半;先填充再描边,边线就完整清晰。所以我先 fill 再 stroke。但是 fill 需要闭合路径,用 ctx.closePath() 或者在循环最后 lineTo 回到起点。我是用 closePath,简单可靠。

另外,Canvas 的 strokeStyle 和 fillStyle 可以分别设置,还可以给描边加宽度 lineWidth。我把线宽设为 3 像素,这样在模拟器上看起来清晰又不显粗糙。
五、完整代码
以下代码适配 DevEco Studio 6.1.1 Beta1、SDK22 语法,Pura X Max 模拟器。新建 Empty Ability 项目,把 entry/src/main/ets/pages/Index.ets 全选替换就行。不需要联网,不需要权限。

/*
* 心形曲线绘制 — 参数方程 + Canvas
* 环境:DevEco Studio 6.1.1 Beta1,Pura X Max 模拟器,SDK22
*/
import { CanvasRenderingContext2D } from '@ohos.graphics.canvas';
@Entry
@Component
struct Index {
@State hue: number = 0; // 色相 0~360
@State scaleFactor: number = 1; // 大小倍数 0.5~2
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.drawHeart();
}
// 绘制心形
private drawHeart(): void {
if (!this.ctx) return;
let ctx = this.ctx;
let w = this.canvasWidth;
let h = this.canvasHeight;
ctx.clearRect(0, 0, w, h);
// 背景
ctx.fillStyle = '#FFF5F5';
ctx.fillRect(0, 0, w, h);
// 计算自适应比例 (数学坐标范围大约 x: -16~16, y: -18~14)
let mathWidth = 32; // 16 - (-16)
let mathHeight = 32; // 14 - (-18) 取宽松
let baseScale = Math.min(w / mathWidth, h / mathHeight) * 0.85;
let scale = baseScale * this.scaleFactor;
let cx = w / 2;
let cy = h / 2;
// 构建路径
let points: { x: number, y: number }[] = [];
let steps = 600;
for (let i = 0; i <= steps; i++) {
let t = (i / steps) * 2 * Math.PI;
let x = 16 * Math.pow(Math.sin(t), 3);
let y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t);
let sx = cx + x * scale;
let sy = cy - y * scale; // y轴翻转
points.push({ x: sx, y: sy });
}
// 绘制
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.closePath();
// 填充颜色 (HSL)
ctx.fillStyle = `hsl(${this.hue}, 80%, 60%)`;
ctx.fill();
// 描边
ctx.strokeStyle = `hsl(${this.hue}, 90%, 40%)`;
ctx.lineWidth = 3;
ctx.stroke();
}
// 滑块改变色相
private onHueChange(value: number): void {
this.hue = value;
this.drawHeart();
}
// 滑块改变大小
private onScaleChange(value: number): void {
this.scaleFactor = value;
this.drawHeart();
}
build() {
Column() {
Text('心形曲线')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 6 })
Text('x = 16sin³t, y = 13cost - 5cos2t - 2cos3t - cos4t')
.fontSize(13)
.fontColor('#888')
.margin({ bottom: 12 })
// 画布
Canvas()
.width('100%')
.height(350)
.backgroundColor('#FFF5F5')
.onReady((event) => {
let ctx = event.context as CanvasRenderingContext2D;
this.onCanvasReady(ctx);
})
// 色相滑块
Row() {
Text('颜色').fontSize(15).width(50)
Slider({
value: this.hue,
min: 0,
max: 360,
step: 1,
style: SliderStyle.OutSet
})
.layoutWeight(1)
.onChange((value: number) => { this.onHueChange(value); })
}
.width('88%')
.margin({ top: 10, bottom: 4 })
// 大小滑块
Row() {
Text('大小').fontSize(15).width(50)
Slider({
value: this.scaleFactor,
min: 0.5,
max: 2,
step: 0.1,
style: SliderStyle.OutSet
})
.layoutWeight(1)
.onChange((value: number) => { this.onScaleChange(value); })
}
.width('88%')
.margin({ bottom: 10 })
Text('拖动滑块改变颜色和大小')
.fontSize(13)
.fontColor('#AAA')
.margin({ bottom: 6 })
Text('💡 心形曲线是参数方程的完美应用,融合三角函数的叠加之美')
.fontSize(12)
.fontColor('#AAA')
.width('90%')
.textAlign(TextAlign.Center)
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
}
代码非常精简:drawHeart 里面用参数方程生成路径,填充和描边分两次画。两个 Slider 分别控制色相和缩放系数,每次变化都触发重绘。600 个采样点保证曲线光滑,自适应缩放确保不同屏幕都居中显示。
运行效果
代码粘贴到 DevEco Studio,Run 到 Pura X Max 模拟器上。屏幕上浮现一颗粉红色心形,边缘深红,微微发着光。拖动“颜色”滑块,心形从正红变成橙、黄、绿、青、蓝、紫,再回到红,像霓虹灯在变。拖动“大小”滑块,心形呼大呼小,在淡粉背景上自如伸缩。每一个参数变化都实时反映,Canvas 重绘速度快到感觉不到任何延迟。


总结
这个小小的绘制项目,把几个看似不相关的知识点拧到了一起:
- 参数方程与图形学:用数学函数描述图形轮廓,理解 t 从 0 到 2π 的扫描过程。
- 三角函数叠加:高阶余弦项如何修正基础形状,体现了傅里叶级数的思想。
- Canvas 坐标映射:数学坐标到屏幕坐标的缩放、平移、翻转,是数据可视化的基本功。
- HSL 色彩空间:用色相控制颜色,比 RGB 更直观,适合需要调色的交互场景。
- 声明式 UI 与实时交互:
@State绑定滑块值,驱动 Canvas 即时重绘,整个过程没有一行手动 DOM 操作。
这颗心虽然是用冰冷的公式算出来的,但它能在手机屏幕上变色、缩放,像一颗活的心脏。送给朋友的时候,我附了一句:“这是一颗用三角波叠加的心,比普通的桃心多几个余弦修正项。”她没听懂,但笑得很开心。这大概就是程序员的浪漫吧——把数学变成看得见的温柔。
更多推荐


所有评论(0)