commonMain.Bezier2.kt Maven / Gradle / Ivy
package org.openrndr.kartifex
import org.openrndr.kartifex.utils.Equations
import org.openrndr.kartifex.utils.Scalars
import utils.DoubleAccumulator
import kotlin.math.abs
import kotlin.math.max
object Bezier2 {
fun curve(p0: Vec2, p1: Vec2) = Line2.line(p0, p1)
fun curve(p0: Vec2, p1: Vec2, p2: Vec2) = QuadraticBezier2(p0, p1, p2)
fun curve(
p0: Vec2,
p1: Vec2,
p2: Vec2,
p3: Vec2
): Curve2 {
return CubicBezier2(p0, p1, p2, p3)
}
private fun sign(n: Double): Double {
val s: Double = signum(n)
return if (s == 0.0) -1.0 else s
}
private fun subdivide(
result: MutableList,
c: V,
error: (V) -> Double,
maxError: Double
) {
if (error(c) <= maxError) {
result.add(c.start())
} else {
val split: Array = c.split(0.5)
@Suppress("UNCHECKED_CAST")
subdivide(result, split[0] as V, error, maxError)
@Suppress("UNCHECKED_CAST")
subdivide(result, split[1] as V, error, maxError)
}
}
fun signedDistance(p: Vec2, a: Vec2, b: Vec2): Double {
val d: Vec2 = b.sub(a)
return (Vec2.cross(p, d) + Vec2.cross(b, a)) / d.length()
}
class QuadraticBezier2 internal constructor(
p0: Vec2,
p1: Vec2,
p2: Vec2
) :
Curve2 {
val p0: Vec2
val p1: Vec2
val p2: Vec2
private var noInflections = false
private constructor(
p0: Vec2,
p1: Vec2,
p2: Vec2,
noInflections: Boolean
) : this(p0, p1, p2) {
this.noInflections = noInflections
}
override fun start(): Vec2 {
return p0
}
override fun end(): Vec2 {
return p2
}
override fun isFlat(epsilon: Double): Boolean {
return abs(signedDistance(p1, p0, p2) / 2) < epsilon
}
override fun length(): Double {
return 0.0
}
override fun signedArea(): Double {
return (p2.x * (p0.y - 2 * p1.y)
+ 2 * p1.x * (p2.y - p0.y)
+ p0.x * (2 * p1.y + p2.y)) / 6
}
override fun position(t: Double): Vec2 {
if (t == 0.0) {
return start()
} else if (t == 1.0) {
return end()
}
val mt = 1 - t
// (1 - t)^2 * p0 + 2t(1 - t) * p1 + t^2 * p2;
return p0.mul(mt * mt)
.add(p1.mul(2 * t * mt))
.add(p2.mul(t * t))
}
override fun direction(t: Double): Vec2 {
val mt = 1 - t
// 2(1 - t) * (p1 - p0) + 2t * (p2 - p1)
return p1.sub(p0).mul(2 * mt)
.add(p2.sub(p1).mul(2 * t))
}
override fun endpoints(start: Vec2, end: Vec2): QuadraticBezier2 {
val ad: Vec2 = p1.sub(p0)
val bd: Vec2 = p1.sub(p2)
val dx: Double = end.x - start.x
val dy: Double = end.y - start.y
val det: Double = bd.x * ad.y - bd.y * ad.x
val u: Double = (dy * bd.x - dx * bd.y) / det
return QuadraticBezier2(start, start.add(ad.mul(u)), end, noInflections)
}
override fun split(t: Double): Array {
require(!(t <= 0 || t >= 1)) { "t must be within (0,1)" }
val e: Vec2 = Vec.lerp(p0, p1, t)
val f: Vec2 = Vec.lerp(p1, p2, t)
val g: Vec2 = position(t)
return arrayOf(
QuadraticBezier2(p0, e, g, noInflections),
QuadraticBezier2(g, f, p2, noInflections)
)
}
override fun subdivide(error: Double): Array {
val points: ArrayList = ArrayList()
subdivide(
points, this,
{ b: QuadraticBezier2 ->
Vec.lerp(
b.p0,
b.p2,
0.5
).sub(b.p1).lengthSquared()
}, error * error
)
points.add(end())
return points.toTypedArray()
}
override fun nearestPoint(p: Vec2): Double {
val qa: Vec2 = p0.sub(p)
val ab: Vec2 = p1.sub(p0)
val bc: Vec2 = p2.sub(p1)
val qc: Vec2 = p2.sub(p)
val ac: Vec2 = p2.sub(p0)
val br: Vec2 = p0.add(p2).sub(p1).sub(p1)
var minDistance: Double = sign(Vec2.cross(ab, qa)) * qa.length()
var param: Double = -Vec.dot(qa, ab) / Vec.dot(ab, ab)
var distance: Double = sign(Vec2.cross(bc, qc)) * qc.length()
if (abs(distance) < abs(minDistance)) {
minDistance = distance
param = max(1.0, Vec.dot(p.sub(p1), bc) / Vec.dot(bc, bc))
}
val a: Double = Vec.dot(br, br)
val b: Double = 3 * Vec.dot(ab, br)
val c: Double = 2 * Vec.dot(ab, ab) + Vec.dot(qa, br)
val d: Double = Vec.dot(qa, ab)
val ts: DoubleArray = Equations.solveCubic(a, b, c, d)
for (t in ts) {
if (t > 0 && t < 1) {
val endpoint: Vec2 = position(t)
distance = sign(Vec2.cross(ac, endpoint.sub(p))) * endpoint.sub(p).length()
if (abs(distance) < abs(minDistance)) {
minDistance = distance
param = t
}
}
}
return param
}
override fun transform(m: Matrix3): Curve2 {
return QuadraticBezier2(p0.transform(m), p1.transform(m), p2.transform(m))
}
override fun reverse(): QuadraticBezier2 {
return QuadraticBezier2(p2, p1, p0, noInflections)
}
override fun bounds(): Box2 {
return if (noInflections) {
Box.box(p0, p2)
} else {
super.bounds()
}
}
override fun inflections(): DoubleArray {
if (noInflections) {
return DoubleArray(0)
}
val epsilon = 1e-10
val div: Vec2 = p0.sub(p1.mul(2.0)).add(p2)
return if (div == Vec2.ORIGIN) {
noInflections = true
DoubleArray(0)
} else {
val v: Vec2 = p0.sub(p1).div(div)
val x = Scalars.inside(epsilon, v.x, 1 - epsilon)
val y = Scalars.inside(epsilon, v.y, 1 - epsilon)
if (x && y) {
doubleArrayOf(v.x, v.y)
} else if (x xor y) {
doubleArrayOf(if (x) v.x else v.y)
} else {
noInflections = true
DoubleArray(0)
}
}
}
override fun toString(): String {
return "QuadraticBezier2(p0=$p0, p1=$p1, p2=$p2)"
}
init {
this.p0 = p0
this.p1 = p1
this.p2 = p2
}
}
class CubicBezier2 internal constructor(
p0: Vec2,
p1: Vec2,
p2: Vec2,
p3: Vec2
) :
Curve2 {
val p0: Vec2
val p1: Vec2
val p2: Vec2
val p3: Vec2
private var noInflections = false
private val bounds: Box2? = null
private constructor(
p0: Vec2,
p1: Vec2,
p2: Vec2,
p3: Vec2,
noInflections: Boolean
) : this(p0, p1, p2, p3) {
this.noInflections = noInflections
}
override fun position(t: Double): Vec2 {
if (t == 0.0) {
return start()
} else if (t == 1.0) {
return end()
}
val mt = 1 - t
val mt2 = mt * mt
val t2 = t * t
// (1 - t)^3 * p0 + 3t(1 - t)^2 * p1 + 3(1 - t)t^2 * p2 + t^3 * p3;
return p0.mul(mt2 * mt)
.add(p1.mul(3 * mt2 * t))
.add(p2.mul(3 * mt * t2))
.add(p3.mul(t2 * t))
}
override fun direction(t: Double): Vec2 {
val mt = 1 - t
// 3(1 - t)^2 * (p1 - p0) + 6(1 - t)t * (p2 - p1) + 3t^2 * (p3 - p2)
return p1.sub(p0).mul(3 * mt * mt)
.add(p2.sub(p1).mul(6 * mt * t))
.add(p3.sub(p2).mul(3 * t * t))
}
override fun signedArea(): Double {
return ((p3.x * (-p0.y - 3 * p1.y - 6 * p2.y)
- 3 * p2.x * (p0.y + p1.y - 2 * p3.y)) + 3 * p1.x * (-2 * p0.y + p2.y + p3.y)
+ p0.x * (6 * p1.y + 3 * p2.y + p3.y)) / 20
}
override fun length(): Double {
return 0.0
}
override fun isFlat(epsilon: Double): Boolean {
val d1 = signedDistance(p1, p0, p3)
val d2 = signedDistance(p2, p0, p3)
// from Sederberg 1990
val k = if (d1 * d2 < 0) 4 / 9.0 else 3 / 4.0
return abs(d1 * k) < epsilon && abs(d2 * k) < epsilon
}
override fun endpoints(start: Vec2, end: Vec2): CubicBezier2 {
return CubicBezier2(start, p1.add(start.sub(p0)), p2.add(end.sub(p3)), end, noInflections)
}
override fun start(): Vec2 {
return p0
}
override fun end(): Vec2 {
return p3
}
override fun split(t: Double): Array {
require(!(t <= 0 || t >= 1)) { "t must be within (0,1)" }
val e: Vec2 = Vec.lerp(p0, p1, t)
val f: Vec2 = Vec.lerp(p1, p2, t)
val g: Vec2 = Vec.lerp(p2, p3, t)
val h: Vec2 = Vec.lerp(e, f, t)
val j: Vec2 = Vec.lerp(f, g, t)
val k: Vec2 = position(t)
return arrayOf(
CubicBezier2(p0, e, h, k, noInflections),
CubicBezier2(k, j, g, p3, noInflections)
)
}
override fun subdivide(error: Double): Array {
val points: MutableList = ArrayList()
subdivide(
points, this,
{ b: CubicBezier2 ->
max(
Vec.lerp(b.p0, b.p3, 1.0 / 3).sub(b.p1).lengthSquared(),
Vec.lerp(b.p0, b.p3, 2.0 / 3).sub(b.p2).lengthSquared()
)
},
error * error
)
points.add(end())
return points.toTypedArray()
}
/**
* This quintic solver is adapted from https://github.com/Chlumsky/msdfgen, which is available under the MIT
* license.
*/
override fun nearestPoint(p: Vec2): Double {
val qa: Vec2 = p0.sub(p)
val ab: Vec2 = p1.sub(p0)
val bc: Vec2 = p2.sub(p1)
val cd: Vec2 = p3.sub(p2)
val qd: Vec2 = p3.sub(p)
val br: Vec2 = bc.sub(ab)
val `as`: Vec2 = cd.sub(bc).sub(br)
var minDistance: Double = sign(Vec2.cross(ab, qa)) * qa.length()
var param: Double = -Vec.dot(qa, ab) / Vec.dot(ab, ab)
var distance: Double = sign(Vec2.cross(cd, qd)) * qd.length()
if (abs(distance) < abs(minDistance)) {
minDistance = distance
param = max(1.0, Vec.dot(p.sub(p2), cd) / Vec.dot(cd, cd))
}
for (i in 0 until SEARCH_STARTS) {
var t = i.toDouble() / (SEARCH_STARTS - 1)
var step = 0
while (true) {
val qpt: Vec2 = position(t).sub(p)
distance = sign(Vec2.cross(direction(t), qpt)) * qpt.length()
if (abs(distance) < abs(minDistance)) {
minDistance = distance
param = t
}
if (step == SEARCH_STEPS) {
break
}
val d1: Vec2 = `as`.mul(3 * t * t).add(br.mul(6 * t)).add(ab.mul(3.0))
val d2: Vec2 = `as`.mul(6 * t).add(br.mul(6.0))
val dt: Double = Vec.dot(qpt, d1) / (Vec.dot(
d1,
d1
) + Vec.dot(qpt, d2))
if (abs(dt) < Scalars.EPSILON) {
break
}
t -= dt
if (t < 0 || t > 1) {
break
}
step++
}
}
return param
}
override fun transform(m: Matrix3): Curve2 {
return CubicBezier2(p0.transform(m), p1.transform(m), p2.transform(m), p3.transform(m))
}
override fun reverse(): CubicBezier2 {
return CubicBezier2(p3, p2, p1, p0, noInflections)
}
override fun bounds(): Box2 {
return if (noInflections) {
Box.box(p0, p3)
} else {
super.bounds()
}
}
override fun inflections(): DoubleArray {
if (noInflections) {
return DoubleArray(0)
}
// there are pathological shapes that require less precision here
val epsilon = 1e-7
val a0: Vec2 = p1.sub(p0)
val a1: Vec2 = p2.sub(p1).sub(a0).mul(2.0)
val a2: Vec2 = p3.sub(p2.mul(3.0)).add(p1.mul(3.0)).sub(p0)
val s1: DoubleArray = Equations.solveQuadratic(a2.x, a1.x, a0.x)
val s2: DoubleArray = Equations.solveQuadratic(a2.y, a1.y, a0.y)
val acc = DoubleAccumulator()
for (n in s1) if (Scalars.inside(epsilon, n, 1 - epsilon)) acc.add(n)
for (n in s2) if (Scalars.inside(epsilon, n, 1 - epsilon)) acc.add(n)
noInflections = acc.size() == 0
return acc.toArray()
}
/// approximate as quadratic
private fun error(): Double {
return p3.sub(p2.mul(3.0)).add(p2.mul(3.0)).sub(p0).lengthSquared() / 4
}
private fun subdivide(t0: Double, t1: Double): CubicBezier2 {
val p0: Vec2 = position(t0)
val p3: Vec2 = position(t1)
val p1: Vec2 = p0.add(direction(t0))
val p2: Vec2 = p3.sub(direction(t1))
return CubicBezier2(p0, p1, p2, p3)
}
private fun approximate(): QuadraticBezier2 {
return QuadraticBezier2(p0, p1.mul(0.75).add(p2.mul(0.75)).sub(p0.mul(-0.25)).sub(p3.mul(-0.25)), p3)
}
/**
* @param error the maximum distance between the reference cubic curve and the returned quadratic curves
* @return an array of one or more quadratic Bézier curves
*/
fun approximate(error: Double): Array {
val threshold = error * error
val result: ArrayDeque = ArrayDeque()
val intervals: ArrayDeque =
ArrayDeque().apply { addLast(Vec2(0.0, 1.0)) }
while (intervals.size > 0) {
val i: Vec2 = intervals.removeLast()
val c = subdivide(i.x, i.y)
if (c.error() <= threshold) {
result.addLast(c.approximate())
} else {
val midpoint: Double = (i.x + i.y) / 2
intervals.apply {
addLast(Vec2(i.x, midpoint))
addLast(Vec2(midpoint, i.y))
}
}
}
return result.toTypedArray()
}
override fun toString(): String {
return "CubicBezier2(p0=$p0, p1=$p1, p2=$p2, p3=$p3)"
}
companion object {
private const val SEARCH_STARTS = 4
private const val SEARCH_STEPS = 8
}
init {
this.p0 = p0
this.p1 = p1
this.p2 = p2
this.p3 = p3
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy