[orx-keyframer] Add envelopes, remove repetitions

This commit is contained in:
Edwin Jakobs
2021-01-09 19:46:58 +01:00
parent 3e23d77fdd
commit 829e0bbf7e
10 changed files with 188 additions and 164 deletions

View File

@@ -8,10 +8,9 @@ fun main() = application {
program {
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
val radius by DoubleChannel("x")
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-simple-repetitions-01.json")))
animation.loadFromJson(URL(resourceUrl("/demo-envelope-01.json")))
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
@@ -19,7 +18,7 @@ fun main() = application {
}
extend {
animation(seconds)
drawer.circle(animation.position, animation.radius)
drawer.circle(animation.position, 100.0)
}
}
}

View File

@@ -0,0 +1,25 @@
[
{
"time": 0.0,
"x": 320.0,
"y": 240.0
},
{
"time": 10.0,
"easing": "cubic-in-out",
"x": {
"envelope": [0.5, 1.0],
"value": 0.0
},
"y": {
"envelope": [0.4, 1.0],
"value": 0.0
}
},
{
"time": 20.0,
"x": 640.0,
"y": 480.0,
"easing": "cubic-in-out"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"time": 0.0,
"x": 320.0,
"y": 240.0,
"radius": 0.0
},
{
"time": 3.0,
"repeat": {
"count": 5,
"keys": [
{
"duration": 1.0,
"easing": "cubic-in-out",
"x": 10.0,
"y": 4.0,
"radius": 400
},
{
"duration": 1.0,
"easing": "cubic-in-out",
"x": 630.0,
"y": 470.0,
"radius": 40
}
]
}
}
]

View File

@@ -20,8 +20,8 @@ DECIMAL : 'Decimal';
STRING : 'String';
// Identifiers
ID : [_]*[a-zA-Z][A-Za-z0-9_]* ;
FUNCTION_ID : [_]*[a-z][A-Za-z0-9_]* ;
ID : [$_]*[a-zA-Z][A-Za-z0-9_]* ;
FUNCTION_ID : [$_]*[a-z][A-Za-z0-9_]* ;
// Literals

View File

@@ -3,7 +3,9 @@ package org.openrndr.extra.keyframer
import org.antlr.v4.runtime.*
import org.antlr.v4.runtime.tree.ParseTreeWalker
import org.antlr.v4.runtime.tree.TerminalNode
import org.openrndr.extra.keyframer.antlr.*
import org.openrndr.extra.keyframer.antlr.KeyLangLexer
import org.openrndr.extra.keyframer.antlr.KeyLangParser
import org.openrndr.extra.keyframer.antlr.KeyLangParserBaseListener
import org.openrndr.extra.noise.uniform
import org.openrndr.math.*
import java.util.*
@@ -357,20 +359,6 @@ fun evaluateExpression(
functions: FunctionExtensions = FunctionExtensions.EMPTY
): Double? {
val lexer = KeyLangLexer(CharStreams.fromString(input))
//
// lexer.removeErrorListeners()
// lexer.addErrorListener(object : BaseErrorListener() {
// override fun syntaxError(
// recognizer: Recognizer<*, *>?,
// offendingSymbol: Any?,
// line: Int,
// charPositionInLine: Int,
// msg: String?,
// e: RecognitionException?
// ) {
// println("syntax error!")
// }
// })
val parser = KeyLangParser(CommonTokenStream(lexer))
parser.removeErrorListeners()
parser.addErrorListener(object : BaseErrorListener() {

View File

@@ -2,14 +2,12 @@ package org.openrndr.extra.keyframer
import org.openrndr.extras.easing.Easing
import org.openrndr.extras.easing.EasingFunction
import org.openrndr.math.map
class Key(val time: Double, val value: Double, val easing: EasingFunction)
internal val defaultEnvelope = doubleArrayOf(0.0, 1.0)
class Key(val time: Double, val value: Double, val easing: EasingFunction, val envelope: DoubleArray = defaultEnvelope)
enum class Hold {
HoldNone,
HoldSet,
HoldAll
}
class KeyframerChannel {
val keys = mutableListOf<Key>()
@@ -18,14 +16,17 @@ class KeyframerChannel {
return 0.0
}
fun add(time: Double, value: Double?, easing: EasingFunction = Easing.Linear.function, jump: Hold = Hold.HoldNone) {
if (jump == Hold.HoldAll || (jump == Hold.HoldSet && value != null)) {
lastValue()?.let {
keys.add(Key(time, it, Easing.Linear.function))
}
fun add(
time: Double,
value: Double?,
easing: EasingFunction = Easing.Linear.function,
envelope: DoubleArray = defaultEnvelope
) {
require(envelope.size >= 2) {
"envelope should contain at least 2 entries"
}
value?.let {
keys.add(Key(time, it, easing))
keys.add(Key(time, it, easing, envelope))
}
}
@@ -65,7 +66,8 @@ class KeyframerChannel {
val rightKey = keys[rightIndex]
val leftKey = keys[leftIndex]
val t0 = (time - leftKey.time) / (rightKey.time - leftKey.time)
val e0 = rightKey.easing(t0, 0.0, 1.0, 1.0)
val te = t0.map(rightKey.envelope[0], rightKey.envelope[1], 0.0, 1.0, clamp = true)
val e0 = rightKey.easing(te, 0.0, 1.0, 1.0)
leftKey.value * (1.0 - e0) + rightKey.value * (e0)
}
}

View File

@@ -14,12 +14,8 @@ class KeyframerChannelQuaternion {
return 0.0
}
fun add(time: Double, value: Quaternion?, easing: EasingFunction = Easing.Linear.function, jump: Hold = Hold.HoldNone) {
if (jump == Hold.HoldAll || (jump == Hold.HoldSet && value != null)) {
lastValue()?.let {
keys.add(KeyQuaternion(time, it, Easing.Linear.function))
}
}
fun add(time: Double, value: Quaternion?, easing: EasingFunction = Easing.Linear.function) {
value?.let {
keys.add(KeyQuaternion(time, it, easing))
}

View File

@@ -13,12 +13,7 @@ class KeyframerChannelVector3 {
return 0.0
}
fun add(time: Double, value: Vector3?, easing: EasingFunction = Easing.Linear.function, jump: Hold = Hold.HoldNone) {
if (jump == Hold.HoldAll || (jump == Hold.HoldSet && value != null)) {
lastValue()?.let {
keys.add(KeyVector3(time, it, Easing.Linear.function))
}
}
fun add(time: Double, value: Vector3?, easing: EasingFunction = Easing.Linear.function) {
value?.let {
keys.add(KeyVector3(time, it, easing))
}

View File

@@ -14,7 +14,6 @@ import java.lang.IllegalStateException
import java.lang.NullPointerException
import java.net.URL
import kotlin.math.max
import kotlin.math.roundToInt
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
@@ -258,7 +257,6 @@ open class Keyframer {
expressionContext.putAll(parameters)
expressionContext["t"] = 0.0
fun easingFunctionFromName(easingCandidate: String): EasingFunction {
return when (easingCandidate) {
"linear" -> Easing.Linear.function
@@ -346,9 +344,20 @@ open class Keyframer {
throw ExpressionException("error in $path.'easing': ${e.message ?: ""}")
}
val hold = Hold.HoldNone
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", "hold")
val reservedKeys = setOf("time", "easing", "envelope")
for (channelCandidate in computed.filter { it.key !in reservedKeys }) {
if (channelCandidate.key in channelKeys) {
@@ -385,6 +394,14 @@ open class Keyframer {
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
@@ -400,12 +417,14 @@ open class Keyframer {
if (dictDuration != null) {
if (dictDuration <= 0.0) {
channel.add(max(lastTime, time + dictDuration), lastValue, Easing.Linear.function, hold)
channel.add(time, value, dictEasing, hold)
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, hold)
channel.add(time + dictDuration, value, dictEasing, hold)
channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope)
channel.add(time + dictDuration, value, dictEasing, dictEnvelope)
}
} else {
channel.add(time, value, dictEasing, dictEnvelope)
}
} else {
@@ -420,39 +439,12 @@ open class Keyframer {
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
}
channel.add(time, value, easing, hold)
channel.add(time, value, easing, envelope)
}
}
}
lastTime = time + duration
expressionContext["t"] = lastTime
if (computed.containsKey("repeat")) {
@Suppress("UNCHECKED_CAST")
val repeatObject = computed["repeat"] as? Map<String, Any> ?: error("'repeat' should be a map")
val count = try {
when (val candidate = repeatObject["count"]) {
null -> 1
is Int -> candidate
is Double -> candidate.toInt()
is String -> evaluateExpression(candidate, expressionContext, functions)?.roundToInt()
?: error("cannot evaluate expression for count: '$candidate'")
else -> error("unknown value type for count: '$candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.repeat.'count': ${e.message ?: ""}")
}
@Suppress("UNCHECKED_CAST")
val repeatKeys = repeatObject["keys"] as? List<Map<String, Any>> ?: error("no repeat keys")
for (i in 0 until count) {
expressionContext["rep"] = i.toDouble()
for (repeatKey in repeatKeys) {
handleKey(repeatKey, "$path.repeat")
}
}
}
}
for ((index, key) in keys.withIndex()) {