[orx-expression-evaluator, orx-keyframer] Split expression evaluator from orx-keyframer

This commit is contained in:
Edwin Jakobs
2023-03-29 13:34:55 +02:00
parent 606e56be3d
commit 8940fb7520
20 changed files with 685 additions and 199 deletions

View File

@@ -0,0 +1,2 @@
*.tokens
gen/

View File

@@ -0,0 +1,82 @@
# orx-expression-evaluator
Tools to evaluate expression strings
# Expression evaluator
```kotlin
val expression = "x + y"
val constants = mapOf("x" to 1.0, "y" to 2.0)
evaluateExpression(expression, constants)
```
## Built-in expression functions
Unary functions:
* `abs(x)`
* `acos(x)`
* `asin(x)`
* `atan(x)`
* `ceil(x)`
* `cos(x)`
* `degrees(x)`
* `exps(x)`
* `floor(x)`
* `radians(x)`
* `round(x)`
* `saturate(x)`, clamp x to [0.0, 1.0]
* `sqrt(x)`
* `tan(x)`
Binary functions:
* `atan2(x, y)`
* `length(x, y)`, the Euclidean length of the vector (x,y)
* `max(x, y)`,
* `min(x, y)`,
* `pow(x, n)`
* `random(x, y)`, return a random number in [x, y)
Ternary functions:
* `length(x, y, z)`, the Euclidean length of the vector (x, y, z)
* `max(x, y, z)`
* `min(x, y, z)`
* `mix(l, r, f)`
* `smoothstep(e0, e1, x)`
* `sum(x, y, z)`
Quaternary functions:
* `length(x, y, z, w)`, the Euclidean length of the vector (x, y, z)
* `max(a, b, c, d)`
* `min(a, b, c, d)`
* `sum(a, b, c, d)`
Quinary functions:
* `map(x0, x1, y0, y1, v)`
* `max(a, b, c, d, e)`
* `min(a, b, c, d, e)`
* `sum(a, b, c, d, e)`
# Compiled functions
```kotlin
val expression = "x * 5.0 + cos(x)"
val f = compileFunction1(expression, "x")
f(0.0)
```
```kotlin
val expression = "x * 5.0 + cos(x) * y"
val f = compileFunction2(expression, "x", "y")
f(0.0, 0.4)
```
# Property delegates
```kotlin
val constants = mutableMapOf("width" to 300.0)
val settings = object {
var xExpression = "cos(t) * 50.0 + width / 2.0"
}
val xFunction by watchingExpression1(settings::xExpression, "t", constants)
xFunction(1.0)
```

View File

@@ -1,6 +1,14 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("org.openrndr.extra.convention.kotlin-jvm") org.openrndr.extra.convention.`kotlin-jvm`
id("antlr") antlr
}
tasks.generateGrammarSource {
maxHeapSize = "64m"
arguments.addAll(listOf("-visitor", "-long-messages", "-package", "org.openrndr.extra.expressions.antlr"))
outputDirectory = file("${project.buildDir}/generated-src/antlr/org/openrndr/extra/expressions/antlr")
} }
sourceSets { sourceSets {
@@ -11,36 +19,23 @@ sourceSets {
} }
} }
tasks.test { tasks.withType<KotlinCompile> {
useJUnitPlatform { kotlinOptions.freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
includeEngines("spek2")
}
}
generateGrammarSource {
maxHeapSize = "64m"
arguments += ["-visitor", "-long-messages"]
outputDirectory = file("${project.buildDir}/generated-src/antlr/org/openrndr/extra/keyframer/antlr".toString())
} }
dependencies { dependencies {
antlr(libs.antlr.core) antlr(libs.antlr.core)
implementation(libs.antlr.runtime) implementation(libs.antlr.runtime)
implementation(project(":orx-noise"))
implementation(project(":orx-easing"))
implementation(libs.openrndr.application) implementation(libs.openrndr.application)
implementation(libs.openrndr.math) implementation(libs.openrndr.math)
implementation(libs.gson) implementation(libs.kotlin.coroutines)
implementation(libs.kotlin.reflect) implementation(project(":orx-property-watchers"))
implementation(project(":orx-noise"))
testImplementation(libs.kluent) testImplementation(libs.kluent)
testImplementation(libs.spek.dsl)
testRuntimeOnly(libs.spek.junit5)
testRuntimeOnly(libs.kotlin.reflect)
demoImplementation(project(":orx-jvm:orx-panel"))
demoImplementation(project(":orx-jvm:orx-gui")) demoImplementation(project(":orx-jvm:orx-gui"))
} }
tasks.getByName("compileKotlin").dependsOn("generateGrammarSource") tasks.getByName("compileKotlin").dependsOn("generateGrammarSource")
tasks.getByName("compileDemoKotlin").dependsOn("generateDemoGrammarSource") tasks.getByName("compileDemoKotlin").dependsOn("generateDemoGrammarSource")
tasks.getByName("compileTestKotlin").dependsOn("generateTestGrammarSource") tasks.getByName("compileTestKotlin").dependsOn("generateTestGrammarSource")
tasks.getByName("sourcesJar").dependsOn("generateGrammarSource") tasks.getByName("sourcesJar").dependsOn("generateGrammarSource")

View File

@@ -1,7 +1,7 @@
import org.openrndr.application import org.openrndr.application
import org.openrndr.extra.expressions.evaluateExpression
import org.openrndr.extra.gui.GUI import org.openrndr.extra.gui.GUI
import org.openrndr.extra.gui.addTo import org.openrndr.extra.gui.addTo
import org.openrndr.extra.keyframer.evaluateExpression
import org.openrndr.extra.parameters.TextParameter import org.openrndr.extra.parameters.TextParameter
fun main() { fun main() {
@@ -13,9 +13,11 @@ fun main() {
val settings = object { val settings = object {
@TextParameter("x expression", order = 10) @TextParameter("x expression", order = 10)
var xExpression = "cos(t) * 50.0 + width / 2.0" var xExpression = "cos(t) * 50.0 + width / 2.0"
@TextParameter("y expression", order = 20) @TextParameter("y expression", order = 20)
var yExpression = "sin(t) * 50.0 + height / 2.0" var yExpression = "sin(t) * 50.0 + height / 2.0"
@TextParameter("radius expression", order = 30 )
@TextParameter("radius expression", order = 30)
var radiusExpression = "cos(t) * 50.0 + 50.0" var radiusExpression = "cos(t) * 50.0 + 50.0"
}.addTo(gui) }.addTo(gui)
@@ -23,10 +25,15 @@ fun main() {
extend { extend {
//gui.visible = mouse.position.x < 200.0 //gui.visible = mouse.position.x < 200.0
val expressionContext = mapOf("t" to seconds, "width" to drawer.bounds.width, "height" to drawer.bounds.height) val expressionContext =
mapOf("t" to seconds, "width" to drawer.bounds.width, "height" to drawer.bounds.height)
fun eval(expression: String) : Double = fun eval(expression: String): Double =
try { evaluateExpression(expression, expressionContext) ?: 0.0 } catch (e: Throwable) { 0.0 } try {
evaluateExpression(expression, expressionContext) ?: 0.0
} catch (e: Throwable) {
0.0
}
val x = eval(settings.xExpression) val x = eval(settings.xExpression)
val y = eval(settings.yExpression) val y = eval(settings.yExpression)

View File

@@ -0,0 +1,45 @@
import org.openrndr.application
import org.openrndr.extra.expressions.evaluateExpression
import org.openrndr.extra.expressions.watchingExpression1
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.gui.addTo
import org.openrndr.extra.parameters.TextParameter
/**
* Improved version of DemoExpressionEvaluator01, it uses [watchingExpression1] to automatically convert an expression
* string into a function with a parameter "t".
*/
fun main() {
application {
program {
val gui = GUI()
gui.compartmentsCollapsedByDefault = false
// the constants used in our expressions
val constants = mutableMapOf("width" to drawer.width.toDouble(), "height" to drawer.height.toDouble())
val settings = object {
@TextParameter("x expression", order = 10)
var xExpression = "cos(t) * 50.0 + width / 2.0"
@TextParameter("y expression", order = 20)
var yExpression = "sin(t) * 50.0 + height / 2.0"
@TextParameter("radius expression", order = 30)
var radiusExpression = "cos(t) * 50.0 + 50.0"
}.addTo(gui)
val xFunction by watchingExpression1(settings::xExpression, "t", constants)
val yFunction by watchingExpression1(settings::yExpression, "t", constants)
val radiusFunction by watchingExpression1(settings::radiusExpression, "t", constants)
extend(gui)
extend {
val x = xFunction(seconds)
val y = yFunction(seconds)
val radius = radiusFunction(seconds)
drawer.circle(x, y, radius)
}
}
}
}

View File

@@ -1,9 +1,5 @@
lexer grammar KeyLangLexer; lexer grammar KeyLangLexer;
@header {
package org.openrndr.extra.keyframer.antlr;
}
channels { WHITESPACE } channels { WHITESPACE }
// Whitespace // Whitespace

View File

@@ -1,10 +1,6 @@
parser grammar KeyLangParser; parser grammar KeyLangParser;
@header {
package org.openrndr.extra.keyframer.antlr;
}
options { tokenVocab=KeyLangLexer; } options { tokenVocab=KeyLangLexer; }
keyLangFile : lines=line+ ; keyLangFile : lines=line+ ;

View File

@@ -0,0 +1,124 @@
package org.openrndr.extra.expressions
import org.antlr.v4.runtime.tree.ParseTreeWalker
/**
* Compile a (Double)->Double function from an expression string
* @param expression the expression string to be compiled
* @param parameter0 the name of the first parameter
* @param constants a map of named constant values that can be referred from the expression
* @param functions a map of named functions that can be invoked from the expression
* @param error in case the expression fails to compile or evaluate, this function is invoked instead
*/
fun compileFunction1(
expression: String,
parameter0: String,
constants: Map<String, Double> = mapOf(),
functions: FunctionExtensions = FunctionExtensions.EMPTY,
error: (Double) -> Double = { 0.0 },
): (Double) -> Double {
require(!constants.containsKey(parameter0))
try {
val root = expressionRoot(expression)
val variables = mutableMapOf<String, Double>()
variables.putAll(constants)
val listener = ExpressionListener(functions, variables)
return { p0 ->
variables[parameter0] = p0
try {
ParseTreeWalker.DEFAULT.walk(listener, root)
listener.lastExpressionResult ?: error("no result")
} catch (e: ExpressionException) {
error(p0)
}
}
} catch (e: ExpressionException) {
return error
}
}
/**
* Compile a (Double, Double)->Double function from an expression string
* @param expression the expression string to be compiled
* @param parameter0 the name of the first parameter
* @param parameter1 the name of the second parameter
* @param constants a map of named constant values that can be referred from the expression
* @param functions a map of named functions that can be invoked from the expression
* @param error in case the expression fails to compile or evaluate, this function is invoked instead
*/
fun compileFunction2(
expression: String,
parameter0: String,
parameter1: String,
constants: Map<String, Double> = mapOf(),
functions: FunctionExtensions = FunctionExtensions.EMPTY,
error: (p0: Double, p1: Double) -> Double = { _, _ -> 0.0 },
): (p0: Double, p1: Double) -> Double {
require(!constants.containsKey(parameter0))
require(!constants.containsKey(parameter1))
try {
val root = expressionRoot(expression)
val variables = mutableMapOf<String, Double>()
variables.putAll(constants)
val listener = ExpressionListener(functions, variables)
return { p0, p1 ->
variables[parameter0] = p0
variables[parameter1] = p1
try {
ParseTreeWalker.DEFAULT.walk(listener, root)
listener.lastExpressionResult ?: error("no result")
} catch (e: ExpressionException) {
error(p0, p1)
}
}
} catch (e: ExpressionException) {
return error
}
}
/**
* Compile a (Double, Double, Double)->Double function from an expression string
* @param expression the expression string to be compiled
* @param parameter0 the name of the first parameter
* @param parameter1 the name of the second parameter
* @param parameter2 the name of the third parameter
* @param constants a map of named constant values that can be referred from the expression
* @param functions a map of named functions that can be invoked from the expression
* @param error in case the expression fails to compile or evaluate, this function is invoked instead
*/
fun compileFunction3(
expression: String,
parameter0: String,
parameter1: String,
parameter2: String,
constants: Map<String, Double> = mapOf(),
functions: FunctionExtensions = FunctionExtensions.EMPTY,
error: (p0: Double, p1: Double, p2: Double) -> Double = { _, _, _ -> 0.0 }
): (p0: Double, p1: Double, p2: Double) -> Double {
require(!constants.containsKey(parameter0))
require(!constants.containsKey(parameter1))
require(!constants.containsKey(parameter2))
try {
val root = expressionRoot(expression)
val variables = mutableMapOf<String, Double>()
variables.putAll(constants)
val listener = ExpressionListener(functions, variables)
return { p0, p1, p2 ->
variables[parameter0] = p0
variables[parameter1] = p1
variables[parameter2] = p2
try {
ParseTreeWalker.DEFAULT.walk(listener, root)
listener.lastExpressionResult ?: error("no result")
} catch (e: ExpressionException) {
error(p0, p1, p2)
}
}
} catch(e: ExpressionException) {
return error
}
}

View File

@@ -0,0 +1,40 @@
package org.openrndr.extra.expressions
import org.openrndr.extra.propertywatchers.watchingProperty
import kotlin.reflect.KProperty0
fun watchingExpression1(
expressionProperty: KProperty0<String>,
parameter0: String = "x",
constants: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY,
error: (p0: Double) -> Double = { 0.0 }
) =
watchingProperty(expressionProperty) {
compileFunction1(it, parameter0, constants, functions, error)
}
fun watchingExpression2(
expressionProperty: KProperty0<String>,
parameter0: String = "x",
parameter1: String = "y",
constants: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY,
error: (p0: Double, p1: Double) -> Double = { _, _ -> 0.0 }
) =
watchingProperty(expressionProperty) {
compileFunction2(it, parameter0, parameter1, constants, functions, error)
}
fun watchingExpression3(
expressionProperty: KProperty0<String>,
parameter0: String = "x",
parameter1: String = "y",
parameter2: String = "z",
constants: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY,
error: (p0: Double, p1: Double, p2: Double) -> Double = { _, _, _ -> 0.0 }
) =
watchingProperty(expressionProperty) {
compileFunction3(it, parameter0, parameter1, parameter2, constants, functions, error)
}

View File

@@ -1,11 +1,11 @@
package org.openrndr.extra.keyframer package org.openrndr.extra.expressions
import org.antlr.v4.runtime.* import org.antlr.v4.runtime.*
import org.antlr.v4.runtime.tree.ParseTreeWalker import org.antlr.v4.runtime.tree.ParseTreeWalker
import org.antlr.v4.runtime.tree.TerminalNode import org.antlr.v4.runtime.tree.TerminalNode
import org.openrndr.extra.keyframer.antlr.KeyLangLexer import org.openrndr.extra.expressions.antlr.KeyLangLexer
import org.openrndr.extra.keyframer.antlr.KeyLangParser import org.openrndr.extra.expressions.antlr.KeyLangParser
import org.openrndr.extra.keyframer.antlr.KeyLangParserBaseListener import org.openrndr.extra.expressions.antlr.KeyLangParserBaseListener
import org.openrndr.extra.noise.uniform import org.openrndr.extra.noise.uniform
import org.openrndr.math.* import org.openrndr.math.*
import java.util.* import java.util.*
@@ -19,12 +19,12 @@ typealias Function4 = (Double, Double, Double, Double) -> Double
typealias Function5 = (Double, Double, Double, Double, Double) -> Double typealias Function5 = (Double, Double, Double, Double, Double) -> Double
class FunctionExtensions( class FunctionExtensions(
val functions0: Map<String, Function0> = emptyMap(), val functions0: Map<String, Function0> = emptyMap(),
val functions1: Map<String, Function1> = emptyMap(), val functions1: Map<String, Function1> = emptyMap(),
val functions2: Map<String, Function2> = emptyMap(), val functions2: Map<String, Function2> = emptyMap(),
val functions3: Map<String, Function3> = emptyMap(), val functions3: Map<String, Function3> = emptyMap(),
val functions4: Map<String, Function4> = emptyMap(), val functions4: Map<String, Function4> = emptyMap(),
val functions5: Map<String, Function5> = emptyMap() val functions5: Map<String, Function5> = emptyMap()
) { ) {
companion object { companion object {
val EMPTY = FunctionExtensions() val EMPTY = FunctionExtensions()
@@ -41,11 +41,14 @@ internal enum class IDType {
FUNCTION5 FUNCTION5
} }
internal class ExpressionListener(val functions: FunctionExtensions = FunctionExtensions.EMPTY) : internal class ExpressionListener(
KeyLangParserBaseListener() { val functions: FunctionExtensions = FunctionExtensions.EMPTY,
val constants: Map<String, Double> = mapOf()
) :
KeyLangParserBaseListener() {
val doubleStack = Stack<Double>() val doubleStack = Stack<Double>()
val functionStack = Stack<(DoubleArray) -> Double>() val functionStack = Stack<(DoubleArray) -> Double>()
val variables = mutableMapOf<String, Double>()
val idTypeStack = Stack<IDType>() val idTypeStack = Stack<IDType>()
var lastExpressionResult: Double? = null var lastExpressionResult: Double? = null
@@ -61,10 +64,10 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
lastExpressionResult = result lastExpressionResult = result
} }
override fun exitAssignment(ctx: KeyLangParser.AssignmentContext) { // override fun exitAssignment(ctx: KeyLangParser.AssignmentContext) {
val value = doubleStack.pop() // val value = doubleStack.pop()
variables[ctx.ID()?.text ?: error("buh")] = value // variables[ctx.ID()?.text ?: error("buh")] = value
} // }
override fun exitMinusExpression(ctx: KeyLangParser.MinusExpressionContext) { override fun exitMinusExpression(ctx: KeyLangParser.MinusExpressionContext) {
val op = doubleStack.pop() val op = doubleStack.pop()
@@ -248,114 +251,145 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
doubleStack.push(node.text.toDouble()) doubleStack.push(node.text.toDouble())
} }
if (type == KeyLangParser.ID) { if (type == KeyLangParser.ID) {
val name = node.text.replace("`","") val name = node.text.replace("`", "")
@Suppress("DIVISION_BY_ZERO") @Suppress("DIVISION_BY_ZERO")
when (val idType = idTypeStack.pop()) { when (val idType = idTypeStack.pop()) {
IDType.VARIABLE -> doubleStack.push( IDType.VARIABLE -> doubleStack.push(
when (name) { when (name) {
"PI" -> PI "PI" -> PI
else -> variables[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0) else -> constants[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0)
} }
) )
IDType.FUNCTION0 -> { IDType.FUNCTION0 -> {
val function: (DoubleArray) -> Double = val function: (DoubleArray) -> Double =
when (name) { when (name) {
"random" -> { _ -> Double.uniform(0.0, 1.0) } "random" -> { _ -> Double.uniform(0.0, 1.0) }
else -> functions.functions0[name]?.let { { _: DoubleArray -> it.invoke() } } else -> functions.functions0[name]?.let { { _: DoubleArray -> it.invoke() } }
?: errorValue( ?: errorValue(
"unresolved function: '${name}()'" "unresolved function: '${name}()'"
) { _ -> error("this is the error function") } ) { _ -> error("this is the error function") }
} }
functionStack.push(function) functionStack.push(function)
} }
IDType.FUNCTION1 -> { IDType.FUNCTION1 -> {
val function: (DoubleArray) -> Double = val function: (DoubleArray) -> Double =
when (name) { when (name) {
"sqrt" -> { x -> sqrt(x[0]) } "sqrt" -> { x -> sqrt(x[0]) }
"radians" -> { x -> Math.toRadians(x[0]) } "radians" -> { x -> Math.toRadians(x[0]) }
"degrees" -> { x -> Math.toDegrees(x[0]) } "degrees" -> { x -> Math.toDegrees(x[0]) }
"cos" -> { x -> cos(x[0]) } "cos" -> { x -> cos(x[0]) }
"sin" -> { x -> sin(x[0]) } "sin" -> { x -> sin(x[0]) }
"tan" -> { x -> tan(x[0]) } "tan" -> { x -> tan(x[0]) }
"atan" -> { x -> atan(x[0]) } "atan" -> { x -> atan(x[0]) }
"acos" -> { x -> acos(x[0]) } "acos" -> { x -> acos(x[0]) }
"asin" -> { x -> asin(x[0]) } "asin" -> { x -> asin(x[0]) }
"exp" -> { x -> exp(x[0]) } "exp" -> { x -> exp(x[0]) }
"abs" -> { x -> abs(x[0]) } "abs" -> { x -> abs(x[0]) }
"floor" -> { x -> floor(x[0]) } "floor" -> { x -> floor(x[0]) }
"round" -> { x -> round(x[0]) } "round" -> { x -> round(x[0]) }
"ceil" -> { x -> ceil(x[0]) } "ceil" -> { x -> ceil(x[0]) }
"saturate" -> { x -> x[0].coerceIn(0.0, 1.0) } "saturate" -> { x -> x[0].coerceIn(0.0, 1.0) }
else -> functions.functions1[name]?.let { { x: DoubleArray -> it.invoke(x[0]) } } else -> functions.functions1[name]?.let { { x: DoubleArray -> it.invoke(x[0]) } }
?: errorValue( ?: errorValue(
"unresolved function: '${name}(x0)'" "unresolved function: '${name}(x0)'"
) { _ -> error("this is the error function") } ) { _ -> error("this is the error function") }
} }
functionStack.push(function) functionStack.push(function)
} }
IDType.FUNCTION2 -> { IDType.FUNCTION2 -> {
val function: (DoubleArray) -> Double = val function: (DoubleArray) -> Double =
when (name) { when (name) {
"max" -> { x -> max(x[0], x[1]) } "max" -> { x -> max(x[0], x[1]) }
"min" -> { x -> min(x[0], x[1]) } "min" -> { x -> min(x[0], x[1]) }
"pow" -> { x -> x[0].pow(x[1]) } "pow" -> { x -> x[0].pow(x[1]) }
"mod" -> { x -> x[0].mod(x[1]) } "mod" -> { x -> x[0].mod(x[1]) }
"atan2" -> { x -> atan2(x[0], x[1]) } "atan2" -> { x -> atan2(x[0], x[1]) }
"random" -> { x -> Double.uniform(x[0], x[1]) } "random" -> { x -> Double.uniform(x[0], x[1]) }
"length" -> { x -> Vector2(x[0], x[1]).length } "length" -> { x -> Vector2(x[0], x[1]).length }
else -> functions.functions2[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1]) } } else -> functions.functions2[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1]) } }
?: errorValue( ?: errorValue(
"unresolved function: '${name}(x0, x1)'" "unresolved function: '${name}(x0, x1)'"
) { _ -> error("this is the error function") } ) { _ -> error("this is the error function") }
} }
functionStack.push(function) functionStack.push(function)
} }
IDType.FUNCTION3 -> { IDType.FUNCTION3 -> {
val function: (DoubleArray) -> Double = val function: (DoubleArray) -> Double =
when (name) { when (name) {
"mix" -> { x -> mix(x[0], x[1], x[2]) } "mix" -> { x -> mix(x[0], x[1], x[2]) }
"min" -> { x -> x.minOrNull()!! } "min" -> { x -> x.minOrNull()!! }
"max" -> { x -> x.maxOrNull()!! } "max" -> { x -> x.maxOrNull()!! }
"sum" -> { x -> x.sum() } "sum" -> { x -> x.sum() }
"smoothstep" -> { x -> smoothstep(x[0], x[1], x[2]) } "smoothstep" -> { x -> smoothstep(x[0], x[1], x[2]) }
"length" -> { x -> Vector3(x[0], x[1], x[2]).length } "length" -> { x -> Vector3(x[0], x[1], x[2]).length }
else -> functions.functions3[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2]) } } else -> functions.functions3[name]?.let {
?: errorValue( { x: DoubleArray ->
"unresolved function: '${name}(x0, x1, x2)'" it.invoke(
) { _ -> error("this is the error function") } x[0],
x[1],
x[2]
)
}
} }
?: errorValue(
"unresolved function: '${name}(x0, x1, x2)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function) functionStack.push(function)
} }
IDType.FUNCTION4 -> { IDType.FUNCTION4 -> {
val function: (DoubleArray) -> Double = val function: (DoubleArray) -> Double =
when (name) { when (name) {
"min" -> { x -> x.minOrNull()!! } "min" -> { x -> x.minOrNull()!! }
"max" -> { x -> x.maxOrNull()!! } "max" -> { x -> x.maxOrNull()!! }
"sum" -> { x -> x.sum() } "sum" -> { x -> x.sum() }
else -> functions.functions4[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2], x[3]) } } else -> functions.functions4[name]?.let {
?: errorValue( { x: DoubleArray ->
"unresolved function: '${name}(x0, x1, x2, x3)'" it.invoke(
) { _ -> error("this is the error function") } x[0],
x[1],
x[2],
x[3]
)
}
} }
?: errorValue(
"unresolved function: '${name}(x0, x1, x2, x3)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function) functionStack.push(function)
} }
IDType.FUNCTION5 -> { IDType.FUNCTION5 -> {
val function: (DoubleArray) -> Double = val function: (DoubleArray) -> Double =
when (name) { when (name) {
"min" -> { x -> x.minOrNull()!! } "min" -> { x -> x.minOrNull()!! }
"max" -> { x -> x.maxOrNull()!! } "max" -> { x -> x.maxOrNull()!! }
"sum" -> { x -> x.sum() } "sum" -> { x -> x.sum() }
"map" -> { x -> map(x[0], x[1], x[2], x[3], x[4]) } "map" -> { x -> map(x[0], x[1], x[2], x[3], x[4]) }
else -> functions.functions5[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2], x[3], x[4]) } } else -> functions.functions5[name]?.let {
?: errorValue( { x: DoubleArray ->
"unresolved function: '${name}(x0, x1, x2, x3, x4)'" it.invoke(
) { _ -> error("this is the error function") } x[0],
x[1],
x[2],
x[3],
x[4]
)
}
} }
?: errorValue(
"unresolved function: '${name}(x0, x1, x2, x3, x4)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function) functionStack.push(function)
} }
else -> error("unsupported id-type $idType") else -> error("unsupported id-type $idType")
} }
} }
@@ -365,33 +399,86 @@ internal class ExpressionListener(val functions: FunctionExtensions = FunctionEx
class ExpressionException(message: String) : RuntimeException(message) class ExpressionException(message: String) : RuntimeException(message)
fun evaluateExpression( fun evaluateExpression(
input: String, expression: String,
variables: Map<String, Double> = emptyMap(), constants: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY functions: FunctionExtensions = FunctionExtensions.EMPTY
): Double? { ): Double? {
val lexer = KeyLangLexer(CharStreams.fromString(input)) val lexer = KeyLangLexer(CharStreams.fromString(expression))
val parser = KeyLangParser(CommonTokenStream(lexer)) val parser = KeyLangParser(CommonTokenStream(lexer))
parser.removeErrorListeners() parser.removeErrorListeners()
parser.addErrorListener(object : BaseErrorListener() { parser.addErrorListener(object : BaseErrorListener() {
override fun syntaxError( override fun syntaxError(
recognizer: Recognizer<*, *>?, recognizer: Recognizer<*, *>?,
offendingSymbol: Any?, offendingSymbol: Any?,
line: Int, line: Int,
charPositionInLine: Int, charPositionInLine: Int,
msg: String?, msg: String?,
e: RecognitionException? e: RecognitionException?
) { ) {
throw ExpressionException("parser error in expression: '$input'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]") throw ExpressionException("parser error in expression: '$expression'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]")
} }
}) })
val root = parser.keyLangFile() val root = parser.keyLangFile()
val listener = ExpressionListener(functions) val listener = ExpressionListener(functions, constants)
listener.variables.putAll(variables)
try { try {
ParseTreeWalker.DEFAULT.walk(listener, root) ParseTreeWalker.DEFAULT.walk(listener, root)
} catch (e: ExpressionException) { } catch (e: ExpressionException) {
throw ExpressionException(e.message ?: "") throw ExpressionException(e.message ?: "")
} }
return listener.lastExpressionResult return listener.lastExpressionResult
} }
fun compileExpression(
expression: String,
constants: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
): () -> Double {
val lexer = KeyLangLexer(CharStreams.fromString(expression))
val parser = KeyLangParser(CommonTokenStream(lexer))
parser.removeErrorListeners()
parser.addErrorListener(object : BaseErrorListener() {
override fun syntaxError(
recognizer: Recognizer<*, *>?,
offendingSymbol: Any?,
line: Int,
charPositionInLine: Int,
msg: String?,
e: RecognitionException?
) {
throw ExpressionException("parser error in expression: '$expression'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]")
}
})
val root = parser.keyLangFile()
val listener = ExpressionListener(functions, constants)
return {
try {
ParseTreeWalker.DEFAULT.walk(listener, root)
} catch (e: ExpressionException) {
throw ExpressionException(e.message ?: "")
}
listener.lastExpressionResult ?: error("no result")
}
}
internal fun expressionRoot(expression: String): KeyLangParser.KeyLangFileContext {
val lexer = KeyLangLexer(CharStreams.fromString(expression))
val parser = KeyLangParser(CommonTokenStream(lexer))
parser.removeErrorListeners()
parser.addErrorListener(object : BaseErrorListener() {
override fun syntaxError(
recognizer: Recognizer<*, *>?,
offendingSymbol: Any?,
line: Int,
charPositionInLine: Int,
msg: String?,
e: RecognitionException?
) {
throw ExpressionException("parser error in expression: '$expression'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]")
}
})
return parser.keyLangFile()
}

View File

@@ -0,0 +1,33 @@
import org.amshove.kluent.invoking
import org.amshove.kluent.`should throw`
import org.amshove.kluent.shouldBeEqualTo
import org.junit.jupiter.api.Test
import org.openrndr.extra.expressions.ExpressionException
import org.openrndr.extra.expressions.compileExpression
class TestCompiledExpression {
@Test
fun `a simple compiled expression`() {
val expression = "someValue"
val function = compileExpression(expression, constants = mutableMapOf("someValue" to 5.0))
function().shouldBeEqualTo(5.0)
}
@Test
fun `a compiled expression with updated context`() {
val expression = "someValue"
val context = mutableMapOf("someValue" to 5.0)
val function = compileExpression(expression, constants = context)
function().shouldBeEqualTo(5.0)
context["someValue"] = 6.0
function().shouldBeEqualTo(6.0)
}
@Test
fun `an erroneous compiled expression`() {
val expression = "1bork"
invoking {
compileExpression(expression, constants = mutableMapOf("someValue" to 5.0))
} `should throw` ExpressionException::class
}
}

View File

@@ -0,0 +1,29 @@
import org.amshove.kluent.invoking
import org.amshove.kluent.`should throw`
import org.amshove.kluent.shouldBeEqualTo
import org.junit.jupiter.api.Test
import org.openrndr.extra.expressions.*
class TestCompiledFunctions {
@Test
fun `a simple compiled function1`() {
val expression = "t"
val function = compileFunction1(expression, "t")
function(-5.0).shouldBeEqualTo(-5.0)
function(5.0).shouldBeEqualTo(5.0)
}
@Test
fun `a simple compiled function2`() {
val expression = "x + y"
val function = compileFunction2(expression, "x", "y")
function(1.0, 2.0).shouldBeEqualTo(3.0)
}
@Test
fun `a simple compiled function3`() {
val expression = "x + y + z"
val function = compileFunction3(expression, "x", "y", "z")
function(1.0, 2.0, 3.0).shouldBeEqualTo(6.0)
}
}

View File

@@ -0,0 +1,16 @@
import org.amshove.kluent.shouldBeEqualTo
import org.openrndr.extra.expressions.watchingExpression1
import kotlin.test.Test
class TestExpressionDelegates {
@Test
fun test() {
val state = object {
var expression = "x * x"
val function1 by watchingExpression1(::expression, "x")
}
state.function1(5.0).shouldBeEqualTo(25.0)
}
}

View File

@@ -1,8 +1,9 @@
import org.amshove.kluent.`should throw` import org.amshove.kluent.`should throw`
import org.amshove.kluent.`with message` import org.amshove.kluent.`with message`
import org.amshove.kluent.invoking import org.amshove.kluent.invoking
import org.openrndr.extra.keyframer.ExpressionException import org.openrndr.extra.expressions.ExpressionException
import org.openrndr.extra.keyframer.evaluateExpression import org.openrndr.extra.expressions.evaluateExpression
import kotlin.test.Test import kotlin.test.Test
class TestExpressionErrors { class TestExpressionErrors {

View File

@@ -1,25 +1,21 @@
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNear import org.amshove.kluent.shouldBeNear
import org.openrndr.extra.keyframer.FunctionExtensions import org.openrndr.extra.expressions.FunctionExtensions
import org.openrndr.extra.keyframer.evaluateExpression import org.openrndr.extra.expressions.evaluateExpression
import org.openrndr.math.map
import kotlin.test.Test import kotlin.test.Test
class TestExpressions { class TestExpressions {
@Test @Test
fun `a value reference`() { fun `a value reference`() {
val expression = "someValue" val expression = "someValue"
val result = evaluateExpression(expression, variables= mapOf("someValue" to 5.0)) val result = evaluateExpression(expression, constants= mapOf("someValue" to 5.0))
result?.shouldBeEqualTo(5.0) result?.shouldBeEqualTo(5.0)
} }
@Test @Test
fun `a backticked value reference`() { fun `a backticked value reference`() {
val expression = "`some-value`" val expression = "`some-value`"
val result = evaluateExpression(expression, variables= mapOf("some-value" to 5.0)) val result = evaluateExpression(expression, constants= mapOf("some-value" to 5.0))
result?.shouldBeEqualTo(5.0) result?.shouldBeEqualTo(5.0)
} }

View File

@@ -1,5 +1,5 @@
import org.amshove.kluent.shouldBeNear import org.amshove.kluent.shouldBeNear
import org.openrndr.extra.keyframer.evaluateExpression import org.openrndr.extra.expressions.evaluateExpression
import kotlin.test.Test import kotlin.test.Test
class TestOperators { class TestOperators {

View File

@@ -0,0 +1,21 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
org.openrndr.extra.convention.`kotlin-jvm`
}
tasks.withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
}
dependencies {
implementation(libs.openrndr.application)
implementation(libs.openrndr.math)
implementation(libs.gson)
implementation(libs.kotlin.reflect)
implementation(project(":orx-noise"))
implementation(project(":orx-easing"))
implementation(project(":orx-jvm:orx-expression-evaluator"))
demoImplementation(project(":orx-jvm:orx-panel"))
testImplementation(libs.kluent)
}

View File

@@ -6,6 +6,9 @@ import com.google.gson.reflect.TypeToken
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.extra.easing.Easing import org.openrndr.extra.easing.Easing
import org.openrndr.extra.easing.EasingFunction 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.Vector2
import org.openrndr.math.Vector3 import org.openrndr.math.Vector3
import org.openrndr.math.Vector4 import org.openrndr.math.Vector4
@@ -64,41 +67,41 @@ open class Keyframer {
inner class DoubleChannel(key: String, defaultValue: Double = 0.0) : inner class DoubleChannel(key: String, defaultValue: Double = 0.0) :
CompoundChannel(arrayOf(key), arrayOf(defaultValue)) { CompoundChannel(arrayOf(key), arrayOf(defaultValue)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Double = getValue(0) operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Double = getValue(0)
} }
inner class Vector2Channel(keys: Array<String>, defaultValue: Vector2 = Vector2.ZERO) : inner class Vector2Channel(keys: Array<String>, defaultValue: Vector2 = Vector2.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y)) { CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector2 = Vector2(getValue(0), getValue(1)) operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector2 = Vector2(getValue(0), getValue(1))
} }
inner class Vector3Channel(keys: Array<String>, defaultValue: Vector3 = Vector3.ZERO) : inner class Vector3Channel(keys: Array<String>, defaultValue: Vector3 = Vector3.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z)) { CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector3 = operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector3 =
Vector3(getValue(0), getValue(1), getValue(2)) Vector3(getValue(0), getValue(1), getValue(2))
} }
inner class Vector4Channel(keys: Array<String>, defaultValue: Vector4 = Vector4.ZERO) : inner class Vector4Channel(keys: Array<String>, defaultValue: Vector4 = Vector4.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z, defaultValue.w)) { CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z, defaultValue.w)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector4 = operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector4 =
Vector4(getValue(0), getValue(1), getValue(2), getValue(3)) Vector4(getValue(0), getValue(1), getValue(2), getValue(3))
} }
inner class RGBaChannel(keys: Array<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) : inner class RGBaChannel(keys: Array<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) :
CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b, defaultValue.alpha)) { CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b, defaultValue.alpha)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): ColorRGBa = operator fun getValue(keyframer: Keyframer, property: KProperty<*>): ColorRGBa =
ColorRGBa(getValue(0), getValue(1), getValue(2), getValue(3)) ColorRGBa(getValue(0), getValue(1), getValue(2), getValue(3))
} }
inner class RGBChannel(keys: Array<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) : inner class RGBChannel(keys: Array<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) :
CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b)) { CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): ColorRGBa = operator fun getValue(keyframer: Keyframer, property: KProperty<*>): ColorRGBa =
ColorRGBa(getValue(0), getValue(1), getValue(2)) ColorRGBa(getValue(0), getValue(1), getValue(2))
} }
inner class DoubleArrayChannel(keys: Array<String>, defaultValue: DoubleArray = DoubleArray(keys.size)) : inner class DoubleArrayChannel(keys: Array<String>, defaultValue: DoubleArray = DoubleArray(keys.size)) :
CompoundChannel(keys, defaultValue.toTypedArray()) { CompoundChannel(keys, defaultValue.toTypedArray()) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): DoubleArray { operator fun getValue(keyframer: Keyframer, property: KProperty<*>): DoubleArray {
val result = DoubleArray(keys.size) val result = DoubleArray(keys.size)
for (i in keys.indices) { for (i in keys.indices) {
@@ -108,14 +111,13 @@ open class Keyframer {
} }
} }
val channels = mutableMapOf<String, KeyframerChannel>() val channels = mutableMapOf<String, KeyframerChannel>()
fun loadFromJson( fun loadFromJson(
file: File, file: File,
format: KeyframerFormat = KeyframerFormat.SIMPLE, format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(), parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY functions: FunctionExtensions = FunctionExtensions.EMPTY
) { ) {
require(file.exists()) { require(file.exists()) {
"failed to load keyframer from json: '${file.absolutePath}' does not exist." "failed to load keyframer from json: '${file.absolutePath}' does not exist."
@@ -128,10 +130,10 @@ open class Keyframer {
} }
fun loadFromJson( fun loadFromJson(
url: URL, url: URL,
format: KeyframerFormat = KeyframerFormat.SIMPLE, format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(), parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY functions: FunctionExtensions = FunctionExtensions.EMPTY
) { ) {
try { try {
loadFromJsonString(url.readText(), format, parameters, functions) loadFromJsonString(url.readText(), format, parameters, functions)
@@ -143,10 +145,10 @@ open class Keyframer {
} }
fun loadFromJsonString( fun loadFromJsonString(
json: String, json: String,
format: KeyframerFormat = KeyframerFormat.SIMPLE, format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(), parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY functions: FunctionExtensions = FunctionExtensions.EMPTY
) { ) {
when (format) { when (format) {
KeyframerFormat.SIMPLE -> { KeyframerFormat.SIMPLE -> {
@@ -160,6 +162,7 @@ open class Keyframer {
error("Error parsing simple Keyframer data: ${e.cause?.message}") error("Error parsing simple Keyframer data: ${e.cause?.message}")
} }
} }
KeyframerFormat.FULL -> { KeyframerFormat.FULL -> {
try { try {
val type = object : TypeToken<Map<String, Any>>() {}.type val type = object : TypeToken<Map<String, Any>>() {}.type
@@ -176,9 +179,9 @@ open class Keyframer {
private val prototypes = mutableMapOf<String, Map<String, Any>>() private val prototypes = mutableMapOf<String, Map<String, Any>>()
fun loadFromObjects( fun loadFromObjects(
dict: Map<String, Any>, dict: Map<String, Any>,
externalParameters: Map<String, Double> = emptyMap(), externalParameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY functions: FunctionExtensions = FunctionExtensions.EMPTY
) { ) {
this.parameters.clear() this.parameters.clear()
this.parameters.putAll(externalParameters) this.parameters.putAll(externalParameters)
@@ -191,7 +194,8 @@ open class Keyframer {
when (val candidate = entry.value) { when (val candidate = entry.value) {
is Double -> candidate is Double -> candidate
is String -> evaluateExpression(candidate, parameters, functions) is String -> evaluateExpression(candidate, parameters, functions)
?: error("could not evaluate expression: '$candidate'") ?: error("could not evaluate expression: '$candidate'")
is Int -> candidate.toDouble() is Int -> candidate.toDouble()
is Float -> candidate.toDouble() is Float -> candidate.toDouble()
else -> error("unknown type for parameter '${entry.key}'") else -> error("unknown type for parameter '${entry.key}'")
@@ -226,9 +230,9 @@ open class Keyframer {
} }
fun loadFromKeyObjects( fun loadFromKeyObjects(
keys: List<Map<String, Any>>, keys: List<Map<String, Any>>,
externalParameters: Map<String, Double>, externalParameters: Map<String, Double>,
functions: FunctionExtensions functions: FunctionExtensions
) { ) {
if (externalParameters !== parameters) { if (externalParameters !== parameters) {
parameters.clear() parameters.clear()
@@ -238,12 +242,12 @@ open class Keyframer {
var lastTime = 0.0 var lastTime = 0.0
val channelDelegates = this::class.memberProperties val channelDelegates = this::class.memberProperties
.mapNotNull { .mapNotNull {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
it as? KProperty1<Keyframer, Any> it as? KProperty1<Keyframer, Any>
} }
.filter { it.isAccessible = true; it.getDelegate(this) is CompoundChannel } .filter { it.isAccessible = true; it.getDelegate(this) is CompoundChannel }
.associate { Pair(it.name, it.getDelegate(this) as CompoundChannel) } .associate { Pair(it.name, it.getDelegate(this) as CompoundChannel) }
val channelKeys = channelDelegates.values.flatMap { channel -> val channelKeys = channelDelegates.values.flatMap { channel ->
channel.keys.map { it } channel.keys.map { it }
@@ -310,7 +314,8 @@ open class Keyframer {
when (val candidate = computed["time"]) { when (val candidate = computed["time"]) {
null -> lastTime null -> lastTime
is String -> evaluateExpression(candidate, expressionContext, functions) is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" } ?: error { "unknown value format for time : $candidate" }
is Double -> candidate is Double -> candidate
is Int -> candidate.toDouble() is Int -> candidate.toDouble()
is Float -> candidate.toDouble() is Float -> candidate.toDouble()
@@ -324,7 +329,8 @@ open class Keyframer {
when (val candidate = computed["duration"]) { when (val candidate = computed["duration"]) {
null -> 0.0 null -> 0.0
is String -> evaluateExpression(candidate, expressionContext, functions) is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" } ?: error { "unknown value format for time : $candidate" }
is Int -> candidate.toDouble() is Int -> candidate.toDouble()
is Float -> candidate.toDouble() is Float -> candidate.toDouble()
is Double -> candidate is Double -> candidate
@@ -380,7 +386,8 @@ open class Keyframer {
null -> error("no value for '${channelCandidate.key}'") null -> error("no value for '${channelCandidate.key}'")
is Double -> candidate is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions) is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate") ?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble() is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate") else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
} }
@@ -395,11 +402,11 @@ open class Keyframer {
} }
val dictEnvelope = when (val candidate = valueMap["envelope"]) { val dictEnvelope = when (val candidate = valueMap["envelope"]) {
null -> envelope null -> envelope
is DoubleArray -> candidate is DoubleArray -> candidate
is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray() is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray() is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
else -> error("unknown envelope for '$candidate") else -> error("unknown envelope for '$candidate")
} }
val dictDuration = try { val dictDuration = try {
@@ -407,7 +414,8 @@ open class Keyframer {
null -> null null -> null
is Double -> candidate is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions) is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate") ?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble() is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate") else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
} }
@@ -417,7 +425,12 @@ open class Keyframer {
if (dictDuration != null) { if (dictDuration != null) {
if (dictDuration <= 0.0) { if (dictDuration <= 0.0) {
channel.add(max(lastTime, time + dictDuration), lastValue, Easing.Linear.function, defaultEnvelope) channel.add(
max(lastTime, time + dictDuration),
lastValue,
Easing.Linear.function,
defaultEnvelope
)
channel.add(time, value, dictEasing, dictEnvelope) channel.add(time, value, dictEasing, dictEnvelope)
} else { } else {
channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope) channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope)
@@ -432,7 +445,8 @@ open class Keyframer {
when (val candidate = channelCandidate.value) { when (val candidate = channelCandidate.value) {
is Double -> candidate is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions) is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate") ?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble() is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate") else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
} }

View File

@@ -1,7 +1,8 @@
import org.amshove.kluent.`should throw` import org.amshove.kluent.`should throw`
import org.amshove.kluent.invoking import org.amshove.kluent.invoking
import org.openrndr.extra.expressions.ExpressionException
import kotlin.test.Test import kotlin.test.Test
import org.openrndr.extra.keyframer.ExpressionException
import org.openrndr.extra.keyframer.Keyframer import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.extra.keyframer.KeyframerFormat import org.openrndr.extra.keyframer.KeyframerFormat
import java.io.File import java.io.File

View File

@@ -25,6 +25,7 @@ include(
"orx-compute-graph-nodes", "orx-compute-graph-nodes",
"orx-jvm:orx-dnk3", "orx-jvm:orx-dnk3",
"orx-easing", "orx-easing",
"orx-jvm:orx-expression-evaluator",
"orx-jvm:orx-file-watcher", "orx-jvm:orx-file-watcher",
"orx-parameters", "orx-parameters",
"orx-fx", "orx-fx",