Created
September 2, 2020 00:33
-
-
Save ylegall/b8001d8c03c47437e2fee9c6e792f8ca to your computer and use it in GitHub Desktop.
openRNDR code for https://imgur.com/gallery/QXVkKrT
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
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