469 lines
20 KiB
Kotlin
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]")
|
|
}
|
|
}
|
|
}
|