Skip to content

Instantly share code, notes, and snippets.

@ylegall
Created November 6, 2020 19:18
Show Gist options
  • Save ylegall/8f2d39edceef2afbf8e37ffb3bc9b980 to your computer and use it in GitHub Desktop.
Save ylegall/8f2d39edceef2afbf8e37ffb3bc9b980 to your computer and use it in GitHub Desktop.
package org.ygl.openrndr.demos
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.color.mix
import org.openrndr.draw.renderTarget
import org.openrndr.draw.shadeStyle
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.fx.distort.StackRepeat
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.parameters.Description
import org.openrndr.extra.parameters.DoubleParameter
import org.openrndr.ffmpeg.VideoWriter
import org.openrndr.math.Vector2
import org.openrndr.math.mix
import org.openrndr.math.transforms.transform
import org.openrndr.shape.ShapeContour
import org.ygl.openrndr.utils.ColorMap
import org.ygl.openrndr.utils.isolatedWithTarget
import kotlin.math.PI
import kotlin.math.asin
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.random.Random
private const val WIDTH = 920
private const val HEIGHT = 920
private const val TOTAL_FRAMES = 360 * 4
private const val LOOPS = 1
private const val DELAY_FRAMES = 60
private const val RECORDING = false
fun main() = application {
configure {
width = WIDTH
height = HEIGHT
}
program {
var time = 0.0
var phase = 0.0
val numPoints = 500
val numLayers = 32
val minScale = 0.25
val maxScale = 4.5
val rng = Random(2)
val stackRepeat = StackRepeat()
val colorMap1 = ColorMap(listOf(
"7c9eb2","52528c","865591","ec8b57","94a7cd",
))
val colorMap2 = ColorMap(listOf(
"ebe9e8","a86e96","4c4376","769fbb","a9bee9"
))
val colors = List(numLayers) { colorMap1[rng.nextDouble()] to colorMap2[rng.nextDouble()] }
val params = @Description("params") object {
@DoubleParameter("a", 0.01, 400.0, precision = 2)
var a = 100.0
@DoubleParameter("b", 0.0, 400.0, precision = 2)
var b = 200.0
@DoubleParameter("maxA", 100.0, 400.0, precision = 1)
var maxA = 246.0
}
val points1 = MutableList(numPoints) { Vector2.ZERO }
val points2 = MutableList(numPoints) { Vector2.ZERO }
var shapes = emptyList<ShapeContour>()
fun signedSqrt(x: Double): Double {
return if (x < 0) {
-sqrt(-x)
} else {
sqrt(x)
}
}
fun combineAndSortPoints(p1: List<Vector2>, p2: List<Vector2> = emptyList()): List<Vector2> {
val combined = p1 + p2
val (top, bottom) = combined.partition { it.y < 0 }
return (top.sortedBy { it.x } + bottom.sortedBy { -it.x }).filter { !it.x.isNaN() && !it.y.isNaN() }
}
fun cassiniShapes(a: Double, b: Double): List<ShapeContour> {
for (i in 0 until numPoints) {
val pct = i / (numPoints - 0.0)
val e = (b / a)
val ee = e * e
val angle = if (a < b) {
pct * 2 * PI
} else {
val t0 = 0.5 * asin(ee % (2 * PI))
mix(-t0, t0, pct)
}
val eeee = ee * ee
val radicand = sqrt(eeee - sin(2 * angle).let { it * it })
val radicand1 = a * signedSqrt(cos(2 * angle) + radicand)
val radicand2 = a * signedSqrt(cos(2 * angle) - radicand)
val x1 = radicand1 * cos(angle)
val x2 = radicand2 * cos(angle)
val y1 = radicand1 * sin(angle)
val y2 = radicand2 * sin(angle)
points1[i] = Vector2(x1, y1)
points2[i] = Vector2(x2, y2)
}
return if (a < b) {
listOf(
ShapeContour.fromPoints(combineAndSortPoints(points1), true)
)
} else {
val rightPoints = combineAndSortPoints(points1, points2)
val leftPoints = rightPoints.map { Vector2(-it.x, it.y) }.reversed()
listOf(
ShapeContour.fromPoints(rightPoints, true),
ShapeContour.fromPoints(leftPoints, true),
)
}
}
fun update() {
time = ((frameCount - 1) % TOTAL_FRAMES) / TOTAL_FRAMES.toDouble()
phase = 0.5 + 0.5 * sin(2 * PI * time)
val a = mix(params.a, params.maxA, phase)
shapes = cassiniShapes(a, params.b)
}
val composite = compose {
draw {
drawer.translate(drawer.bounds.center)
drawer.clear(ColorRGBa.TRANSPARENT)
for (i in 0 until numLayers) {
val pct = i / (numLayers - 1.0)
val scale = mix(minScale, maxScale, 1 - pct)
val scaledShapes = shapes.map {
it.transform(transform {
scale(scale)
rotate(degrees = 180.0 * (time + pct))
})
}
val color = mix(colors[i].first, colors[i].second, phase)
drawer.fill = color
drawer.strokeWeight = 2.5
drawer.stroke = color.shade(0.25)
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
float dist = distance(c_boundsPosition.xy, vec2(0.5, 0.5));
x_fill.rgb *= smoothstep(-0.3, 1.0, 1 - dist);
""".trimIndent()
}
drawer.contours(scaledShapes)
}
}
post(stackRepeat) {
repeats = 2
rotation = 90.0
}
if (RECORDING) {
post(FrameBlur())
}
}
val videoTarget = renderTarget(width, height) { colorBuffer() }
val videoWriter = VideoWriter.create()
.size(width, height)
.frameRate(60)
.output("video/cassini-ovals-3.mp4")
if (RECORDING) {
videoWriter.start()
} else {
extend(GUI()) {
add(params)
add(stackRepeat)
}
}
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