Skip to content

Instantly share code, notes, and snippets.

@ylegall
Last active April 2, 2021 01:29
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ylegall/a636601e75539e4ad0c9d7ac705601c0 to your computer and use it in GitHub Desktop.
Save ylegall/a636601e75539e4ad0c9d7ac705601c0 to your computer and use it in GitHub Desktop.
code for morphing point portraits
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.ColorFormat
import org.openrndr.draw.colorBuffer
import org.openrndr.draw.loadImage
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.noise.poissonDiskSampling
import org.openrndr.extra.olive.oliveProgram
import org.openrndr.extras.easing.easeQuadIn
import org.openrndr.extras.easing.easeQuadInOut
import org.openrndr.ffmpeg.MP4Profile
import org.openrndr.ffmpeg.ScreenRecorder
import org.openrndr.math.Vector2
import org.openrndr.math.map
import org.openrndr.math.mix
import org.openrndr.math.smoothstep
import org.openrndr.shape.IntRectangle
import org.openrndr.shape.Rectangle
import studio.rndnr.packture.IntegralImage
import kotlin.math.abs
/**
* This is a template for a live program.
*
* It uses oliveProgram {} instead of program {}. All code inside the
* oliveProgram {} can be changed while the program is running.
*/
fun main() = application {
configure {
width = 920
height = 920
}
//oliveProgram {
program {
val radius = 12
val initialPoints = poissonDiskSampling(width.toDouble(), height.toDouble(), radius.toDouble()).also { println(it.size) }
val totalFrames = 420
fun cubicPulse(center: Double, width: Double, x: Double): Double {
val x1 = abs(x - center)
if (x1 > width) return 0.0
val x2 = x1 / width
return 1.0 - x2 * x2 * (3.0 - 2.0 * x2)
}
/**
* create a circle packing by iteratively relaxing circles
* NOTE this requires a custom KD Tree implementation.
*/
fun relaxPoints(
points: MutableList<Vector2>,
radii: List<Double>,
maxIterations: Int = -1,
minOverlap: Double = 0.001
): Int {
var iterations = 0
val positionDeltas = MutableList(points.size) { Vector2.ZERO }
val maxRadius = radii.maxOrNull() ?: 16.0
while (iterations < maxIterations || maxIterations < 0) {
val index = KDTree2.fromPoints(points.indices.toList()) { points[it] }
var changed = false
for (i in points.indices) {
val p1 = points[i]
val radius1 = radii[i]
val neighbors = index.queryRange(Rectangle.fromCenter(p1, 4 * maxRadius, 4 * maxRadius)).filter { it != i }
var overlappingNeighbors = 0
for (j in neighbors) {
val p2 = points[j]
val radius2 = radii[j]
val delta = p1 - p2
val dist = delta.length
val overlap = radius1 + radius2 - dist
if (overlap > minOverlap) {
overlappingNeighbors++
positionDeltas[i] += delta.normalized * (overlap / 2)
}
}
if (overlappingNeighbors > 0) {
changed = true
positionDeltas[i] = positionDeltas[i] / overlappingNeighbors.toDouble()
}
}
if (!changed) {
break
}
for (i in points.indices) {
points[i] += positionDeltas[i]
}
positionDeltas.fill(Vector2.ZERO)
iterations++
}
println("total iterations: $iterations")
return iterations
}
fun getImagePoints(filename: String): List<Vector2> {
val image = loadImage(filename)
image.shadow.download()
val integralImage = IntegralImage.fromColorBufferShadow(image.shadow)
val newPoints = initialPoints.toMutableList()
// change the max iterations here for time/accuracy trade-off
repeat(6) {
// move the points closer to the center
for (i in newPoints.indices) {
val delta = newPoints[i] - image.bounds.center
newPoints[i] += delta.normalized * 4.0
//newPoints[i] = mix(newPoints[i], image.bounds.center, 0.05)
}
val radii: List<Double> = newPoints.map { point ->
val x = point.x - radius/2
val y = point.y - radius/2
val result = integralImage.sum(IntRectangle(x.toInt(), y.toInt(), radius, radius))
//val avg = integralImage.
map(0.0, 256.0, 36.0, 6.0, result.toDouble() / (radius * radius))
}
// change the max iterations here for time/accuracy trade-off
relaxPoints(newPoints, radii, 30)
}
println("done computing points for $filename")
return newPoints
}
val points1 = getImagePoints("data/images/image-1.jpg")
val points2 = getImagePoints("data/images/image-2.jpg")
val points3 = getImagePoints("data/images/image-3.jpg")
extend(ScreenRecorder()) {
outputFile = "stipple.mp4"
frameClock = true
frameRate = 30
profile = MP4Profile().apply { mode(MP4Profile.WriterMode.Lossless) }
maximumFrames = totalFrames.toLong()
}
extend {
val t = ((frameCount - 1) % totalFrames) / totalFrames.toDouble()
val imagePoints = when {
t <= 0.33 -> points1
t <= 0.66 -> points2
else -> points3
}
val t1 = (t * 3.0) % 1.0
val factor = smoothstep(0.0, 0.75, cubicPulse(0.5, 0.45, t1))
val framePoints = initialPoints.indices.map { i ->
mix(initialPoints[i], imagePoints[i], factor * factor)
}
drawer.stroke = null
drawer.fill = ColorRGBa.WHITE
drawer.circles(framePoints, 4.0)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment