1. 为什么要在HarmonyOS里实现抛物线的光学性质
这次HarmonyOS应用实例做到第206个,我选了一个偏“图形与数学结合”的题目:用ArkTS的Canvas把抛物线的光学性质动态画出来。中学课本上讲过,平行于抛物线主轴的入射光,经抛物面反射后都会汇聚到焦点。这个性质听着简单,但真正要在手机上做出可交互演示,牵扯到坐标系映射、导数求切线、法线向量、反射向量计算,还考验Canvas绘制的刷新策略。我打算做一个应用,把这条几何规律变成一张“看得见、能拖动、能调参”的动态光路图。
这个应用适合两类人看:一类是刚开始接触HarmonyOS自定义绘制的开发者,可以照着思路练Canvas基础;另一类是正在做教学类、演示类工具的朋友,抛物线光学性质只是载体,真正可复用的是“数学坐标系和屏幕坐标系互转”“向量反射算法”“触控命中检测”这套方法。哪怕你目前只会写一点ArkTS,没什么图形学底子,也可以跟着把整个项目跑通。
1.1 这个实例到底要演示什么
我先说清楚最终效果,免得后面看代码时没有画面感。整个界面是一个偏深色背景的画布,画布中央偏下位置有一条“碗口朝上”的抛物线,也就是 y = a x² 这条曲线,顶点在原点;原点上方某个高度有一个高亮的实心点,那是焦点F。当程序启动时,会有多条竖直的平行光线从画布顶部射下来,每一条光线落在抛物线上不同位置后发生反射,反射光线会明显地向同一个点收拢,这个点就是焦点。调节底部的滑块改变开口系数a,抛物线会变得“瘦高”或者“矮胖”,焦点位置也跟着变化;用手指点击曲线上任意一点,程序会单独追踪这一条光路,把入射线、法线、反射线同时画出来,方便逐点观察。
这个演示的价值在于,它不只是一张静态示意图,而是把整个反射过程真实建模了。哪怕用户没有任何几何直觉,只要拖一拖滑块、点一点曲线,就能直观理解“为什么太阳能灶的反射面要做成抛物面”“为什么射电望远镜的接收器要放在焦点附近”。我在实际做的时候,还没开始写UI,先把原理公式验证了一遍,不然界面再漂亮也没有意义。
1.2 实现之前需要建立的那些基础认知
这里要坦白一下,最开始我差点把方向搞反。平时看教科书画抛物线光学性质,入射光往往是水平的,沿着抛物线的主轴方向射向镜面,反射到焦点。但代码里坐标系和教材图往往不同,教材通常把抛物线画成开口朝右,或者把主轴画成水平方向,导致平行光也是水平箭头;而我为了方便在手机竖屏上展示,选择了开口朝上的 y = a x²,这样主轴是竖直的,平行光就要从上方竖直向下照射到凹面内壁,效果就像一束束“雨滴”垂直落到碗里,再被反射到焦点。这个方向选择直接影响了后面反射向量的正负号,建议大家拿到任何图形项目时,先画一张草图,把数学坐标方向、屏幕坐标方向、光线传播方向都标清楚,再开始写convert函数。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前的核心知识准备:抛物线的数学底子
一个抛物线镜面题,代码反而好写,难的是把公式推明白。有些Canvas教程会闭着眼睛把“反射后经过焦点”当作已知条件,直接画一条从反射点到焦点的线,这样看起来效果也对,但是没有真正计算反射方向,换一条曲线或者换一个入射方向就露馅。我的做法是只依赖最底层的两个性质:第一,抛物线上所有点到焦点的距离等于它到准线的距离;第二,反射遵循“入射角等于反射角”,用向量形式表达。这两点推清楚了,程序才有灵魂。
2.1 焦点坐标怎么来的,一张公式说清楚
以顶点在原点、开口向上的抛物线为例,设焦点坐标为F(0, p),准线方程就是 y = -p。抛物线上随便取一点P(x, y),它到焦点F的距离是 √(x² + (y - p)²),它到准线的距离是 y + p。根据抛物线定义,这两个距离相等,于是有:
√(x² + (y - p)²) = y + p
两边平方后可以得到 x² + (y - p)² = (y + p)²,展开并约分,剩下的就是 x² = 4py。也就是说,y = x² / (4p)。如果我把一般抛物线写成 y = a x²,对比一下就知道 4p = 1 / a,也就是 p = 1 / (4a)。
这个p值其实非常关键,滑块调节a时,焦点高度会按反比例变化。在界面上我会动态把焦点坐标显示出来,因为很多第一次接触的人都会惊讶:开口系数稍微变小一点,看起来只是“碗”变浅了,焦点竟然可以一下子跑出去很远。这种直觉冲击正是演示应用该有的效果。
2.2 平行光为什么一定会被反射到焦点
接下来最重要的几何结论是:竖直向下的平行光打在抛物线上任意一点P后,反射方向一定指向F。用工程方法检验很简单,我在写程序前先用笔算了一组采样数据。例如 a = 0.045,焦点 p = 1 / (4 × 0.045) ≈ 5.56。取抛物线上一点 x = 3,则 y = 0.045 × 9 = 0.405,该点切线斜率 k = dy/dx = 2ax = 0.27。
曲线在该点的内法线方向取 n₀ = (-k, 1),也就是 (-0.27, 1),归一化之后大约是 (-0.26, 0.965)。入射线方向 d = (0, -1)。用向量反射公式 r = d - 2(d · n)n,其中点积 d · n = -0.965,代入得到 r ≈ (-0.502, 0.86)。再看从P(3, 0.405)指向F(0, 5.56)的方向向量是(-3, 5.155),归一化后也是(-0.503, 0.86),两者几乎一模一样。这就说明反射方向精确指向了焦点,而不是我硬画出来的。
这段验算过程我会原封不动放在第4节,因为它直接对应程序里的reflect函数。学图形算法的时候,最怕公式套错但画面看起来还行,表面正常其实方向反了。我建议你在电脑上也复算一遍这个例子,再去敲代码。
3. 核心UI与整体设计:Canvas画布和坐标系转换
HarmonyOS的Canvas绘制接口和Web Canvas长得非常像,Path、beginPath、moveTo、lineTo、stroke这些都有,但有一个致命差异:HarmonyOS的Canvas基于屏幕像素坐标,x轴向右增加,y轴向下增加。而数学教材里的平面直角坐标系,x轴向右增加,y轴向上增加。如果不做转换,直接画y = ax²,得到的是开口向下的抛物线,整个演示从第一步就错了。
3.1 坐标映射:别让抛物线在屏幕上翻跟头
我习惯把数学坐标转屏幕坐标封装成两个小函数,所有画点都走这两个函数。数学坐标单位先定义为“单位长度”,再用一个变量pixelScale表示“每个数学单位长度对应多少像素”,这样画布尺寸变化时代码逻辑不用改。具体公式是:
屏幕X = 原点屏幕X + 数学X × pixelScale
屏幕Y = 原点屏幕Y - 数学Y × pixelScale
注意屏幕Y是减号,因为屏幕y轴朝下,数学y轴朝上。这里的“原点屏幕X/Y”也不是随便取的,为了让抛物线在竖屏画面里分布协调,我会把原点放在画布水平中点、垂直方向大约78%的位置。这样做的好处是,开口较大的抛物线向下占据大部分空间,而焦点在抛物线上方,可以落在画布顶部附近,不会被裁掉。
我在项目里用的数学观察范围是x ∈ [-6, 6],y方向大概到8左右。进入onReady回调后,Canvas的实际宽高已经确定,此时计算pixelScale。宽度的约束是,12个数学单位的横向距离不能超过画布宽度;高度方面,抛物线顶点到顶部至少留出焦点余量。实际调试时我用竖屏真机,宽度约360vp,高度约700vp,计算结果是pixelScale通常落在25到35之间。如果屏幕横过来,这个值也会随之变化,所以不能把pixelScale写死。
3.2 抛物线路径、焦点和文字标签的绘制顺序
界面全部内容都在同一个Canvas里,绘制顺序很重要。我是先clearRect清空上帧残留,再刷背景色,接着画坐标轴和网格刻度,然后画虚线准线,再画抛物线本体,之后画焦点圆点,最后才画入射光、反射光和文字标签。顺序不能乱,如果先把反射光画好再画抛物线,光会被曲线盖住一部分;如果先画文字再画光路,文字很容易被路径覆盖。
抛物线本身用一条路径完成,以x = -6为起点,按0.05步长递增到6,每步把数学坐标转成屏幕坐标后lineTo。步长0.05是我试过的比较合适的值,太大了曲线变成折线,太小了path点太多,虽然现代设备不至于卡顿,但性能没必要浪费。绘制时线条宽度我设为3vp,配色用亮青绿色,和深色背景形成反差,视觉上更清晰。焦点我用一个半径约6vp的实心橙色圆点表示,旁边用fillText写明坐标,例如F(0, 5.56),这样用户第一眼就能定位焦点。
4. 光线反射的向量算法:切线、法线与反射光路
数学公式准备好了,坐标转换也做好了,剩下就是反射光线怎么算。这一节是整个项目的算法核心,我会把代码逻辑拆开讲明白。程序里的光线不是美术素材,而是实时计算出来的;抛物线开口系数一变,所有反射光线都会立刻跟着变,这就
