Skip to content

复合组件

这里是 Widget 的进阶文档!在这里,我们将要介绍拼积木的方式来构建我们的 Widget——通过将一些基础的 Widget 组合在一起,我们可以创建出更复杂的 Widget:

typescript
constructor(/** 省略 */) {
  this.add(new Widget(/** 省略 */))
}

很简单,对吧?

通过这种方式,即便看起来很复杂的图形也可以一步一步被我们实现!

接下来我们来动手实现一个箭头:

typescript
import type { CanvasKit } from 'canvaskit-wasm'
import { deepMerge } from '@newcar/utils'
import type { Vector2 } from '../../utils/vector2'
import type { FigureOptions, FigureStyle } from './figure'
import { Figure } from './figure'
import { Polygon } from './polygon'
import { Line } from './line'
import { Widget } from '@newcar/core'

/**
 * Calculates the rotation angle for an arrow based on the line's start and end points,
 * with the angle expressed in degrees. The angle is calculated with respect
 * to the horizontal axis pointing to the right.
 *
 * @param startPoint The starting point of the line.
 * @param endPoint The ending point of the line.
 * @returns The rotation angle in degrees, where 0 degrees points to the right (east),
 * and positive angles are measured clockwise.
 */

function calculateArrowRotationAngle(
  startPoint: Vector2,
  endPoint: Vector2,
): number {
  // Calculate the differences in the x and y coordinates
  const dx = endPoint[0] - startPoint[0]
  const dy = endPoint[1] - startPoint[1]
  // Calculate the angle in radians using Math.atan2(dy, dx)
  const angleRadians = Math.atan2(dy, dx)
  // Convert the angle to degrees
  let angleDegrees = angleRadians * (180 / Math.PI)
  // Normalize the angle to the range [0, 360)
  if (angleDegrees < 0)
    angleDegrees += 360
  return angleDegrees
}

export interface ArrowOptions extends FigureOptions {
  style?: ArrowStyle
}

export interface ArrowStyle extends FigureStyle {}

export class Arrow extends Figure {
  private tip: Polygon
  private trim: Line
  radian: number

  constructor(
    public from: Vector2,
    public to: Vector2,
    options?: ArrowOptions,
  ) {
    options ??= {}
    super(options)
    this.radian = calculateArrowRotationAngle(this.from, this.to)
    this.tip = new Polygon(
      [
        [0, 10],
        [22, 0],
        [0, -10],
      ],
      {
        x: this.to[0],
        y: this.to[1],
        style: {
          scaleX: this.from[0] > this.to[0] ? -1 : 1,
          scaleY: this.from[1] > this.to[1] ? -1 : 1,
          rotation: this.radian,
          ...this.style,
        },
        progress: this.progress,
      },
    )

    this.trim = new Line(this.from, this.to, {
      style: deepMerge({
        color: this.style.borderColor,
        width: this.style.borderWidth,
      }, this.style),
      progress: this.progress,
    })

    this.add(this.trim, this.tip)
  }

  predraw(ck: CanvasKit, propertyChanged: string): void {
    switch (propertyChanged) {
      case 'from':
      case 'to': {
        this.radian = calculateArrowRotationAngle(this.from, this.to)
        this.tip.style.rotation = this.radian
        this.trim.from = this.from
        this.trim.to = this.to
        break
      }
      case 'progress': {
        this.tip.progress = this.progress
        this.trim.progress = this.progress
        break
      }
      case 'style.transparency': {
        this.tip.style.transparency = this.style.transparency
        this.trim.style.transparency = this.style.transparency
        break
      }
      case 'style.offset':
      case 'style.interval': {
        this.tip.style.offset = this.style.offset
        this.tip.style.interval = this.style.interval
      }
    }
  }
}

在上面的代码中,我们添加了 trim(箭头的杆)和 tip(箭头的尖),给它们设置合适的大小、位置和样式,组合在一起就完成了箭头的实现。

计算方法

你可能会疑惑,上一节中我们提到的 calculateIn 方法和 calculateRange 方法在这里为什么都没有出现,这是因为 Widget 中默认的包装会自动处理子组件的计算,只有当前组件存在独立绘制的内容时才需要单独实现这两个方法(并且同样只需要考虑独立绘制的部分!)。

TIP

请在 construnctor 里创建并加入子组件,因为 init 只有动画 play 后才会进行调用,所以可能会有一定的几率报错

Released under the Apache-2.0 license