Files
orx/orx-jvm/orx-keyframer/src/main/kotlin/Keyframer.kt

469 lines
20 KiB
Kotlin

package org.openrndr.extra.keyframer
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.easing.Easing
import org.openrndr.extra.easing.EasingFunction
import org.openrndr.extra.expressions.ExpressionException
import org.openrndr.extra.expressions.FunctionExtensions
import org.openrndr.extra.expressions.evaluateExpression
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import java.io.File
import java.lang.IllegalStateException
import java.lang.NullPointerException
import java.net.URL
import kotlin.math.max
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
enum class KeyframerFormat {
SIMPLE,
FULL
}
open class Keyframer {
private var currentTime = 0.0
operator fun invoke(time: Double) {
currentTime = time
}
open inner class CompoundChannel(val keys: Array<String>, private val defaultValues: Array<Double>) {
private var channelTimes: Array<Double> = Array(keys.size) { Double.NEGATIVE_INFINITY }
private var compoundChannels: Array<KeyframerChannel?> = Array(keys.size) { null }
private var cachedValues: Array<Double?> = Array(keys.size) { null }
open fun reset() {
for (i in channelTimes.indices) {
channelTimes[i] = Double.NEGATIVE_INFINITY
}
}
fun getValue(compound: Int): Double {
if (compoundChannels[compound] == null) {
compoundChannels[compound] = channels[keys[compound]]
}
return if (compoundChannels[compound] != null) {
if (channelTimes[compound] == currentTime && cachedValues[compound] != null) {
cachedValues[compound] ?: defaultValues[compound]
} else {
val value = compoundChannels[compound]?.value(currentTime) ?: defaultValues[compound]
cachedValues[compound] = value
value
}
} else {
defaultValues[compound]
}
}
}
val duration: Double
get() = channels.values.maxByOrNull { it.duration() }?.duration() ?: 0.0
inner class DoubleChannel(key: String, defaultValue: Double = 0.0) :
CompoundChannel(arrayOf(key), arrayOf(defaultValue)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Double = getValue(0)
}
inner class Vector2Channel(keys: Array<String>, defaultValue: Vector2 = Vector2.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector2 = Vector2(getValue(0), getValue(1))
}
inner class Vector3Channel(keys: Array<String>, defaultValue: Vector3 = Vector3.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector3 =
Vector3(getValue(0), getValue(1), getValue(2))
}
inner class Vector4Channel(keys: Array<String>, defaultValue: Vector4 = Vector4.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z, defaultValue.w)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector4 =
Vector4(getValue(0), getValue(1), getValue(2), getValue(3))
}
inner class RGBaChannel(keys: Array<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) :
CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b, defaultValue.alpha)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): ColorRGBa =
ColorRGBa(getValue(0), getValue(1), getValue(2), getValue(3))
}
inner class RGBChannel(keys: Array<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) :
CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): ColorRGBa =
ColorRGBa(getValue(0), getValue(1), getValue(2))
}
inner class DoubleArrayChannel(keys: Array<String>, defaultValue: DoubleArray = DoubleArray(keys.size)) :
CompoundChannel(keys, defaultValue.toTypedArray()) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): DoubleArray {
val result = DoubleArray(keys.size)
for (i in keys.indices) {
result[i] = getValue(i)
}
return result
}
}
val channels = mutableMapOf<String, KeyframerChannel>()
fun loadFromJson(
file: File,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
require(file.exists()) {
"failed to load keyframer from json: '${file.absolutePath}' does not exist."
}
try {
loadFromJsonString(file.readText(), format, parameters, functions)
} catch (e: ExpressionException) {
throw ExpressionException("Error loading from '${file.path}': ${e.message ?: ""}")
}
}
fun loadFromJson(
url: URL,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
try {
loadFromJsonString(url.readText(), format, parameters, functions)
} catch (e: ExpressionException) {
throw ExpressionException("Error loading $format from '${url}': ${e.message ?: ""}")
} catch (e: IllegalStateException) {
throw ExpressionException("Error loading $format from '${url}': ${e.message ?: ""}")
}
}
fun loadFromJsonString(
json: String,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
when (format) {
KeyframerFormat.SIMPLE -> {
try {
val type = object : TypeToken<List<Map<String, Any>>>() {}.type
val keys: List<MutableMap<String, Any>> = Gson().fromJson(json, type)
loadFromKeyObjects(keys, parameters, functions)
} catch (e: JsonSyntaxException) {
error("Error parsing simple Keyframer data: ${e.cause?.message}")
} catch (e: NullPointerException) {
error("Error parsing simple Keyframer data: ${e.cause?.message}")
}
}
KeyframerFormat.FULL -> {
try {
val type = object : TypeToken<Map<String, Any>>() {}.type
val keys: Map<String, Any> = Gson().fromJson(json, type)
loadFromObjects(keys, parameters, functions)
} catch (e: JsonSyntaxException) {
error("Error parsing full Keyframer data: ${e.cause?.message}")
}
}
}
}
private val parameters = mutableMapOf<String, Double>()
private val prototypes = mutableMapOf<String, Map<String, Any>>()
fun loadFromObjects(
dict: Map<String, Any>,
externalParameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
this.parameters.clear()
this.parameters.putAll(externalParameters)
prototypes.clear()
@Suppress("UNCHECKED_CAST")
(dict["parameters"] as? Map<String, Any>)?.let { lp ->
for (entry in lp) {
this.parameters[entry.key] = try {
when (val candidate = entry.value) {
is Double -> candidate
is String -> evaluateExpression(candidate, parameters, functions)
?: error("could not evaluate expression: '$candidate'")
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
else -> error("unknown type for parameter '${entry.key}'")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in 'parameters': ${e.message ?: ""} ")
}
}
}
this.parameters.putAll(externalParameters)
@Suppress("UNCHECKED_CAST")
(dict["prototypes"] as? Map<String, Map<String, Any>>)?.let {
prototypes.putAll(it)
}
@Suppress("UNCHECKED_CAST")
(dict["keys"] as? List<Map<String, Any>>)?.let { keys ->
loadFromKeyObjects(keys, parameters, functions)
}
}
private fun resolvePrototype(prototypeNames: String): Map<String, Any> {
val prototypeTokens = prototypeNames.split(" ").map { it.trim() }.filter { it.isNotBlank() }
val prototypeRefs = prototypeTokens.mapNotNull { prototypes[it] }
val computed = mutableMapOf<String, Any>()
for (ref in prototypeRefs) {
computed.putAll(ref)
}
return computed
}
fun loadFromKeyObjects(
keys: List<Map<String, Any>>,
externalParameters: Map<String, Double>,
functions: FunctionExtensions
) {
if (externalParameters !== parameters) {
parameters.clear()
parameters.putAll(externalParameters)
}
var lastTime = 0.0
val channelDelegates = this::class.memberProperties
.mapNotNull {
@Suppress("UNCHECKED_CAST")
it as? KProperty1<Keyframer, Any>
}
.filter { it.isAccessible = true; it.getDelegate(this) is CompoundChannel }
.associate { Pair(it.name, it.getDelegate(this) as CompoundChannel) }
val channelKeys = channelDelegates.values.flatMap { channel ->
channel.keys.map { it }
}.toSet()
for (delegate in channelDelegates.values) {
delegate.reset()
}
val expressionContext = mutableMapOf<String, Double>()
expressionContext.putAll(parameters)
expressionContext["t"] = 0.0
fun easingFunctionFromName(easingCandidate: String): EasingFunction {
return when (easingCandidate) {
"linear" -> Easing.Linear.function
"back-in" -> Easing.BackIn.function
"back-out" -> Easing.BackOut.function
"back-in-out" -> Easing.BackInOut.function
"bounce-in" -> Easing.BounceIn.function
"bounce-out" -> Easing.BounceOut.function
"bounce-in-out" -> Easing.BounceInOut.function
"circ-in" -> Easing.CircIn.function
"circ-out" -> Easing.CircOut.function
"circ-in-out" -> Easing.CircInOut.function
"cubic-in" -> Easing.CubicIn.function
"cubic-out" -> Easing.CubicOut.function
"cubic-in-out" -> Easing.CubicInOut.function
"elastic-in" -> Easing.ElasticIn.function
"elastic-out" -> Easing.ElasticInOut.function
"elastic-in-out" -> Easing.ElasticOut.function
"expo-in" -> Easing.ExpoIn.function
"expo-out" -> Easing.ExpoOut.function
"expo-in-out" -> Easing.ExpoInOut.function
"quad-in" -> Easing.QuadIn.function
"quad-out" -> Easing.QuadOut.function
"quad-in-out" -> Easing.QuadInOut.function
"quart-in" -> Easing.QuartIn.function
"quart-out" -> Easing.QuartOut.function
"quart-in-out" -> Easing.QuartInOut.function
"quint-in" -> Easing.QuintIn.function
"quint-out" -> Easing.QuintOut.function
"quint-in-out" -> Easing.QuintInOut.function
"sine-in" -> Easing.SineIn.function
"sine-out" -> Easing.SineOut.function
"sine-in-out" -> Easing.SineInOut.function
"one" -> Easing.One.function
"zero" -> Easing.Zero.function
else -> error("unknown easing name '$easingCandidate'")
}
}
fun handleKey(key: Map<String, Any>, path: String) {
val prototype = (key["prototypes"] as? String)?.let {
resolvePrototype(it)
} ?: emptyMap()
val computed = mutableMapOf<String, Any>()
computed.putAll(prototype)
computed.putAll(key)
val time = try {
when (val candidate = computed["time"]) {
null -> lastTime
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" }
is Double -> candidate
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
else -> error("unknown time format for '$candidate'")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'time': ${e.message ?: ""}")
}
val duration = try {
when (val candidate = computed["duration"]) {
null -> 0.0
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" }
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
is Double -> candidate
else -> error("unknown duration type for '$candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'duration': ${e.message ?: ""}")
}
val easing = try {
when (val easingCandidate = computed["easing"]) {
null -> Easing.Linear.function
is String -> easingFunctionFromName(easingCandidate)
else -> error("unknown easing for '$easingCandidate'")
}
} catch (e: IllegalStateException) {
throw ExpressionException("error in $path.'easing': ${e.message ?: ""}")
}
val envelope = try {
when (val candidate = computed["envelope"]) {
null -> defaultEnvelope
is DoubleArray -> candidate
is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
else -> error("unknown envelope for '$candidate")
}
} catch (e: IllegalStateException) {
throw ExpressionException("error in $path.'envelope': ${e.message ?: ""}")
}
val reservedKeys = setOf("time", "easing", "envelope")
for (channelCandidate in computed.filter { it.key !in reservedKeys }) {
if (channelCandidate.key in channelKeys) {
val channel = channels.getOrPut(channelCandidate.key) {
KeyframerChannel()
}
val lastValue = channel.lastValue() ?: 0.0
expressionContext["v"] = lastValue
val lastTime = (channel.lastTime()) ?: 0.0
expressionContext["d"] = time - lastTime
if (channelCandidate.value is Map<*, *>) {
@Suppress("UNCHECKED_CAST")
val valueMap = channelCandidate.value as Map<String, Any>
val value = try {
when (val candidate = valueMap["value"]) {
null -> error("no value for '${channelCandidate.key}'")
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
}
val dictEasing = when (val candidate = valueMap["easing"]) {
null -> easing
is String -> easingFunctionFromName(candidate)
else -> error("unknown easing for '$candidate'")
}
val dictEnvelope = when (val candidate = valueMap["envelope"]) {
null -> envelope
is DoubleArray -> candidate
is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
else -> error("unknown envelope for '$candidate")
}
val dictDuration = try {
when (val candidate = valueMap["duration"]) {
null -> null
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
}
if (dictDuration != null) {
if (dictDuration <= 0.0) {
channel.add(
max(lastTime, time + dictDuration),
lastValue,
Easing.Linear.function,
defaultEnvelope
)
channel.add(time, value, dictEasing, dictEnvelope)
} else {
channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope)
channel.add(time + dictDuration, value, dictEasing, dictEnvelope)
}
} else {
channel.add(time, value, dictEasing, dictEnvelope)
}
} else {
val value = try {
when (val candidate = channelCandidate.value) {
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
}
channel.add(time, value, easing, envelope)
}
}
}
lastTime = time + duration
expressionContext["t"] = lastTime
}
for ((index, key) in keys.withIndex()) {
handleKey(key, "keys[$index]")
}
}
}