Created
January 17, 2021 04:16
-
-
Save ylegall/93d36f7e7cebbf5ec118bd1da81c7579 to your computer and use it in GitHub Desktop.
apollonian gasket (https://www.reddit.com/r/creativecoding/comments/kyl7io/genuary_day_16_circles_only/)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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