Interactive animations

Animatable

Anything that should be animated inherits the Animatable class. The 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)
        }
    }
}

Link to the full example

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)
        }
    }
}

Link to the full example

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)
        }
    }
}

Link to the full example

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)
        }
    }
}

Link to the full example

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)
            }
        }
    }
}

Link to the full example

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)
        }
    }
}

Link to the full example

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)
        }
    }
}

edit on GitHub