ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

几何变换演示仪:基于HarmonyOS与ArkUI的交互式矩阵教学应用

几何变换演示仪:基于HarmonyOS与ArkUI的交互式矩阵教学应用 几何变换这四个字数学老师讲起来一套一套的平移、旋转、缩放公式写在黑板上学生盯着矩阵发呆真正理解的有几个我做开发这些年越来越觉得这类抽象概念最好的教具就是能动手拖的东西。所以就有了这个 HarmonyOS 应用实例几何变换演示仪——用 ArkUI 的 Canvas 画一个矩形和一个圆形在下方用滑块分别控制 X/Y 轴平移量、旋转角度、X/Y 轴缩放比例图形实时跟着变旁边再把矩阵数值和关键点坐标同步算出来。这个例子适合三类人给中学生讲几何变换的老师想练 ArkTS 绘图和 Canvas API 的开发者以及想看看 UI 状态驱动 图形渲染怎么配合的初学者。整个工程不复杂但把数学公式 - 矩阵运算 - 屏幕绘制这条路子完整走通了。下面我先把设计思路讲清楚再逐步拆代码最后把调试时踩过的坑全部摊开说。1. 项目背景与整体设计思路1.1 这个演示仪要解决什么痛点上过几何课的人都知道最难讲清楚的就是变换的组合。单独讲平移学生觉得我就是把图挪了个位置单独讲旋转学生也能想象出来。可一旦把旋转和缩放叠在一起再插一个平移谁先谁后图形最终落在哪儿大部分人就开始凭感觉瞎猜了。黑板上画静态图永远解决不了这个问题。学生真正需要的是参数一变图形立刻动给你看的互动工具。这一点电脑天然擅长但市面上的通用几何软件要么收费要么操作重针对性强一点的演示工具又很少适配移动端。于是我在 HarmonyOS 上做了这个轻量演示仪打开 App 就能拖不用配置、不用注册完全围绕课堂上那几分钟的演示需求来设计。1.2 为什么选 Canvas ArkTS 这套组合确定方向之后第一个要回答的问题是图形用什么方案画HarmonyOS 的 ArkUI 里可选的路子大概有三条Canvas 组件、SVG 标签、自定义组件堆叠。我最终选了 Canvas原因很直接——我要动态地改变换矩阵要在同一个坐标系里画网格、画参考图形、画变换后的图形还要顺手算关键点坐标。Canvas 的 CanvasRenderingContext2D 直接提供了 translate、rotate、scale 这一类方法等于把矩阵运算封装好了我只需要专注于想表达什么变换而不是自己拿像素拼图形。如果用 SVG语法上也能做但在 ArkUI 里和状态变量的联动没有 Canvas 这么顺手而且动态刷新时频繁改 SVG 节点描述性能和代码可读性都不占优。用自定义组件堆叠比如用 Button 的 position 模拟图形位置就更不靠谱了——旋转和缩放会让整个布局计算变得非常痛苦完全没有矩阵思维。ArkTS 这边的好处是类型严格写矩阵计算的时候不容易把变量类型搞混配合 State 做 UI 状态管理滑块一变绘图函数立刻被调用整个交互链路非常清晰。1.3 功能拆解与交互主线这个演示仪在功能上不贪多就围绕一个核心目标把一个几何图形的变换过程透明化。打开应用后你能看到一块带网格的画布画布中心有一个灰色半透明的矩形加圆形这是原始图形。然后通过下方的滑块调整参数原始图形的位置上会出现一个彩色图形它就是变换结果的实时预览。具体拆开来看功能点有这些基础图形矩形 圆形二者都定义在局部坐标系原点附近方便做旋转和缩放。三个控制维度X 轴平移、Y 轴平移、旋转角度0到360度、X 轴缩放、Y 轴缩放共五个滑块。参考网格10像素一格中心轴线加重显示方便观察图形移动距离。原图对照灰色半透明的原始图形保留在画布上变换前和变换后一眼就能对比。信息面板实时显示当前的变换矩阵数值和图形关键点比如矩形左上角和右下角变换后的画布坐标。一键重置所有参数归零图形回到初始位置。预设场景内置旋转90度放大两倍旋转缩放平移综合演示三个按钮一键生成教学常用场景。交互主线很直白拖滑块 - 图形变化 - 看数据反馈。这个参数 - 视觉 - 数值三联动就是整个演示仪的灵魂。2. 几何变换的数学底子矩阵和坐标系2.1 平移、旋转、缩放的矩阵表达写代码之前我建议你先在纸上把数学搞明白不然后面看代码容易一头雾水。二维平面上的三种基本变换用公式表达是这样平移x x tx y y ty缩放x sx * x y sy * y旋转逆时针转 θ 角x x * cosθ - y * sinθ y x * sinθ y * cosθ这三个式子里平移和缩放都很好理解旋转稍微抽象一点但本质上就是把一个点的坐标投影到旋转后的坐标轴上。打个比方转方向盘的时候方向盘上每个点都在绕中心转点离中心越远移动的弧线就越长所以公式里要同时乘上 cos 和 sin。拿个具体的点验算一下旋转90度时 cos 是0、sin 是1原来的 (50, 0) 就变成了 (0, 50) 刚好是绕原点转了四分之一圈。2.2 齐次坐标为什么需要它如果只有单个变换直接套公式就行。可现实里图形通常是先缩放、再旋转、最后平移这样串起来的。三个公式分别算也不是不行但很麻烦而且要处理先做哪个变换的顺序问题。工程上有个标准的解法把二维点 (x, y) 扩成齐次坐标 (x, y, 1)然后把三种变换统一写成 3x3 矩阵的乘法[ x ] [ cosθ -sinθ tx ] [ x ] [ y ] [ sinθ cosθ ty ] [ y ] [ 1 ] [ 0 0 1 ] [ 1 ]这样无论有多少步变换最终都可以合成一个矩阵。你不需要记得中间过程只需要维护一个 3x3 矩阵矩阵一乘结果就出来了。这个思路放到代码里就是 Canvas 的 transform 系列方法内部在做的事。以后想自己实现更复杂的变换比如镜像、错切、绕任意点旋转也都可以从矩阵层面直接推不用回退到最原始的公式。2.3 变换顺序先旋转再平移与先平移再旋转完全不同这一步是很多初学者栽跟头的地方。矩阵乘法不满足交换律变换顺序不同结果天差地别。我用同一个点 (50, 0) 来演算一下操作顺序计算结果图形效果先旋转90度再平移(100, 0)(100, 50)图形先转正再往右走位置落在右上方先平移(100, 0)再旋转90度(0, 150)图形带偏移绕原点转最终跑到下方这就很明显了如果你想让一个矩形在原来位置的基础上转一下再往右挪那必须先把旋转矩阵乘进去再乘平移矩阵写反了图形绕着原点带着偏移一起转结果完全不是你想要的。更绕的是在代码里写 Canvas 的 translate、rotate、scale 时变换的应用顺序和代码书写顺序是反的——最下面那行代码反而最先作用到图形上。这点很多教程都不强调我后面会在第3章结合代码再讲一次。2.4 屏幕坐标系与数学坐标系的方向差异还有一个必须提前说清楚的问题屏幕坐标系的 y 轴是向下的而数学课里通常画的是 y 轴向上。这意味着同一个旋转30度在数学坐标系里是逆时针在屏幕上看着却是顺时针。第一次做图形应用的人十有八九会在这儿愣一下。做教学演示仪的时候我建议不要硬把屏幕坐标系改成数学坐标系。虽然理论上可以用 ctx.scale(1, -1) 把 y 轴翻转过来但翻转之后文字会变成镜像还得再单独处理徒增复杂度。直接保留屏幕坐标系在界面上标注清楚屏幕坐标y 向下让学生知道这和课本坐标的差异本身就是一节很好的知识点。另一个常见坑是角度单位Canvas 的 rotate 方法接收的是弧度而滑块上展示和用户输入的是角度中间必须在边界处做一次 Math.PI / 180 的转换漏掉这一行图形转得歪七扭八查半天都查不出来。3. 核心实现从搭建页面到完成动态变换3.1 页面骨架与状态管理工程结构不需要复杂我用的是一个单页面应用所有代码都收敛在一个 Component 里方便阅读和调试。页面从上到下分三块标题、画布、控制区。控制区用 Column 排布里面放滑块、预设按钮、信息面板和重置按钮。先看状态管理这部分。ArkUI 里驱动界面刷新靠 State 装饰器。我把五个变换参数都定义成 StateEntry Component struct GeometryTransformDemo { private settings: RenderingContextSettings new RenderingContextSettings(true) private context: CanvasRenderingContext2D new CanvasRenderingContext2D(this.settings) State transX: number 0 State transY: number 0 State rotateAngle: number 0 State scaleX: number 1 State scaleY: number 1 private canvasWidth: number 360 private canvasHeight: number 420 build() { Column({ space: 12 }) { Text(几何变换演示仪) .fontSize(20) .fontWeight(FontWeight.Bold) Canvas(this.context) .width(this.canvasWidth) .height(this.canvasHeight) .backgroundColor(#F8F9FA) .onReady(() { this.drawScene() }) this.controlPanel() } .width(100%) .padding(16) } }这里注意我为什么把画布宽高写成固定值而不是百分比因为后续要在信息面板里显示关键点变换后的画布坐标这个坐标是绝对像素值如果画布尺寸随屏幕变化坐标计算就要动态读取布局参数凭空增加复杂度。教学演示仪优先保证逻辑清晰固定画布大小完全够用。如果你想适配不同屏幕可以用 onAreaChange 回调拿到实际宽高后存到成员变量里绘图函数统一读这个变量这样也能做到自适应。3.2 画笔与基础图形的绘制我把基础图形抽象成一个方法 drawShape()里面画一个矩形和一个圆形。关键的设计决策是图形的坐标直接以原点 (0,0) 为中心来定义。private drawShape(): void { const ctx this.context // 矩形宽100高60中心在原点 ctx.beginPath() ctx.rect(-50, -30, 100, 60) ctx.fillStyle #3DB1FF ctx.fill() ctx.lineWidth 2 ctx.strokeStyle #0B5FBF ctx.stroke() // 圆形圆心在(90, 0)半径22 ctx.beginPath() ctx.arc(90, 0, 22, 0, Math.PI * 2) ctx.fillStyle #F5A623 ctx.fill() ctx.strokeStyle #B97509 ctx.stroke() }为什么要把图形定义在原点附近这其实是在给后面的矩阵变换铺路。如果我把矩形画成从 (50, 50) 到 (150, 110)那旋转的时候就只能绕某个角落转还得心里记着中心点坐标一不留神就算错。而让图形中心落在局部坐标系原点旋转和缩放天然就是绕图形中心进行的逻辑上不知道省了多少事。圆心的位置也刻意放在了 (90, 0)没有放在矩形内部。这样旋转这个组合图形时圆形绕着矩形转圈的效果非常直观能看到平动和转动的区别。3.3 把变换矩阵翻译成 Canvas 调用核心绘图函数 drawScene 是整个应用的心脏。它要做四件事清空画布、画参考网格、画原始图形对照、画变换后的图形。private drawScene(): void { const ctx this.context const cx this.canvasWidth / 2 const cy this.canvasHeight / 2 // 1. 清空画布 ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight) // 2. 画参考网格 this.drawGuideGrid(cx, cy) // 3. 画原始图形灰色半透明对照 ctx.save() ctx.globalAlpha 0.3 ctx.strokeStyle #AAAAAA ctx.lineWidth 1 this.drawShape() ctx.restore() // 4. 画变换后的图形 ctx.save() ctx.translate(cx this.transX, cy this.transY) ctx.rotate(this.rotateAngle * Math.PI / 180) ctx.scale(this.scaleX, this.scaleY) this.drawShape() ctx.restore() // 5. 更新信息面板里的矩阵和坐标数据 this.updateInfoPanel() }这里值得停下来讲三句话。第一save 和 restore 必须成对出现它们的意义是把变换矩阵的当前状态压栈和出栈。没有 restore 的话每次 drawScene 调用后变换矩阵都会叠加第二次绘图时图形就跑到天边去了这是新手最容易踩的雷。第二代码顺序和实际变换顺序是反的。上面第4步里代码先 translate 再 rotate 再 scale但实际作用到图形上时是先缩放、再旋转、最后连同中心点一起平移。为什么因为 Canvas 每次调用 translate、rotate、scale都是把当前矩阵和对应变换矩阵做乘法而变换矩阵是乘在右侧的。对向量 p 来说最终效果是 p 先被最下面那行代码对应的矩阵处理。记住一句话读代码从下往上读就是图形真实的变换顺序。第三我把用户平移量 transX 和 transY 直接加在了画布中心坐标上也就是 ctx.translate(cx transX, cy transY)。这样设计是为了让用户拖平移滑块时图形沿屏幕的左右上下移动而不是沿旋转后的局部坐标轴移动。对初学者来说往右拖就往右移最符合直觉。如果你想让平移发生在局部坐标系里把 translate 放到 rotate 后面写就行两种效果都可以在课堂上对比展示一举两得。3.4 滑块联动把参数变成实时反馈滑块是整个应用的输入入口。我用了 ArkUI 的 Slider 组件每个滑块绑定一个 State 参数在 onChange 回调里更新参数并立即重绘画布。Builder controlPanel() { Column({ space: 8 }) { // 平移 X Row() { Text(平移 X) Text(${this.transX.toFixed(0)} px).fontColor(#666666) } .justifyContent(FlexAlign.SpaceBetween) .width(100%) Slider({ value: this.transX, min: -150, max: 150, step: 1, style: SliderStyle.OutSet }) .onChange((value: number) { this.transX value this.drawScene() }) // 旋转角度 Row() { Text(旋转角度) Text(${this.rotateAngle.toFixed(0)}°).fontColor(#666666) } .justifyContent(FlexAlign.SpaceBetween) .width(100%) Slider({ value: this.rotateAngle, min: 0, max: 360, step: 1, style: SliderStyle.OutSet }) .onChange((value: number) { this.rotateAngle value this.drawScene() }) // 缩放 X 和缩放 Y 的滑块逻辑类似这里省略重复代码 // 预设场景按钮和重置按钮 Row({ space: 8 }) { Button(旋转90°).onClick(() { this.rotateAngle 90 this.scaleX 1 this.scaleY 1 this.transX 0 this.transY 0 this.drawScene() }) Button(综合变换).onClick(() { this.rotateAngle 45 this.scaleX 0.8 this.scaleY 0.8 this.transX 50 this.transY -30 this.drawScene() }) Button(重置).onClick(() { this.rotateAngle 0 this.scaleX 1 this.scaleY 1 this.transX 0 this.transY 0 this.drawScene() }) } } .width(100%) }滑块范围我做了限制平移范围正负150像素缩放范围0.2到2.5。这个范围是结合画布尺寸算出来的——画布宽360、高420图形原始尺寸100x60如果平移超过150图形一半就跑到画布外面了缩放超过2.5图形会占满整个画布反而不利于观察。这些数值不是拍脑袋定的是保证演示效果始终处于可视范围的务实选择。关于 UI 更新方式还有个小细节Slider 的 value 参数直接传了 State 变量onChange 回调用手动赋值并调用 drawScene。这两个渠道是分离的Slider 展示值和实际参数值始终由 State 统一维护不会出现滑块拖到了但图形没变的脱节问题。如果你想让状态绑定更自动也可以尝试双向绑定写法但那样重绘时机比较隐蔽我建议初学者就老老实实用 onChange至少排查问题的时候思路是线性的。3.5 参考网格、坐标标注与信息面板网格的意义在于给移动和缩放提供视觉参照。我每30像素画一条细线中心横轴和纵轴用稍微深的颜色画出。private drawGuideGrid(cx: number, cy: number): void { const ctx this.context ctx.save() ctx.strokeStyle #E5E7EB ctx.lineWidth 1 const step 30 for (let x cx % step; x this.canvasWidth; x step) { ctx.beginPath() ctx.moveTo(x, 0) ctx.lineTo(x, this.canvasHeight) ctx.stroke() } for (let y cy % step; y this.canvasHeight; y step) { ctx.beginPath() ctx.moveTo(0, y) ctx.lineTo(this.canvasWidth, y) ctx.stroke() } ctx.strokeStyle #C0C4CC ctx.lineWidth 1.5 ctx.beginPath() ctx.moveTo(cx, 0) ctx.lineTo(cx, this.canvasHeight) ctx.stroke() ctx.beginPath() ctx.moveTo(0, cy) ctx.lineTo(this.canvasWidth, cy) ctx.stroke() ctx.restore() }这里用 cx % step 作为循环起点是为了保证网格在画布里完整分布不跟中心点较劲。如果你直接从0开始画中心点那列的线可能不在 cx 上看起来就别扭。信息面板是做这个应用最提现工程价值的地方。面板直接放在画布下方用 Text 组件显示当前变换矩阵和关键点坐标。坐标数据不能靠肉眼从图上读我写了一个 transformPoint 方法来手动推算interface TransformerPoint { x: number y: number } private transformPoint(px: number, py: number): TransformerPoint { // 这个函数的变换顺序必须和 Canvas 的调用顺序保持一致 // 先缩放再旋转最后平移 const rad this.rotateAngle * Math.PI / 180 const cos Math.cos(rad) const sin Math.sin(rad) const sx px * this.scaleX const sy py * this.scaleY return { x: sx * cos - sy * sin this.canvasWidth / 2 this.transX, y: sx * sin sy * cos this.canvasHeight / 2 this.transY } }这个函数每次都会被滑块变化触发实时计算矩形左上角 (-50, -30) 和右下角 (50, 30) 变换后的坐标。它的意义在于把 Canvas 背后的黑盒过程揭开了屏幕上图形的位置完全可以通过这个公式手动算出来。课堂演示的时候可以先让学生预测图形会跑到哪里再去看面板上的实际数值验证对错教学效果比干讲公式强太多了。3.6 预设场景与一键重置预设场景按钮的作用是降低使用门槛。老师上课的时候不可能花一分钟慢慢拖滑块调到某个状态一个按钮把参数设好直接进入要讲的知识点效率高得多。我做这三个预设时特别注意了它们的教学意义旋转90度单独演示旋转的效果参数极简适合引入概念。放大两倍演示缩放时图形的尺寸变化和中心不动。综合变换把旋转45度、缩放0.8、平移(50, -30)放在一起专门用来讨论变换顺序这个话题。综合变换预设我在按钮点击逻辑里刻意把旋转写在前面、平移写在后面。因为 Canvas 里 translate 写在 rotate 前面产生的是屏幕空间平移图形旋转之后平移仍然沿屏幕方向走。这样学生能直观地看到图形自身的方向变了但移动方向还是横平竖直这个现象理解矩阵组合的顺序问题就有了抓手。4. 坐标转换、命中检测与数据同步里的坑4.1 原点的选择决定后续计算量这一章要说的坑都是我实际调试时踩过的。第一个坑是关于原点选在哪的。刚开始我把矩形画在画布的 (100, 80) 位置想着反正 translate 能挪。结果一做旋转发现图形绕着左下角转根本不是视觉中心。后来想改成绕中心转就得手动算中心点算完之后还要考虑缩放中心越想越乱。正确的做法就是我在3.2节里强调的图形的局部坐标永远以原点为中心定义所有变换都建立在图形中心在原点这个前提下。需要图形出现在画布哪个位置就在变换矩阵里加一个 translate。这样做的最大好处是旋转、缩放数学上都是严格围绕原点进行的最终图形的中心点坐标就是 (cx transX, cy transY)。逻辑简单代码也不会出现为什么图形跑到奇怪位置的玄学问题。4.2 关键点坐标的手动推算公式第二个坑是我自己给自己挖的信息面板显示关键点坐标但这些坐标怎么算一开始我直接用 Canvas 的 transform 序列然后试图从 context 里读矩阵发现拿不到可靠的数值。其实 CanvasRenderingContext2D 没有暴露直接的矩阵读取接口所以最稳妥的方式就是用 transformPoint 这样的手算方法。手算最关键的一点是保证顺序和 Canvas 一致。我的 Canvas 调用顺序是 translate - rotate - scale实际作用顺序是 scale - rotate - translate。所以在 transformPoint 函数里我也必须先乘缩放再乘旋转最后再加平移位移。当初我没意识到这一点先加平移再算旋转结果面板上的坐标和屏幕上的图形位置差了十万八千里对着调试了很久才发现问题。这个教训也值得记进项目文档里凡是画布上画的东西和你文字面板上显示的数值必须共用同一套变换顺序否则图形和数据不一致这种最难查的 bug 分分钟出现。4.3 状态同步State 的更新时机第三个坑和 ArkUI 的状态管理有关。我在早期版本里试过用一种看起来更优雅的方式在 Watch 装饰的方法里统一触发 drawScene这样所有 State 参数变了都会自动重绘。听起来不错但实操时发现Watch 回调触发的时机是在属性变化之后理论上没问题可如果多个属性连续变化比如按预设按钮时一次改5个参数Watch 会被连续触发5次画布同一帧里被重绘多次虽然最终结果没错但属于无谓的浪费。后来我改成了现在这种写法预设按钮里集中改参数、最后统一调用一次 drawScene滑块 onChange 里改一个参数调一次。这样每帧最多重绘一次性能干净利落。这个调整让我意识到状态管理是好用的工具但不是所有场景都要把逻辑塞进 Watch 里。绘图这种手动手绘的场景显式调用反而更可控。4.4 触摸事件与变换后图形的命中检测最后一个坑是关于点图形的。如果把应用扩展成手指拖动图形改变位置就绕不开命中检测手指按在屏幕上你怎么知道按的是哪个图形如果图形没有做过变换命中检测很简单拿触摸坐标和图形矩形范围比较就行。可一旦加了旋转和缩放触摸坐标是屏幕坐标系的图形却在局部坐标系里直接比较是错的。正确做法是先反变换再命中检测——把触摸坐标应用逆变换缩放倒数、旋转的负角度、平移的负值转换成局部坐标再用这个局部坐标去和未变换的图形范围比较。这个思路和 transformPoint 正好相反。在实际工程里处理这个可以用 Canvas 的 transform 批量操作也可以手写逆矩阵。我建议在这个演示仪项目里先不做拖拽把变换展示做扎实命中检测作为扩展练习留给读者。真要加的话注意点只有一个逆变换的顺序和正变换完全相反别把顺序搞反了。5. 常见问题排查与避坑技巧5.1 图形消失或跑飞了最常见的现象是一拖滑块图形不见了。排查思路按优先级排列如下先看是不是平移越界了。transX、transY 设置过大图形整个移出画布范围自然看不见。解决方法是把滑块范围限制在画布尺寸的一半以内。再看缩放是否等于0。scaleX 或 scaleY 一旦为0矩阵不可逆图形会坍缩成一条线甚至消失。滑块 min 值我设置成0.2就是怕用户滑到0。还要检查 drawScene 是否在 onReady 之前被调用。Canvas 没准备好时绘图命令会被静默丢弃。我的代码里所有对外入口都等 onReady 触发后才绘图这是保障。如果图还在但位置离谱多半是 save/restore 没配对。每次 drawScene 开头保存、结尾恢复才能保证两次绘制之间互不影响。5.2 旋转中心不对图形旋转的时候应该绕着自身中心转结果却绕着画布原点左上角转。这个问题几乎都出在 translate 的调用时机上。记住在调用 rotate 之前必须先 translate 到你想让它当旋转中心的位置。在我们的项目里就是画布中心加上用户平移量。另外一个隐蔽情况是旋转前已经做过多次 translate但 save/restore 使用不当导致 translate 叠加。我建议每次旋转前都显式把 ctx 状态 save 一次rotate 完立刻 restore宁可多写几次 save/restore也不要依赖上下文状态。5.3 缩放后边缘发虚或线条粗细不一Canvas 绘制是矢量计算理论上缩放不糊但线条宽度是跟着变换矩阵一起缩放的。scaleX 设为2时线宽2像素实际画出来是4像素scaleX 设为0.5时线宽就变成1像素视觉上会明显变细在低分屏上看就是发虚。解决方法要看场景如果想保持线宽恒定可以在缩放之后手动把 lineWidth 设置为「基础线宽 / scale」在 drawShape 里读一下当前的 scaleX 然后反推。但如果你的本意就是展示缩放对图形的整体影响那让线宽跟着变反而更符合数学直觉。我在演示仪里保留了线宽随缩放变化的行为因为这是真实变换的一部分教学目的上更诚实。5.4 滑块拖动时卡顿滑块拖动频繁触发 onChange每次都重绘画布。我们这个应用图形简单、网格也少完全没有性能压力。但如果你把 canvas 改大或者图形越来越复杂就要注意渲染开销了。我曾经见过一个同事的项目每帧重绘整个复杂图表滑块拖起来掉帧严重。排查后发现问题不是绘制本身而是 onChange 里顺手做了大量字符串拼接和状态更新。优化方向有两个一是把 onChange 里非必要的 UI 更新剥离出去二是借助帧回调把同一帧内的多次参数变化合并成一次绘制。还有个小技巧如果信息面板的内容只需要按需更新就不要让每个滑块都触发面板重算只在需要的时候调用 updateInfoPanel。5.5 参数面板与画布数值不一致面板上显示的关键点坐标和图形实际位置对不上这个坑我在4.2节已经展开说了根源就是手算顺序和 Canvas 的变换顺序不一致。除了顺序还要注意精度问题。滑块步长设成1但缩放值可能是0.8这种小数面板上如果直接显示原始值会带出一长串小数。统一用 toFixed(1) 格式化输出既能保持界面整洁也避免了看着数值对不上的错觉。还有一个容易忽略的点角度单位。Canvas 的 rotate 接收弧度信息面板要显示角度。如果你在 updateInfoPanel 里忘了把弧度转回角度面板上就会显示3.14这种数值学生直接看懵。我建议做一条硬性规范内部存储统一用角度只在调用 rotate 时临时转弧度这样面板显示、滑块范围、预设按钮的数值全都能保持直觉一致。6. 实测体验与扩展方向6.1 真机实测效果我在真机上跑了一遍整体体验非常顺滑。网格绘制和图形重绘在几十行绘制代码的规模下毫无压力滑块从一端拖到另一端图形平滑变化信息面板的数字跟着刷新肉眼看不到任何卡顿或闪烁。有个小细节值得一提Canvas 的背景色我用的是接近白色的浅灰网格线是更浅的灰色原始对照图是半透明的深灰变换后的图形用蓝色和橙色。这套配色看上去不花哨但在投影仪上也能看得很清楚。做教学工具界面要够直观但也不能喧宾夺主图形本身的颜色区分度才是最重要的。实际演示综合变换预设时学生通常第一反应是猜图形会跑到哪里等看到结果后往往会追问为什么不是我想的位置。这时候把信息面板里的矩阵数值摆出来一步一步照着公式算给他看基本都能讲明白。这比在PPT里放一张静态图效果是碾压级的。6.2 后续可以怎么扩展这个演示仪目前只是几何变换的入门版本往上扩展的空间还很大。我梳理了几个方向都挺适合当练手项目增加更多图形三角形、五边形、自定义路径。路径绘制和矩形逻辑一样但关键点更多信息面板可以做得更丰富。增加组合变换的矩阵展示目前面板只显示关键点坐标可以再加一块区域把「缩放矩阵 × 旋转矩阵 × 平移矩阵」的具体数值实时算出来让抽象的矩阵乘法看得见。引入变换历史回放记录每一步变换用动画逐步播放。这对理解组合变换的顺序问题特别有帮助。支持拖拽和直接操作在画布上手指拖动图形内部通过逆矩阵做命中检测和位置更新从参数驱动升级为手势驱动。把画布导出成图片老师演示完一键保存成图直接放进课件实用性很高。哪怕不做这些扩展单就目前这个版本用来给初学者讲清楚什么是齐次坐标矩阵乘法为什么不满足交换律屏幕坐标和数学坐标有什么区别已经非常够用了。最后再分享一个小技巧做这类演示工具的时候不要急着堆功能。先把一个参数对应一个视觉效果这条路走通再逐步加复杂度。我刚开始也想过直接做三维变换演示后来冷静下来老老实实从二维平移旋转缩放做起。事实证明这个选择是对的——二维版本的代码量小、调试快核心教学价值一点不少而且后续要扩展三维这套矩阵思维的底子也一样能复用。
返回列表