///|
/// Applies a transform to every point in a point cloud.
pub fn transform_points(points : Array[Vec2], matrix : Matrix2D) -> Array[Vec2] {
  let result = []
  for point in points {
    let (x, y) = matrix.transform_point(point.x, point.y)
    result.push(Vec2::new(x, y))
  }
  result
}

///|
/// Creates a translated shadow copy of a motif.
pub fn drop_shadow(
  motif : Motif,
  offset : Vec2,
  color : Color,
  opacity : Double,
) -> Motif {
  let shadow_style = SvgStyle::new(fill=color.to_hex(), stroke="none", opacity~)
  Motif::new(motif.name + "-shadow", motif.path, shadow_style).placed(
    motif.anchor.x + offset.x,
    motif.anchor.y + offset.y,
    motif.scale,
    motif.rotation,
  )
}

///|
/// Creates a set of concentric outline copies to simulate a glow.
pub fn glow_motifs(
  motif : Motif,
  layers : Int,
  spread : Double,
  color : Color,
) -> Array[Motif] {
  let result = []
  for i in 1..<=layers {
    let amount = i.to_double() / layers.to_double()
    let style = SvgStyle::new(
      fill="none",
      stroke=color.to_hex(),
      stroke_width=spread * amount,
      opacity=(1.0 - amount) * motif.style.opacity,
    )
    result.push(
      Motif::new(motif.name + "-glow-{i}", motif.path, style).placed(
        motif.anchor.x,
        motif.anchor.y,
        motif.scale + amount * 0.03,
        motif.rotation,
      ),
    )
  }
  result
}

///|
/// Interpolates two paths when they have matching segment shapes.
pub fn interpolate_paths(left : Path, right : Path, amount : Double) -> Path {
  let result = Path::new()
  let count = if left.segments.length() < right.segments.length() {
    left.segments.length()
  } else {
    right.segments.length()
  }
  for i in 0.. {
        let _ = result.move_to(
          animate_double(x1, x2, amount),
          animate_double(y1, y2, amount),
        )
      }
      (LineTo(x1, y1), LineTo(x2, y2)) => {
        let _ = result.line_to(
          animate_double(x1, x2, amount),
          animate_double(y1, y2, amount),
        )
      }
      (QuadTo(x1~, y1~, x~, y~), QuadTo(x1=x2, y1=y2, x=x3, y=y3)) => {
        let _ = result.quad_to(
          animate_double(x1, x2, amount),
          animate_double(y1, y2, amount),
          animate_double(x, x3, amount),
          animate_double(y, y3, amount),
        )
      }
      (
        CubicTo(x1~, y1~, x2~, y2~, x~, y~),
        CubicTo(x1=a1, y1=b1, x2=a2, y2=b2, x=a3, y=a4),
      ) => {
        let _ = result.cubic_to(
          animate_double(x1, a1, amount),
          animate_double(y1, b1, amount),
          animate_double(x2, a2, amount),
          animate_double(y2, b2, amount),
          animate_double(x, a3, amount),
          animate_double(y, a4, amount),
        )
      }
      (ClosePath, ClosePath) => {
        let _ = result.close()
      }
      (MoveTo(x, y), _) => {
        let _ = result.move_to(x, y)
      }
      (_, LineTo(x, y)) => {
        let _ = result.line_to(x, y)
      }
      _ => ()
    }
  }
  result
}

///|
/// Repeats a motif with a rotation and scale progression.
pub fn spiral_motifs(
  motif : Motif,
  count : Int,
  angle_step : Double,
  scale_step : Double,
) -> Array[Motif] {
  let result = []
  for i in 0..