【OpenHarmony/HarmonyOs 】数学曲线画板实战:圆、椭圆、双曲线、抛物线的参数化绘制

项目类型:OpenHarmony / HarmonyOS ArkTS 数学学习应用
项目名称:数学视界
对应主题:悬浮导航栏、沉浸光感、全新视觉与交互体验等
关键词:ArkTS、Canvas、解析几何、参数化绘制、数学画板、端侧计算 📐

一、为什么这篇不写普通 Canvas,而写“数学曲线画板”?

之前很多 OpenHarmony 文章会写 Canvas 画线、画圆、画动画,但数学学习 App 里更有价值的是:把抽象公式变成可交互、可观察、可收藏的学习对象。

在“数学视界”项目中,CanvasBoard.ets 不只是一个涂鸦板,它更像一个解析几何实验台:

  • ⭕ 圆:支持圆心、半径、方程、半径线;
  • 📐 双曲线:支持横向/纵向、渐近线、顶点、焦点;
  • ⬭ 椭圆:支持长短轴、中心、顶点;
  • 🔻 抛物线:支持顶点、方向、参数;
  • 📈 函数:支持表达式采样绘制;
  • 🧭 坐标系:支持网格、坐标轴、标签、平移。

所以这篇文章重点不是“Canvas 怎么画圆”,而是讲如何把数学模型参数化,然后再映射到屏幕坐标中绘制出来。

二、整体状态设计:画板先保存数学对象

画板页里维护了两类核心数据:

@State shapes: DrawShape[] = []
@State functionGraphs: FuncGraph[] = []
@State modelList: MathModel[] = []
@State graphRange: DrawGraphRange = {
  xMin: -10,
  xMax: 10,
  yMin: -10,
  yMax: 10
}

这里的关键是:画板不是直接保存像素点,而是保存数学对象。

  • shapes 保存用户手绘的线段、点;
  • functionGraphs 保存函数表达式;
  • modelList 保存圆、椭圆、双曲线、抛物线等参数模型;
  • graphRange 保存当前坐标视野。

这样做有一个非常大的好处:当用户缩放、平移、切换深色模式时,不需要重新生成业务数据,只需要根据当前坐标范围重新绘制。

三、坐标转换:数学坐标和屏幕坐标的桥梁

Canvas 绘制使用的是屏幕坐标,左上角是 (0, 0),向右为 x 正方向,向下为 y 正方向。数学坐标系通常以中心为原点,向上为 y 正方向。

所以画板必须先做坐标转换:

mathToCanvas(mathX: number, mathY: number): DrawPoint {
  if (this.canvasWidth === 0) return { x: 0, y: 0 }

  const x: number =
    ((mathX - this.graphRange.xMin) /
    (this.graphRange.xMax - this.graphRange.xMin)) *
    this.canvasWidth

  const y: number =
    ((this.graphRange.yMax - mathY) /
    (this.graphRange.yMax - this.graphRange.yMin)) *
    this.canvasHeight

  return { x, y }
}

反向转换则用于触摸操作:

canvasToMath(canvasX: number, canvasY: number): DrawPoint {
  const mathX: number =
    (canvasX / this.canvasWidth) *
    (this.graphRange.xMax - this.graphRange.xMin) +
    this.graphRange.xMin

  const mathY: number =
    this.graphRange.yMax -
    (canvasY / this.canvasHeight) *
    (this.graphRange.yMax - this.graphRange.yMin)

  return { x: mathX, y: mathY }
}

这两个函数是整个数学画板的基础。只要坐标转换准确,后续的圆、椭圆、双曲线、函数图像都可以稳定绘制。

四、添加数学模型:从参数面板到 modelList

项目中通过 selectedTool 判断当前添加哪类模型:

private modelTools: DrawTool[] = [
  { id: 'circle', label: '圆', icon: '⭕' },
  { id: 'hyperbola', label: '双曲线', icon: '📐' },
  { id: 'ellipse', label: '椭圆', icon: '⬭' },
  { id: 'parabola', label: '抛物线', icon: '🔻' },
  { id: 'function', label: '函数', icon: '📈' },
]

当用户点击“添加模型”时,会把当前参数固化为一个 MathModel

if (this.selectedTool === 'circle') {
  const model: MathModel = {
    id: this.newId(),
    type: 'circle',
    modelType: 'horizontal',
    h: this.circle_h,
    k: this.circle_k,
    r: this.circle_r,
    a: 0,
    b: 0,
    color: this.circleColor,
    strokeWidth: this.strokeWidth,
    filled: this.isFilled,
    dashed: this.isDashed,
    showEquation: this.circleShowEq,
    showCenter: this.circleShowCenter,
    showRadius: this.circleShowRadius,
    showVertices: false,
    showAsymptotes: false,
    showFoci: false,
  }
  this.modelList.push(model)
}

圆需要的核心参数是:

  • h:圆心 x 坐标;
  • k:圆心 y 坐标;
  • r:半径;
  • showEquation:是否显示方程;
  • showCenter:是否显示圆心;
  • showRadius:是否显示半径线。

这比直接在 Canvas 上画一次圆更灵活,因为模型对象可以删除、重绘、收藏、分享。

五、圆的绘制:标准方程可视化

圆的标准方程是:

(x - h)^2 + (y - k)^2 = r^2

代码中先把圆心和半径转换成 Canvas 坐标:

drawCircleModel(ctx: CanvasRenderingContext2D, model: MathModel): void {
  const c: DrawPoint = this.mathToCanvas(model.h, model.k)
  const rPx: number =
    Math.abs(this.mathToCanvas(model.h + model.r, model.k).x - c.x)

  ctx.strokeStyle = model.color
  ctx.fillStyle = model.color
  ctx.lineWidth = model.strokeWidth

  ctx.beginPath()
  ctx.arc(c.x, c.y, rPx, 0, Math.PI * 2)
  ctx.stroke()
}

这里的 rPx 很有意思:它不是直接把数学半径当像素,而是通过 mathToCanvas(model.h + model.r, model.k) 计算出半径对应的屏幕长度。这样当坐标范围变化时,圆会自动缩放。

如果用户开启显示圆心:

if (model.showCenter) {
  ctx.beginPath()
  ctx.arc(c.x, c.y, 4, 0, Math.PI * 2)
  ctx.fill()
}

如果开启显示半径:

if (model.showRadius) {
  const re: DrawPoint = this.mathToCanvas(model.h + model.r, model.k)
  ctx.setLineDash([4, 2])
  ctx.beginPath()
  ctx.moveTo(c.x, c.y)
  ctx.lineTo(re.x, re.y)
  ctx.stroke()
  ctx.setLineDash([])
}

这种设计对学生很友好:不仅看到圆,还能看到圆心、半径、方程之间的关系。

六、方程显示:让图像和公式互相对应

圆的方程由 buildCircleEq() 生成:

buildCircleEq(h: number, k: number, r: number): string {
  let eq: string = ''
  if (Math.abs(h) < 0.01) eq += 'x²'
  else eq += `(x${h >= 0 ? '-' : '+'}${Math.abs(h).toFixed(1)})²`

  eq += ' + '

  if (Math.abs(k) < 0.01) eq += 'y²'
  else eq += `(y${k >= 0 ? '-' : '+'}${Math.abs(k).toFixed(1)})²`

  eq += ' = ' + r.toFixed(2) + '²'
  return eq
}

这段代码看似只是字符串拼接,但对学习体验很重要。学生调整圆心和半径后,可以马上看到方程变化:

  • 圆心在原点:x² + y² = r²
  • 圆心右移:(x-h)² + y² = r²
  • 圆心上移:x² + (y-k)² = r²

这就是数学画板比静态公式表更有价值的地方。

七、双曲线、椭圆、抛物线:统一模型,不同绘制

添加双曲线时,参数是 h、k、a、b、modelType

const model: MathModel = {
  id: this.newId(),
  type: 'hyperbola',
  modelType: this.hyperbolaType,
  h: this.hyperbola_h,
  k: this.hyperbola_k,
  r: 0,
  a: this.hyperbola_a,
  b: this.hyperbola_b,
  color: this.hyperbolaColor,
  strokeWidth: this.strokeWidth,
  filled: false,
  dashed: this.isDashed,
  showEquation: this.hyperbolaShowEq,
  showCenter: true,
  showVertices: this.hyperbolaShowVertices,
  showAsymptotes: this.hyperbolaShowAsymptotes,
  showFoci: this.hyperbolaShowFoci,
}

模型结构保持统一,绘制时通过 type 分发:

drawModel(ctx: CanvasRenderingContext2D, model: MathModel): void {
  switch (model.type) {
    case 'circle':
      this.drawCircleModel(ctx, model)
      break
    case 'hyperbola':
      this.drawHyperbolaModel(ctx, model)
      break
    case 'ellipse':
      this.drawEllipseModel(ctx, model)
      break
    case 'parabola':
      this.drawParabolaModel(ctx, model)
      break
  }
}

这种“统一模型 + 分类绘制”的结构很适合继续扩展,比如后续增加:

  • 直线;
  • 抛物线标准式切换;
  • 极坐标曲线;
  • 参数方程;
  • 圆锥曲线综合模式。

八、触摸交互:画板不是静态展示

画板也支持触摸绘制、选中、平移、橡皮擦。触摸事件会先转成数学坐标:

const touch: TouchObject = event.touches[0]
const canvasX: number = touch.x
const canvasY: number = touch.y
const mathPt: DrawPoint = this.canvasToMath(canvasX, canvasY)

this.cursorPos = {
  x: canvasX,
  y: canvasY,
  mathX: mathPt.x,
  mathY: mathPt.y
}

如果当前工具是平移:

const dx: number =
  (canvasX - this.lastPanPos.x) /
  this.canvasWidth *
  (this.graphRange.xMax - this.graphRange.xMin)

this.graphRange.xMin -= dx
this.graphRange.xMax -= dx

这样平移改变的是坐标视野,而不是移动像素图层。这一点非常重要,因为数学画板应该始终以坐标系为中心,而不是以屏幕截图为中心。

九、学习数据联动:画图也算学习行为

当用户添加模型或完成绘制时,项目会记录学习行为:

AppState.recordDrawing()
this.redraw()

这让画板和首页学习进度、成就系统产生连接。用户不是孤立地画一个图,而是在完成一次“数学探究”。

十、总结

这篇文章对应的是“全新视觉与交互体验”主题,但它不是泛泛写 UI,而是聚焦数学项目特有的解析几何画板。

核心实现可以总结为:

  • 📐 用 MathModel 保存圆、椭圆、双曲线、抛物线参数;
  • 🧭 用 mathToCanvas()canvasToMath() 打通数学坐标和屏幕坐标;
  • ⭕ 用参数化方式绘制圆,并显示圆心、半径、方程;
  • 📈 用统一 drawModel() 分发不同曲线绘制;
  • 👆 用触摸事件支持平移、绘制、删除;
  • 🎯 用 AppState.recordDrawing() 接入学习统计。

数学类应用最吸引人的地方,不是把公式堆在页面上,而是让公式真正动起来、画出来、被操作。这个画板就是数学视界里最有辨识度的功能之一。🚀

img

Logo

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

更多推荐