Skip to content

Instantly share code, notes, and snippets.

@ylegall
Created January 17, 2021 04:16
Show Gist options
  • Save ylegall/93d36f7e7cebbf5ec118bd1da81c7579 to your computer and use it in GitHub Desktop.
Save ylegall/93d36f7e7cebbf5ec118bd1da81c7579 to your computer and use it in GitHub Desktop.
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.color.mix
import org.openrndr.color.rgb
import org.openrndr.draw.isolatedWithTarget
import org.openrndr.draw.renderTarget
import org.openrndr.extra.compositor.compose
import org.openrndr.extra.compositor.draw
import org.openrndr.extra.compositor.post
import org.openrndr.extra.fx.blur.FrameBlur
import org.openrndr.extra.noise.simplex
import org.openrndr.extras.easing.easeCubicInOut
import org.openrndr.ffmpeg.MP4Profile
import org.openrndr.ffmpeg.VideoWriter
import org.openrndr.math.Polar
import org.openrndr.math.Vector2
import org.openrndr.math.smoothstep
import org.openrndr.shape.Circle
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
private const val DELAY_FRAMES = 30
private const val FPS = 60
private const val TOTAL_FRAMES = (10 * FPS + FPS/2)
private const val LOOPS = 1
private const val RECORDING = false
// https://lsandig.org/blog/2014/08/apollon-python/en/
// https://github.com/lsandig/apollon
data class Complex(
val real: Double,
val imaginary: Double = 0.0
) {
constructor(point: Vector2): this(point.x, point.y)
fun toVector2() = Vector2(real, imaginary)
operator fun times(x: Double) = Complex(real * x, imaginary * x)
operator fun times(c: Complex) = Complex(
real * c.real - imaginary * c.imaginary,
real * c.imaginary + c.real * imaginary
)
operator fun plus(c: Complex) = Complex(real + c.real, imaginary + c.imaginary)
operator fun plus(x: Double) = Complex(real + x, imaginary)
operator fun minus(c: Complex) = Complex(real - c.real, imaginary - c.imaginary)
operator fun minus(x: Double) = Complex(real - x, imaginary)
operator fun div(x: Double) = Complex(real / x, imaginary / x)
fun norm(): Double {
return sqrt(real * real + imaginary * imaginary)
}
}
private fun sqrt(z: Complex): Complex {
val r = z.norm()
val num = z + r
val den = num.norm()
return num * sqrt(r) / den
}
private fun perpindicularIntersection(v1: Vector2, v2: Vector2, v3: Vector2): Vector2 {
val dx = v2.x - v1.x
val dy = v2.y - v1.y
val k = (dx * (v3.x - v1.x) + dy * (v3.y - v1.y)) / (dx * dx + dy * dy)
val x = v1.x + k * dx
val y = v1.y + k * dy
return Vector2(x, y)
}
private fun incenter(v1: Vector2, v2: Vector2, v3: Vector2): Vector2 {
val l12 = v1.distanceTo(v2)
val l23 = v2.distanceTo(v3)
val l31 = v3.distanceTo(v1)
val totalLength = l12 + l23 + l31
val x = (l23 * v1.x + l31 * v2.x + l12 * v3.x) / totalLength
val y = (l23 * v1.y + l31 * v2.y + l12 * v3.y) / totalLength
return Vector2(x, y)
}
fun main() = application {
configure {
width = 920
height = 920
}
program {
var time = 0.0
val maxLevel = 6
val baseSeed = 3
val outerRadius = 400.0
val startingPoints = List(3) {
Vector2.fromPolar(Polar(360.0 * it / 3, 200.0))
}
var circles = emptyList<Circle>()
var outerCircle: Circle
var incenter = Vector2.ZERO
val noiseRadius = 0.12
val bgColor1 = rgb("023047")
val bgColor2 = rgb("342544")
val colors = listOf(
rgb("ffb703") to rgb("76ddc3"),
rgb("8ecae6") to rgb("0a8792"),
rgb("fb8500") to rgb("FFD4C6"),
rgb("219ebc") to rgb("e6755b"),
)
fun noiseOffset(index: Int, t: Double): Vector2 {
val seed1 = baseSeed + index
val seed2 = seed1 + startingPoints.size
val pos = Vector2.fromPolar(Polar(360 * 2 * t, 3 * noiseRadius))
val angle = 100 * simplex(index, pos)
val radius = 100 * simplex(seed2, pos)
return Vector2.fromPolar(Polar(angle, radius))
}
fun initialCircles(): List<Circle> {
// noise offset:
val centers = startingPoints.mapIndexed { index, pos ->
val offset = noiseOffset(index, time)
pos + offset
}
// other method of moving initial circles:
//val centers = startingPoints.toMutableList()
//val t = smoothstep(0.0, 0.75, (time * 8) % 1.0)
//val angle1 = 2 * PI * time
//val angle2 = PI/2 + 2 * PI * t
////val angle2 = 2 * PI * ((t.toInt() + smoothstep(0.25, 1.0, t % 1.0)) / 8.0)
//
//centers[0] += Vector2(
// 14 * cos(16 * angle1),
// 14 * sin(16 * angle1),
//)
//centers[1] += Vector2(
// 16 * sin(+1 * angle2),
// 18 * cos(+1 * angle2),
//)
incenter = incenter(centers[0], centers[1], centers[2])
val p01 = perpindicularIntersection(centers[0], centers[1], incenter)
val p12 = perpindicularIntersection(centers[1], centers[2], incenter)
//val p20 = perpindicularIntersection(centers[2], centers[0], incenter)
return listOf(
Circle(centers[0], p01.distanceTo(centers[0])),
Circle(centers[1], p12.distanceTo(centers[1])),
//Circle(centers[2], p20.distanceTo(centers[2])),
Circle(centers[2], p12.distanceTo(centers[2])),
)
}
fun outerTangentCircle(c1: Circle, c2: Circle, c3: Circle): Circle {
val cur1 = 1.0 / c1.radius
val cur2 = 1.0 / c2.radius
val cur3 = 1.0 / c3.radius
val cc1 = Complex(c1.center) / c1.radius
val cc2 = Complex(c2.center) / c2.radius
val cc3 = Complex(c3.center) / c3.radius
val cur4 = -2.0 * sqrt(cur1 * cur2 + cur2 * cur3 + cur1 * cur3) + cur1 + cur2 + cur3
val r4 = 1.0 / cur4
val dis = cc1 * cc2 + cc2 * cc3 + cc1 * cc3
// TODO: everytime there is a square root, there are 2 solutions.
// TODO: figure out how to pick the best one so that the animation does not "jump"
val p4 = ((sqrt(dis) * 2.0) + cc1 + cc2 + cc3) * r4
val p5 = ((sqrt(dis) * -2.0) + cc1 + cc2 + cc3) * r4
//val centroid = (c1.center + c2.center + c3.center) / 3.0
//val p = if (p4.toVector2().distanceTo(centroid) < p5.toVector2().distanceTo(centroid)) p4 else p5
val p = if (p4.toVector2().distanceTo(incenter) < p5.toVector2().distanceTo(incenter)) p4 else p5
//val p = if (p4.toVector2().length < p5.toVector2().length) p4 else p5
return Circle(p.toVector2(), abs(r4))
}
fun secondSolution(fixed: Circle, c1: Circle, c2: Circle, c3: Circle): Circle {
val curf = 1.0 / fixed.radius
val cur1 = 1.0 / c1.radius
val cur2 = 1.0 / c2.radius
val cur3 = 1.0 / c3.radius
val newCurvature = 2 * (cur1 + cur2 + cur3) - curf
val pos = ((c1.center * cur1 + c2.center * cur2 + c3.center * cur3) * 2.0 - fixed.center * curf) / newCurvature
return Circle(pos, 1.0 / newCurvature)
}
fun generateCirclesRecursive(circles: List<Circle>, level: Int, generated: MutableList<Circle>) {
if (level >= maxLevel) {
return
}
val c1 = circles[0]
val c2 = circles[1]
val c3 = circles[2]
val c4 = circles[3]
val newCircle2 = secondSolution(c2, c1, c3, c4)
generated.add(newCircle2)
val newCircle3 = secondSolution(c3, c1, c2, c4)
generated.add(newCircle3)
val newCircle4 = secondSolution(c4, c1, c2, c3)
generated.add(newCircle4)
generateCirclesRecursive(listOf(newCircle2, c1, c3, c4), level + 1, generated)
generateCirclesRecursive(listOf(newCircle3, c1, c2, c4), level + 1, generated)
generateCirclesRecursive(listOf(newCircle4, c1, c2, c3), level + 1, generated)
}
fun generateCircles(outerCircle: Circle, initialCircles: List<Circle>): List<Circle> {
val outer = Circle(outerCircle.center, -outerCircle.radius)
val c0 = secondSolution(outer, initialCircles[0], initialCircles[1], initialCircles[2])
val c1 = initialCircles[0]
val c2 = initialCircles[1]
val c3 = initialCircles[2]
val generatedCircles = mutableListOf(c0)
generatedCircles.addAll(initialCircles)
generateCirclesRecursive(listOf(c0, c1, c2, c3), 0, generatedCircles)
generateCirclesRecursive(listOf(outer, c1, c2, c3), 0, generatedCircles)
return generatedCircles
}
fun normalizeCircles(outerCircle: Circle, circles: List<Circle>): List<Circle> {
val positionOffset = outerCircle.center
val scaleOffset = outerRadius / outerCircle.radius
return circles.map {
Circle((it.center - positionOffset) * scaleOffset, it.radius * scaleOffset)
}
}
fun update() {
time = ((frameCount - 1) % TOTAL_FRAMES) / TOTAL_FRAMES.toDouble()
//val t = ((frame - 1) % TOTAL_FRAMES) / TOTAL_FRAMES.toDouble()
val initial = initialCircles()
outerCircle = outerTangentCircle(initial[0], initial[1], initial[2])
val allCircles = generateCircles(outerCircle, initial)
//println(allCircles.size)
//application.exit()
circles = normalizeCircles(outerCircle, allCircles)
}
val composite = compose {
draw {
var colorTime = 0.5 + 0.5 * sin(2 * PI * time)
colorTime = easeCubicInOut(colorTime)
val bgColor = mix(bgColor1, bgColor2, colorTime)
drawer.translate(drawer.bounds.center)
drawer.rotate(30 * sin(2 * PI * time))
drawer.clear(bgColor)
drawer.fill = bgColor //ColorRGBa.BLACK
drawer.stroke = null
drawer.circle(0.0, 0.0, outerRadius + 8.0)
drawer.strokeWeight = 1.0
drawer.fill = ColorRGBa.WHITE
for (i in circles.indices) {
val (c1, c2) = colors[i % 4]
drawer.fill = mix(c1, c2, colorTime)
drawer.stroke = mix(drawer.fill!!, bgColor, 0.5)
drawer.circle(circles[i])
}
}
if (RECORDING) {
post(FrameBlur()) {
blend = 0.7
}
}
}
val videoTarget = renderTarget(width, height) { colorBuffer() }
val videoWriter = VideoWriter
.create()
.size(width, height)
.frameRate(FPS)
.profile(MP4Profile().apply { mode(MP4Profile.WriterMode.Lossless) })
.output("apollonius.mp4")
if (RECORDING) videoWriter.start()
extend {
update()
if (RECORDING) {
drawer.isolatedWithTarget(videoTarget) {
composite.draw(this)
}
drawer.image(videoTarget.colorBuffer(0))
if (frameCount > DELAY_FRAMES) {
videoWriter.frame(videoTarget.colorBuffer(0))
}
if (frameCount >= TOTAL_FRAMES * LOOPS + DELAY_FRAMES) {
videoWriter.stop()
application.exit()
}
} else {
composite.draw(drawer)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment