【OpenHarmony/HarmonyOs 】平抛运动 2D 与 3D 视图:Canvas 动画、轨迹点与视角滑块实践

本文基于我的 OpenHarmony/HarmonyOS 项目「物理视界 PhysicsVision」整理。项目中的「平抛运动」模型不仅有 2D 抛物线轨迹,还提供了 3D 视图、视角滑块、轨迹点记录、速度箭头和播放控制。
这一篇对应“全新视觉与交互体验”和“端侧 AI/端侧计算能力”的方向,重点讲如何用 ArkTS + Canvas 做动态物理模拟。⚽

一、为什么平抛运动适合做成动态模型?

平抛运动是高中物理中的经典模型。它有两个方向:

  • 水平方向:匀速直线运动;
  • 竖直方向:自由落体运动。

课本上通常给公式:

x = v0t
y = 1/2gt²

但学生真正难理解的是:
为什么水平速度不变,竖直速度却越来越大?轨迹为什么是抛物线?

所以项目把它做成 Canvas 动画,让用户看到小球从平台飞出、轨迹逐渐形成、速度箭头实时变化。

二、核心状态设计

平抛页面中定义了这些状态:

@State v0: number = 10
@State isPlaying: boolean = false
@State time: number = 0
@State posX: number = 0
@State posY: number = 0
@State is3D: boolean = false
@State camAngle: number = 0.6

这些状态分别表示:

  • 初速度;
  • 是否播放;
  • 当前时间;
  • 水平位移;
  • 竖直下落位移;
  • 当前是否为 3D 视图;
  • 3D 摄像机角度。

通过这些状态,页面可以同时管理物理计算、动画播放和视角切换。

三、轨迹点结构

项目定义了一个简单接口:

interface ProjPoint {
  x: number
  y: number
}

并用数组保存轨迹:

private points: ProjPoint[] = []

每一帧计算出当前位置后,把点加入轨迹数组。
这样 Canvas 就能绘制历史路径,而不只是显示当前小球位置。

四、动画播放:16ms 定时刷新

启动动画的方法如下:

startAnimation(): void {
  if (this.timerId !== -1) return
  this.time = 0
  this.posX = 0
  this.posY = 0
  this.points = []
  this.isPlaying = true
  this.timerId = setInterval(() => {
    this.time += 0.03
    this.posX = this.v0 * this.time
    this.posY = 0.5 * this.gravity * this.time * this.time
    let p: ProjPoint = { x: this.posX, y: this.posY }
    this.points.push(p)
    if (this.canvasReady) this.redraw()
  }, 16)
}

这里的核心就是公式:

this.posX = this.v0 * this.time
this.posY = 0.5 * this.gravity * this.time * this.time

每 16ms 刷新一次,接近 60fps 的动画体验。

五、生命周期清理:避免定时器泄漏

页面退出时清理定时器:

aboutToDisappear(): void {
  if (this.timerId !== -1) {
    clearInterval(this.timerId)
    this.timerId = -1
  }
}

这是动画页面必须注意的细节。
如果不清理,用户离开页面后定时器还在跑,会浪费资源,也可能导致状态异常。

六、2D 绘制:平台、网格、轨迹、小球

2D 绘制方法中先画背景、平台和地面:

ctx.clearRect(0, 0, w, h)
ctx.fillStyle = this.canvasBg()
ctx.fillRect(0, 0, w, h)

ctx.fillStyle = '#DFE6E9'
ctx.strokeStyle = this.wireColor()
ctx.lineWidth = 3
ctx.beginPath()
ctx.rect(0, offsetY - 10, offsetX + 10, h - offsetY + 10)
ctx.fill()
ctx.stroke()

再绘制轨迹:

if (this.points.length > 1) {
  ctx.strokeStyle = '#4D96FF'
  ctx.lineWidth = 2
  ctx.setLineDash([4, 4])
  ctx.beginPath()
  for (let i = 0; i < this.points.length; i++) {
    let px = this.points[i].x * scaleF + offsetX
    let py = this.points[i].y * scaleF + offsetY
    if (i === 0) ctx.moveTo(px, py)
    else ctx.lineTo(px, py)
  }
  ctx.stroke()
  ctx.setLineDash([])
}

虚线轨迹让运动路径更清晰。

七、速度箭头:拆分水平和竖直方向

播放时显示水平和竖直速度箭头:

if (this.isPlaying) {
  ctx.strokeStyle = '#1A73E8'
  ctx.beginPath()
  ctx.moveTo(ballX + 18, ballY)
  ctx.lineTo(ballX + 45, ballY)
  ctx.stroke()

  let vyLen = Math.min(this.posY * 2, 50)
  if (vyLen > 5) {
    ctx.strokeStyle = '#FF6D00'
    ctx.beginPath()
    ctx.moveTo(ballX, ballY + 18)
    ctx.lineTo(ballX, ballY + 18 + vyLen)
    ctx.stroke()
  }
}

蓝色表示水平速度,橙色表示竖直方向变化。
学生可以直观看到:水平速度保持,竖直速度越来越明显。

八、3D 视图:用投影函数模拟空间感

项目中并没有引入 Three.js,而是用 Canvas 自己做简单 3D 投影:

proj(x3d: number, y3d: number, z3d: number): number[] {
  let c = Math.cos(this.camAngle)
  let s = Math.sin(this.camAngle)
  let rx = x3d * c - z3d * s
  let rz = x3d * s + z3d * c + 400
  let f = 480
  let sc = f / rz
  return [this.cw / 2 + rx * sc, this.ch * 0.55 - y3d * sc, sc]
}

这段代码做了三个步骤:

  1. 根据视角旋转坐标;
  2. 加上景深距离;
  3. 通过透视比例映射到屏幕。

虽然是简化 3D,但对教学展示已经很有效。

九、视角滑块:让用户主动观察

3D 模式下显示视角滑块:

Slider({ value: this.camAngle * 100, min: 0, max: 628, step: 5 })
  .layoutWeight(1)
  .trackColor($r('app.color.slider_track'))
  .selectedColor('#4D96FF')
  .onChange((v: number) => {
    this.camAngle = v / 100
    if (this.canvasReady) this.redraw()
  })

用户拖动滑块后,3D 场景实时变化。
这让模型从“看动画”变成“操作实验”。

十、总结

平抛运动页面是项目中很典型的“端侧物理模拟”案例。
它没有请求网络,也没有调用外部 AI,而是用本地公式、Canvas 绘制和状态更新完成了动态演示。

这篇文章对应的主题是:全新视觉与交互体验 + 端侧计算能力
对 CSDN 读者来说,它能展示 OpenHarmony/HarmonyOS 不只适合做表单和列表,也能做有动画、有交互、有物理逻辑的学习应用。🚀

img

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐