线性渐变旋转算法实现

线性渐变我见过的最常见的用法就是指定两种或多种颜色,指定起止点位置或者旋转角度。比如在 gimp 中,就是设定好颜色,然后渐变工具拉一条线段。而在 css 中,则是指定方向或者旋转角度。前者很好理解,问题是后者这个角度。

我目前在用一个 2d canvas 库——konva。这个库的线性渐变就需要指定起止点,但我需要页面上是旋转角,然后程序内部根据图形宽高和旋转角计算出对应的起止点。一开始我以为很简单,直接计算直线和矩形外接圆的两个交点就可以了。但转念一想,不对,不应该是外接圆,应该是外接矩形。

然后我看了 MDN 上 linear-gradient 文档,看到了这个图片:

以从上到下为 0°,顺时针旋转。好了,现在就是怎么算的问题了。经过思索,我发现,90° 以内可以将图形右上角作为起点,然后是右下角,左下角,左上角。其中,左下角和左上角可以直接根据右上角和右下角的计算结果处理一下偏移就可以了。而右上角和右下角的区别是夹角不同,大致计算是一致的。我来画个图。

这两个图分别是右上角和右下角为起点的情况,其区别基本就是夹角不同,目的都是计算夹角减去旋转角之后所形成的直角三角形的临边端点。因为对角线和相关角度都是可以求出的,所以临边长度就很容易求出来,然后以起点作为圆心,临边作为半径,根据旋转角计算终点在球上的坐标就很容了。

/**
 * 计算渐变起止点
 * @param width
 * @param height
 * @param angle
 * @returns
 */
export function getLinearPoints(width: number, height: number, angle: number) {
  if (width <= 0 || height <= 0) {
    return {
      start: { x: 0, y: 0 },
      end: { x: 0, y: 0 },
    }
  }
  angle = angle % 360
  if (angle < 0) {
    angle += 360
  }
  // 计算 180 度以内的角,其余的角度都可以转换为 180 度以内的角度
  const _angle = angle % 180

  // 90 度以内计算高和对角线的角度,90 度以上计算宽和对角线的角度
  const diagonalAngle =
    _angle < 90
      ? Math.atan(width / height) * (180 / Math.PI)
      : Math.atan(height / width) * (180 / Math.PI)
  // 计算角度差
  const diffAngle = diagonalAngle - (_angle % 90)
  // 计算对角线长度
  const diagonal = Math.sqrt(width * width + height * height)
  // 角度差与对角线形成一个直角三角形,对角线为斜边,求邻边长度
  const radius = Math.cos((diffAngle * Math.PI) / 180) * diagonal
  // 以 radius 为半径,-90 - angle 为旋转角度,求交点坐标
  const point = getPointFromCircle(radius, -90 - angle)

  // 以矩形右上角为原点修正
  if (angle < 90) {
    point.x += width
    point.y = 0 - point.y
    return {
      start: { x: width, y: 0 },
      end: point,
    }
  }
  // 以矩形右下角为原点修正
  if (angle < 180) {
    point.x += width
    point.y = height - point.y
    return {
      start: { x: width, y: height },
      end: point,
    }
  }
  // 以矩形左下角为原点修正
  if (angle < 270) {
    point.y = height - point.y
    return {
      start: { x: 0, y: height },
      end: point,
    }
  }
  // 以矩形左上角为原点修正
  point.y = 0 - point.y
  return {
    start: { x: 0, y: 0 },
    end: point,
  }
}

/**
 * 求圆上的点坐标
 * 从圆心出发,x 轴方向逆时针旋转 angle 角度后的点坐标
 * @param radius 圆半径
 * @param angle 角度
 */
export function getPointFromCircle(radius: number, angle: number) {
  const _angleInRad = (angle * Math.PI) / 180
  const x = Math.cos(_angleInRad) * radius
  const y = Math.sin(_angleInRad) * radius
  return { x, y }
}

内容很简单,但确实写了很久。脑子不灵光就是做不了哪怕稍微有点深度的东西。