Last active
April 2, 2021 01:29
-
-
Save ylegall/a636601e75539e4ad0c9d7ac705601c0 to your computer and use it in GitHub Desktop.
code for morphing point portraits
This file contains 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.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