Skip to content

Instantly share code, notes, and snippets.

@ylegall
Created September 2, 2020 00:33
Show Gist options
  • Save ylegall/b8001d8c03c47437e2fee9c6e792f8ca to your computer and use it in GitHub Desktop.
Save ylegall/b8001d8c03c47437e2fee9c6e792f8ca 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.draw.BlendMode
import org.openrndr.draw.CircleBatchBuilder
import org.openrndr.draw.circleBatch
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.blur.GaussianBloom
import org.openrndr.ffmpeg.ScreenRecorder
import org.openrndr.math.Polar
import org.openrndr.math.Vector2
import org.openrndr.shape.ShapeContour
import org.ygl.kxa.ease.Ease
import org.ygl.openrndr.utils.ColorMap
import org.ygl.openrndr.utils.cubicPulse
import org.ygl.openrndr.utils.smoothCurve
import kotlin.math.PI
import kotlin.math.sin
import kotlin.random.Random
private const val WIDTH = 920
private const val HEIGHT = 920
private const val TOTAL_FRAMES = 360 * 6
private const val RECORDING = false
fun main() = application {
configure {
width = WIDTH
height = HEIGHT
}
program {
var time = 0.0
val rings = 24
val startRing = 1
val ringPoints = 180
val rng = Random(1)
val maxRadius = 450.0
val particlesPerPath = 128
val gb = GaussianBloom()
val colors = listOf(
ColorMap(listOf("ffa07e","f72585","7a41ff","4b8dff","4df0e2")),
ColorMap(listOf("3a3d99","217ae7","38b0d7","4fe5c7","63ffb4")),
ColorMap(listOf("f09e75","ec5766","c83ba0","a41ed9")),
ColorMap(listOf("ffa07e","f72585","7a41ff","4b8dff","4df0e2")),
ColorMap(listOf("3645ec","40a4f0","95d6f9","d7f4f1")),
ColorMap(listOf("f21a25","f0419e","fa96d4","f3d7f5")),
ColorMap(listOf("cd5b45","e88820","edb873","ffe77d","fffeb7")),
)
data class Particle(
val pathIdx: Int,
val offset: Double,
val sizeOffset: Double
)
data class PolarCoord(
val dir: Int,
val rad: Int
) {
fun toVector2() = Vector2.fromPolar(Polar(
360.0 / ringPoints * if (rad % 2 == 0) dir + 0.0 else dir + 0.5,
maxRadius * rad / rings.toDouble()
))
}
class PathParams(
val branchFactor: Double,
val shuffleNeighbors: Boolean = true,
val seen: MutableSet<PolarCoord> = mutableSetOf(),
val results: MutableList<List<Vector2>> = mutableListOf()
)
fun neighbors(coord: PolarCoord) = if (coord.rad % 2 == 0) {
listOf(
PolarCoord((ringPoints + coord.dir - 1) % ringPoints, coord.rad + 1),
PolarCoord((ringPoints + coord.dir + 0) % ringPoints, coord.rad + 1),
)
} else {
listOf(
PolarCoord((ringPoints + coord.dir + 0) % ringPoints, coord.rad + 1),
PolarCoord((ringPoints + coord.dir + 1) % ringPoints, coord.rad + 1),
)
}
// TODO: generate multiple paths with different parameters, e.g. branch factor
fun computePath(
coord: PolarCoord,
points: List<Vector2>,
params: PathParams
) {
if (coord in params.seen) {
return
}
params.seen.add(coord)
if (coord.rad == rings) {
params.results.add(points)
return
}
val neighbors = neighbors(coord).filter { it !in params.seen }
if (neighbors.isNotEmpty()) {
val shuffled = if (params.shuffleNeighbors) neighbors.shuffled(rng) else neighbors
val coord1 = shuffled.first()
computePath(coord1, points + coord1.toVector2(), params)
if (shuffled.size > 1 && rng.nextDouble() < params.branchFactor) {
val coord2 = shuffled.last()
computePath(coord2, points + coord2.toVector2(), params)
}
}
}
fun computePaths(startPoints: List<Int>, branchFactor: Double, shuffleNeighbors: Boolean): List<ShapeContour> {
val params = PathParams(branchFactor, shuffleNeighbors)
startPoints.forEach { dir ->
val coord = PolarCoord(dir, startRing)
computePath(coord, listOf(coord.toVector2()), params)
}
return params.results.map {
//ShapeContour.fromPoints(it, false)
smoothCurve(it, false)
}
}
val startPoints = (0 until ringPoints).shuffled(rng)
val fullBranches = computePaths(startPoints, 0.0, shuffleNeighbors = true)
val smoothLines = computePaths(startPoints, 0.0, shuffleNeighbors = false)
val halfBranches = computePaths(startPoints, 0.5, shuffleNeighbors = true)
val halfLines = computePaths(startPoints, 0.5, shuffleNeighbors = false)
val petalBranches = computePaths(startPoints, 1.0, shuffleNeighbors = true)
val petalLines = computePaths(startPoints, 1.0, shuffleNeighbors = false)
val pathsList = listOf(
fullBranches,
smoothLines,
smoothLines,
halfBranches,
halfLines,
petalBranches,
petalLines,
)
val maxPaths = pathsList.map { it.size }.maxOrNull()!!
val pathOffsets = List(maxPaths) { rng.nextDouble(0.2) }
val particles = (0 until maxPaths).flatMap { pathIdx ->
(0 until particlesPerPath).map {
Particle(pathIdx, rng.nextDouble(), rng.nextDouble())
}
}
fun curvePosition(particle: Particle, animationIndex: Int): Vector2 {
val paths = pathsList[animationIndex]
val t = (pathsList.size * time) % 1.0
val outerPosition = when (animationIndex) {
0 -> if (t < 0.5) Ease.QUART_OUT(t * 2) else 1 - Ease.BACK_IN((t - 0.5) * 2)
1 -> if (t < 0.5) Ease.QUART_OUT(t * 2) else 1 - Ease.BOUNCE_IN((t - 0.5) * 2)
2 -> Ease.CIRC_OUT(t)
5 -> cubicPulse(0.5, 0.5, t)
else -> if (t < 0.5) Ease.QUART_OUT(t * 2) else 1 - Ease.CIRC_IN((t - 0.5) * 2)
}
val idx = particle.pathIdx % paths.size
// make the particles wave up and down along the path
val waveOffset = (particle.offset * 0.02) * sin(6 * PI * (t + particle.offset + particle.sizeOffset))
val baseOffset = particle.offset + waveOffset
val pos = when (animationIndex) {
1 -> baseOffset * outerPosition
2 -> outerPosition + baseOffset * outerPosition
4 -> baseOffset * outerPosition
else -> baseOffset * (outerPosition * (1 - pathOffsets[idx]))
}
return paths[particle.pathIdx % paths.size].position(pos)
}
val composite = compose {
draw {
drawer.clear(ColorRGBa.BLACK)
val pathsIndex = (pathsList.size * time).toInt() % pathsList.size
val t = (pathsList.size * time) % 1.0
val points = particles.map {
curvePosition(it, pathsIndex)
}
val radii = particles.map {
val decayFactor = if (pathsIndex == 2) (1 - t) else 1.0
val pathPct = it.pathIdx / (ringPoints - 0.0)
val periods = 4 + 4 * it.offset
val angle = 2 * periods * (t - it.offset + pathPct)
val scale = 0.5 + 0.5 * sin(angle)
(2.5 + scale * 3.5 * it.sizeOffset * it.offset) * decayFactor
}
drawer.drawStyle.blendMode = BlendMode.ADD
val circleBatch = drawer.circleBatch {
for (i in 0 until points.size) {
val particle = particles[i]
val colorOffset = (particle.sizeOffset + particle.offset) % 1.0
val color = colors[pathsIndex % colors.size][colorOffset].opacify(0.5)
this.entries.add(
CircleBatchBuilder.Entry(
fill = color,
strokeWeight = 0.0,
stroke = null,
offset = points[i].xy0,
radius = Vector2(radii[i], radii[i])
)
)
}
}
drawer.circles(circleBatch)
drawer.stroke = ColorRGBa.WHITE.opacify(1 - t)
drawer.strokeWeight = 5.0
drawer.fill = ColorRGBa.BLACK
drawer.circle(Vector2.ZERO, 16.0)
}
post(gb) {
sigma = 0.3
shape = 0.1
}
post(FrameBlur()) {
blend = 0.3
}
}
if (RECORDING) {
extend(ScreenRecorder()) {
outputFile = "video/BranchingStar.mp4"
frameRate = 60
frameClock = true
}
} else {
//extend(GUI()) {
// add(gb)
//}
}
extend {
time = ((frameCount - 1) % TOTAL_FRAMES) / TOTAL_FRAMES.toDouble()
drawer.translate(drawer.bounds.center)
composite.draw(drawer)
if (RECORDING && frameCount >= TOTAL_FRAMES) {
application.exit()
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment