[orx-shapes] Extend circle inversion functionalities and update demos
This commit is contained in:
@@ -1,8 +1,14 @@
|
|||||||
package org.openrndr.extra.shapes.primitives
|
package org.openrndr.extra.shapes.primitives
|
||||||
|
|
||||||
|
import org.openrndr.math.GeometricPrimitive2D
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
import org.openrndr.shape.Circle
|
import org.openrndr.shape.Circle
|
||||||
|
import org.openrndr.shape.Line2D
|
||||||
|
import org.openrndr.shape.LineSegment
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +59,7 @@ fun Circle.invert(point: Vector2): Vector2 {
|
|||||||
* @return The inverted circle
|
* @return The inverted circle
|
||||||
* @throws IllegalArgumentException if the circle to be inverted is centered at the center of the inverting circle
|
* @throws IllegalArgumentException if the circle to be inverted is centered at the center of the inverting circle
|
||||||
*/
|
*/
|
||||||
fun Circle.invert(circle: Circle): Circle {
|
fun Circle.invert(circle: Circle): GeometricPrimitive2D {
|
||||||
// Vector from this circle's center to the other circle's center
|
// Vector from this circle's center to the other circle's center
|
||||||
val v = circle.center - this.center
|
val v = circle.center - this.center
|
||||||
|
|
||||||
@@ -73,8 +79,7 @@ fun Circle.invert(circle: Circle): Circle {
|
|||||||
// Special case: the result would be a line, which we can't represent as a Circle
|
// Special case: the result would be a line, which we can't represent as a Circle
|
||||||
// We'll approximate it as a very large circle
|
// We'll approximate it as a very large circle
|
||||||
val direction = v.normalized
|
val direction = v.normalized
|
||||||
val farPoint = this.center + direction * 1e6
|
return Line2D(this.center, direction)
|
||||||
return Circle(farPoint, 1e6)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate power of the point (center of the inverting circle) with respect to the circle being inverted
|
// Calculate power of the point (center of the inverting circle) with respect to the circle being inverted
|
||||||
@@ -101,7 +106,7 @@ fun Circle.invert(circle: Circle): Circle {
|
|||||||
* @return The conformally inverted circle
|
* @return The conformally inverted circle
|
||||||
* @throws IllegalArgumentException if the circle to be inverted is centered at the center of the inverting circle
|
* @throws IllegalArgumentException if the circle to be inverted is centered at the center of the inverting circle
|
||||||
*/
|
*/
|
||||||
fun Circle.invertConformal(circle: Circle): Circle {
|
fun Circle.invertConformal(circle: Circle): GeometricPrimitive2D {
|
||||||
// Vector from this circle's center to the other circle's center
|
// Vector from this circle's center to the other circle's center
|
||||||
val v = circle.center - this.center
|
val v = circle.center - this.center
|
||||||
|
|
||||||
@@ -118,11 +123,8 @@ fun Circle.invertConformal(circle: Circle): Circle {
|
|||||||
|
|
||||||
// Check if the circle to be inverted passes through the center of the inverting circle
|
// Check if the circle to be inverted passes through the center of the inverting circle
|
||||||
if (abs(circle.radius - distance) < 1e-10) {
|
if (abs(circle.radius - distance) < 1e-10) {
|
||||||
// Special case: the result would be a line, which we can't represent as a Circle
|
|
||||||
// We'll approximate it as a very large circle
|
|
||||||
val direction = v.normalized
|
val direction = v.normalized
|
||||||
val farPoint = this.center + direction * 1e6
|
return Line2D(this.center, direction)
|
||||||
return Circle(farPoint, 1e6)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For conformal inversion that preserves tangency, we use the standard circle inversion formula
|
// For conformal inversion that preserves tangency, we use the standard circle inversion formula
|
||||||
@@ -141,4 +143,60 @@ fun Circle.invertConformal(circle: Circle): Circle {
|
|||||||
val newRadius = abs(this.radius * this.radius * circle.radius / power)
|
val newRadius = abs(this.radius * this.radius * circle.radius / power)
|
||||||
|
|
||||||
return Circle(newCenter, newRadius)
|
return Circle(newCenter, newRadius)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Circle.invert(segment: LineSegment): GeometricPrimitive2D {
|
||||||
|
val a = segment.start
|
||||||
|
val b = segment.end
|
||||||
|
val c = segment.position(0.5)
|
||||||
|
|
||||||
|
// Direction of the line (normalized)
|
||||||
|
val dir = (b - a)
|
||||||
|
val dirLen = dir.length
|
||||||
|
if (dirLen < 1e-10) {
|
||||||
|
// Degenerate segment: treat as a point inversion
|
||||||
|
return invert(a)
|
||||||
|
}
|
||||||
|
val u = dir / dirLen
|
||||||
|
|
||||||
|
// Foot of the perpendicular from circle center to the infinite line AB
|
||||||
|
val ao = center - a
|
||||||
|
val t = ao.dot(u)
|
||||||
|
val foot = a + u * t
|
||||||
|
|
||||||
|
val perpVec = center - foot
|
||||||
|
val dist = perpVec.length
|
||||||
|
|
||||||
|
// If the line passes through the center of inversion, it maps to a line
|
||||||
|
if (dist < 1e-10) {
|
||||||
|
val aInv = invert(a)
|
||||||
|
val bInv = invert(b)
|
||||||
|
return LineSegment(aInv, bInv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverse of a line not through the center is a circle passing through the center
|
||||||
|
val rPrime = (radius * radius) / (2.0 * dist)
|
||||||
|
val n = (foot - center).normalized // direction from center towards the line
|
||||||
|
val circleCenter = center + n * rPrime
|
||||||
|
|
||||||
|
// The circle radius equals rPrime (since it passes through the center)
|
||||||
|
val aInv = invert(a)
|
||||||
|
val bInv = invert(b)
|
||||||
|
val cInv = invert(c)
|
||||||
|
|
||||||
|
// Compute angles (in degrees) for the arc between inverted endpoints
|
||||||
|
val angleA = atan2(aInv.y - circleCenter.y, aInv.x - circleCenter.x) * 180.0 / kotlin.math.PI
|
||||||
|
val angleB = atan2(bInv.y - circleCenter.y, bInv.x - circleCenter.x) * 180.0 / kotlin.math.PI
|
||||||
|
val angleC = atan2(cInv.y - circleCenter.y, cInv.x - circleCenter.x) * 180.0 / kotlin.math.PI
|
||||||
|
|
||||||
|
var angle0 = min(angleA, angleB)
|
||||||
|
var angle1 = max(angleA, angleB)
|
||||||
|
|
||||||
|
if (angleC in angle0..angle1) {
|
||||||
|
angle1 -= 360.0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Arc(circleCenter, rPrime, angle0, angle1).conjugate()
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ fun main() = application {
|
|||||||
drawer.clear(ColorRGBa.WHITE)
|
drawer.clear(ColorRGBa.WHITE)
|
||||||
drawer.fill = ColorRGBa.BLACK
|
drawer.fill = ColorRGBa.BLACK
|
||||||
drawer.stroke = null
|
drawer.stroke = null
|
||||||
drawer.circle(mc.invertConformal(c))
|
val ci = mc.invertConformal(c)
|
||||||
|
when (ci) {
|
||||||
|
is Circle -> drawer.circle(ci)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,10 @@ fun main() = application {
|
|||||||
for (i in 0 until 10) {
|
for (i in 0 until 10) {
|
||||||
val c = Circle(i * width / 10.0 + width / 20.0, j * height / 10.0 + height / 20.0, 36.0)
|
val c = Circle(i * width / 10.0 + width / 20.0, j * height / 10.0 + height / 20.0, 36.0)
|
||||||
if (p !in c) {
|
if (p !in c) {
|
||||||
drawer.circle(mc.invertConformal(c))
|
val ci = mc.invertConformal(c)
|
||||||
|
when (ci) {
|
||||||
|
is Circle -> drawer.circle(ci)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package primitives
|
||||||
|
|
||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.isolated
|
||||||
|
import org.openrndr.extra.shapes.primitives.Arc
|
||||||
|
import org.openrndr.extra.shapes.primitives.grid
|
||||||
|
import org.openrndr.extra.shapes.primitives.invert
|
||||||
|
import org.openrndr.math.Polar
|
||||||
|
import org.openrndr.shape.Circle
|
||||||
|
import org.openrndr.shape.LineSegment
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 720
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
extend {
|
||||||
|
|
||||||
|
val p = Polar(seconds * 36.0, 100.0).cartesian + drawer.bounds.center
|
||||||
|
val c = Circle(p, 180.0)
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
val s = sin(seconds + i) * 0.25 + 0.25
|
||||||
|
drawer.fill = null
|
||||||
|
val ls = drawer.bounds.horizontal((i + 0.5) / 10.0).sub(0.5-s,0.5+s)
|
||||||
|
drawer.stroke = ColorRGBa.PINK
|
||||||
|
|
||||||
|
val cir = c.invert(ls)
|
||||||
|
when (cir) {
|
||||||
|
is Circle -> drawer.circle(cir)
|
||||||
|
is Arc -> drawer.contour(cir.contour)
|
||||||
|
is LineSegment -> drawer.lineSegment(cir)
|
||||||
|
else -> error("unsupported result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawer.isolated {
|
||||||
|
val pts = drawer.bounds.grid(10, 10).flatten().map {
|
||||||
|
c.invert(it.center)
|
||||||
|
}
|
||||||
|
drawer.fill = ColorRGBa.BLACK
|
||||||
|
drawer.stroke = null
|
||||||
|
drawer.circles(pts, 5.0)
|
||||||
|
|
||||||
|
}
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
drawer.fill = null
|
||||||
|
val s = cos(seconds + i) * 0.25 + 0.25
|
||||||
|
val ls = drawer.bounds.vertical((i + 0.5) / 10.0).sub(0.5-s,0.5+s)
|
||||||
|
drawer.stroke = ColorRGBa.PINK
|
||||||
|
val cir = c.invert(ls)
|
||||||
|
when (cir) {
|
||||||
|
is Circle -> drawer.circle(cir)
|
||||||
|
is Arc -> drawer.contour(cir.contour)
|
||||||
|
is LineSegment -> drawer.lineSegment(cir)
|
||||||
|
else -> error("unsupported result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user