All Downloads are FREE. Search and download functionalities are using the official Maven repository.

indigo.shared.geometry.Bezier.scala Maven / Gradle / Ivy

The newest version!
package indigo.shared.geometry

import indigo.shared.collections.Batch
import indigo.shared.collections.NonEmptyBatch
import indigo.shared.collections.NonEmptyList
import indigo.shared.temporal.Signal
import indigo.shared.time.Seconds

import scala.annotation.tailrec

opaque type Bezier = Batch[Vertex]
object Bezier:

  inline def apply(vertices: Batch[Vertex]): Bezier = vertices

  inline def apply(start: Vertex, vertices: Vertex*): Bezier =
    start :: Batch.fromSeq(vertices)

  extension (b: Bezier)
    /** Calculate the position of a Bezier curve using specialised calculations for linear, quadratic and cubic curves.
      */
    def at(unitInterval: Double): Vertex =
      b.length match
        case 0 => Vertex.zero
        case 1 => b.head
        case 2 => BezierMath.linearNormalised(unitInterval, b(0), b(1))
        case 3 => BezierMath.quadraticNormalised(unitInterval, b(0), b(1), b(2))
        case 4 => BezierMath.cubicNormalised(unitInterval, b(0), b(1), b(2), b(3))
        case _ => reduce(b, Math.max(0, Math.min(1, unitInterval)))

    def toVertices(subdivisions: Int): Batch[Vertex] =
      Batch.fromIndexedSeq(
        (0 to subdivisions).map { i =>
          b.at((1 / subdivisions.toDouble) * i.toDouble)
        }
      )

    def toPolygon(subdivisions: Int): Polygon =
      Polygon.Open(toVertices(subdivisions))

    def toLineSegments(subdivisions: Int): Batch[LineSegment] =
      Polygon.Open(toVertices(subdivisions)).lineSegments

    def toSignal(duration: Seconds): Signal[Vertex] =
      Signal { t =>
        b.at(t.toDouble / duration.toDouble)
      }

    def bounds: BoundingBox =
      BoundingBox.fromVertices(b)

  def pure(start: Vertex, vertices: Batch[Vertex]): Bezier =
    Bezier(start :: vertices)

  def fromPoints(vertices: Batch[Vertex]): Option[Bezier] =
    NonEmptyBatch.fromBatch(vertices).map(fromVerticesNonEmpty)

  def fromVerticesNonEmpty(vertices: NonEmptyBatch[Vertex]): Bezier =
    Bezier(vertices.toBatch)
  @deprecated("Bezier.fromVerticesNel deprecated, use 'fromVerticesNonEmpty' instead.")
  def fromVerticesNel(vertices: NonEmptyBatch[Vertex]): Bezier =
    fromVerticesNonEmpty(vertices)

  /** Calculate the position of a Bezier curve using the same calculation method regardless of vertex count.
    */
  def atUnspecialised(bezier: Bezier, unitInterval: Double): Vertex =
    reduce(bezier, Math.max(0, Math.min(1, unitInterval)))

  def interpolate(a: Vertex, b: Vertex, unitInterval: Double): Vertex =
    a + ((b - a) * unitInterval)

  @tailrec
  def reduce(vertices: Batch[Vertex], unitInterval: Double): Vertex =
    @tailrec
    def pair(remaining: List[Vertex], acc: Batch[(Vertex, Vertex)]): Batch[(Vertex, Vertex)] =
      remaining match
        case Nil =>
          acc.reverse

        case _ :: Nil =>
          acc.reverse

        case x :: y :: xs =>
          pair(y :: xs, (x, y) :: acc)

    vertices.length match
      case 0 => Vertex.zero
      case 1 => vertices.head
      case 2 => interpolate(vertices.head, vertices(1), unitInterval)
      case _ => reduce(pair(vertices.toList, Batch.empty).map(p => interpolate(p._1, p._2, unitInterval)), unitInterval)

object BezierMath:
  /*
   * Linear
   * B(t) = P0 + ((P1 - P0) * t)
   *      = ((1 - t) * P0) +
   *        (t * P1)
   *
   *      = (1 - t) * P0 + t * P1
   */
  def linear(t: Double, p0: Double, p1: Double): Double =
    (1 - t) * p0 + t * p1

  def linearWithVertices(t: Double, p0: Vertex, p1: Vertex): Vertex =
    Vertex(linear(t, p0.x, p1.x), linear(t, p0.y, p1.y))

  def linearNormalised(t: Double, p0: Vertex, p1: Vertex): Vertex =
    linearWithVertices(Math.max(0, Math.min(1, t)), p0, p1)

  /*
   * Quadratic
   * B(t) = (((1 - t) * (1 - t)) * P0) +
   *        (((2 * t) * (1 - t) * P1) +
   *        ((t * t) * P2)
   *
   *        pow(1 - t, 2) * P0 + 2 * t * (1 - t) * P1 + pow(t, 2) * P2
   */
  def quadratic(t: Double, p0: Double, p1: Double, p2: Double): Double =
    Math.pow(1 - t, 2) * p0 + 2 * t * (1 - t) * p1 + Math.pow(t, 2) * p2

  def quadraticWithVertices(t: Double, p0: Vertex, p1: Vertex, p2: Vertex): Vertex =
    Vertex(quadratic(t, p0.x, p1.x, p2.x), quadratic(t, p0.y, p1.y, p2.y))

  def quadraticNormalised(t: Double, p0: Vertex, p1: Vertex, p2: Vertex): Vertex =
    quadraticWithVertices(Math.max(0, Math.min(1, t)), p0, p1, p2)

  /*
   * Cubic
   * B(t) = (((1 - t) * (1 - t) * (1 - t)) * P0) +
   *        (((3 * t) * ((1 - t) * (1 - t)) * P1) +
   *        ((3 * (t * t)) * (1 -t)) * P2) +
   *        ((t * t * t) * P3)
   *
   *      = pow(1 - t, 3) * P0 + 3 * t * pow(1 - t, 2) * P1 + 3 * pow(t, 2) * (1 - t) * P2 + pow(t, 3) * P3
   */
  def cubic(t: Double, p0: Double, p1: Double, p2: Double, p3: Double): Double =
    Math.pow(1 - t, 3) * p0 + 3 * t * Math.pow(1 - t, 2) * p1 + 3 * Math.pow(t, 2) * (1 - t) * p2 + Math.pow(t, 3) * p3

  def cubicWithVertices(t: Double, p0: Vertex, p1: Vertex, p2: Vertex, p3: Vertex): Vertex =
    Vertex(cubic(t, p0.x, p1.x, p2.x, p3.x), cubic(t, p0.y, p1.y, p2.y, p3.y))

  def cubicNormalised(t: Double, p0: Vertex, p1: Vertex, p2: Vertex, p3: Vertex): Vertex =
    cubicWithVertices(Math.max(0, Math.min(1, t)), p0, p1, p2, p3)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy