Interactive animations
Animatable
The OPENRNDR Animatable
class provides animation logic.
Displayed below is a very simple animation setup in which we animate a circle from left to right. We do this by animating the x
property of our animation object.
fun main() = application {
program {
// -- create an animation object
val animation = object : Animatable() {
var x = 0.0
var y = 360.0
}
animation.apply {
::x.animate(width.toDouble(), 5000)
}
extend {
animation.updateAnimation()
drawer.fill = ColorRGBa.PINK
drawer.stroke = null
drawer.circle(animation.x, animation.y, 100.0)
}
}
}
By using .complete()
we can create sequences of property animations.
fun main() = application {
program {
val animation = object : Animatable() {
var x = 0.0
var y = 0.0
}
animation.apply {
::x.animate(width.toDouble(), 5000)
::x.complete()
::y.animate(height.toDouble(), 5000)
}
extend {
animation.updateAnimation()
drawer.fill = ColorRGBa.PINK
drawer.stroke = null
drawer.circle(animation.x, animation.y, 100.0)
}
}
}
If we leave out that ::x.complete()
line we will see that animations for x
and y
run simultaneously.
fun main() = application {
program {
val animation = object : Animatable() {
var x = 0.0
var y = 0.0
}
animation.apply {
::x.animate(width.toDouble(), 5000)
::y.animate(height.toDouble(), 5000)
}
extend {
animation.updateAnimation()
drawer.fill = ColorRGBa.PINK
drawer.stroke = null
drawer.circle(animation.x, animation.y, 100.0)
}
}
}
For those wondering where that ::x.animate()
notation comes from, those are Kotlin’s property references.
Easing
A simple trick for making animations less stiff is to specify an easing.
To demonstrate we take one of the previously shown animations and add easings.
Available Easings
fun main() = application {
program {
val animation = object : Animatable() {
var x = 0.0
var y = 0.0
}
animation.apply {
::x.animate(width.toDouble(), 5000, Easing.CubicInOut)
::y.animate(height.toDouble(), 5000, Easing.CubicInOut)
}
extend {
animation.updateAnimation()
drawer.fill = ColorRGBa.PINK
drawer.stroke = null
drawer.circle(animation.x, animation.y, 100.0)
}
}
}
Behavioral animation
fun main() = application {
program {
class AnimatedCircle : Animatable() {
var x = 0.0
var y = 0.0
var radius = 100.0
var latch = 0.0
fun shrink() {
// -- first stop any running animations for the radius property
::radius.cancel()
::radius.animate(10.0, 200, Easing.CubicInOut)
}
fun grow() {
::radius.cancel()
::radius.animate(Double.uniform(60.0, 90.0), 200, Easing.CubicInOut)
}
fun jump() {
::x.cancel()
::y.cancel()
::x.animate(Double.uniform(0.0, width.toDouble()), 400, Easing.CubicInOut)
::y.animate(Double.uniform(0.0, height.toDouble()), 400, Easing.CubicInOut)
}
fun update() {
updateAnimation()
if (!::latch.hasAnimations) {
val duration = Double.uniform(100.0, 700.0).toLong()
::latch.animate(1.0, duration).completed.listen {
val action = listOf(::shrink, ::grow, ::jump).random()
action()
}
}
}
}
val animatedCircles = List(5) {
AnimatedCircle()
}
extend {
drawer.fill = ColorRGBa.PINK
drawer.stroke = null
for (ac in animatedCircles) {
ac.update()
drawer.circle(ac.x, ac.y, ac.radius)
}
}
}
}
Looping animations
While Animatable
doesn’t provide explicit support for looping animations. They can be achieved through the following pattern:
fun main() = application {
val animation = object : Animatable() {
var x: Double = 0.0
}
program {
extend {
animation.updateAnimation()
if (!animation.hasAnimations()) {
animation.apply {
::x.animate(width.toDouble(), 1000, Easing.CubicInOut)
::x.complete()
::x.animate(0.0, 1000, Easing.CubicInOut)
::x.complete()
}
}
drawer.fill = ColorRGBa.PINK
drawer.stroke = null
drawer.circle(animation.x, height / 2.0, 100.0)
}
}
}
Animatable properties
Thus far we have only worked with Double
properties in our animations. However, animation is not limited to Doubles.
Any property that is a LinearType
can be animated through Animatable.
fun main() = application {
program {
val animation = object : Animatable() {
var color = ColorRGBa.WHITE
var position = Vector2.ZERO
}
animation.apply {
::color.animate(ColorRGBa.PINK, 5000)
::position.animate(Vector2(width.toDouble(), height.toDouble()), 5000)
}
}
}