[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

@@ -1,46 +0,0 @@
plugins {
id("org.openrndr.extra.convention.kotlin-jvm")
id("antlr")
}
sourceSets {
main {
java {
srcDir("build/generated-src/antlr")
}
}
}
tasks.test {
useJUnitPlatform {
includeEngines("spek2")
}
}
generateGrammarSource {
maxHeapSize = "64m"
arguments += ["-visitor", "-long-messages"]
outputDirectory = file("${project.buildDir}/generated-src/antlr/org/openrndr/extra/keyframer/antlr".toString())
}
dependencies {
antlr(libs.antlr.core)
implementation(libs.antlr.runtime)
implementation(project(":orx-noise"))
implementation(project(":orx-easing"))
implementation(libs.openrndr.application)
implementation(libs.openrndr.math)
implementation(libs.gson)
implementation(libs.kotlin.reflect)
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"))
}
tasks.getByName("compileKotlin").dependsOn("generateGrammarSource")
tasks.getByName("compileDemoKotlin").dependsOn("generateDemoGrammarSource")
tasks.getByName("compileTestKotlin").dependsOn("generateTestGrammarSource")
tasks.getByName("sourcesJar").dependsOn("generateGrammarSource")

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

@@ -1,39 +0,0 @@
import org.openrndr.application
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.gui.addTo
import org.openrndr.extra.keyframer.evaluateExpression
import org.openrndr.extra.parameters.TextParameter
fun main() {
application {
program {
val gui = GUI()
gui.compartmentsCollapsedByDefault = false
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)
extend(gui)
extend {
//gui.visible = mouse.position.x < 200.0
val expressionContext = mapOf("t" to seconds, "width" to drawer.bounds.width, "height" to drawer.bounds.height)
fun eval(expression: String) : Double =
try { evaluateExpression(expression, expressionContext) ?: 0.0 } catch (e: Throwable) { 0.0 }
val x = eval(settings.xExpression)
val y = eval(settings.yExpression)
val radius = eval(settings.radiusExpression)
drawer.circle(x, y, radius)
}
}
}
}

View File

@@ -1,56 +0,0 @@
lexer grammar KeyLangLexer;
@header {
package org.openrndr.extra.keyframer.antlr;
}
channels { WHITESPACE }
// Whitespace
NEWLINE : '\r\n' | '\r' | '\n' ;
WS : [\t ]+ -> channel(WHITESPACE) ;
// Keywords
INPUT : 'input' ;
VAR : 'var' ;
PRINT : 'print';
AS : 'as';
INT : 'Int';
DECIMAL : 'Decimal';
STRING : 'String';
// Identifiers
ID : [$_]*[a-zA-Z][A-Za-z0-9_]* | '`'[$_]*[A-Za-z0-9_-]*'`';
FUNCTION_ID : [$_]*[a-z][A-Za-z0-9_]* ;
// Literals
DECLIT : [0-9][0-9]* '.' [0-9]+ ;
INTLIT : '0'|[0-9][0-9]* ;
// Operators
PLUS : '+' ;
PERCENTAGE : '%' ;
MINUS : '-' ;
ASTERISK : '*' ;
DIVISION : '/' ;
ASSIGN : '=' ;
LPAREN : '(' ;
RPAREN : ')' ;
COMMA : ',' ;
STRING_OPEN : '"' -> pushMode(MODE_IN_STRING);
UNMATCHED : . ;
mode MODE_IN_STRING;
ESCAPE_STRING_DELIMITER : '\\"' ;
ESCAPE_SLASH : '\\\\' ;
ESCAPE_NEWLINE : '\\n' ;
ESCAPE_SHARP : '\\#' ;
STRING_CLOSE : '"' -> popMode ;
STRING_CONTENT : ~["\n\r\t\\#]+ ;

View File

@@ -1,46 +0,0 @@
parser grammar KeyLangParser;
@header {
package org.openrndr.extra.keyframer.antlr;
}
options { tokenVocab=KeyLangLexer; }
keyLangFile : lines=line+ ;
line : statement (NEWLINE | EOF) ;
statement : inputDeclaration # inputDeclarationStatement
| varDeclaration # varDeclarationStatement
| assignment # assignmentStatement
| print # printStatement
| expression # expressionStatement ;
print : PRINT LPAREN expression RPAREN ;
inputDeclaration : INPUT type name=ID ;
varDeclaration : VAR assignment ;
assignment : ID ASSIGN expression ;
expression : INTLIT # intLiteral
| DECLIT # decimalLiteral
| ID LPAREN RPAREN # functionCall0Expression
| ID LPAREN expression RPAREN # functionCall1Expression
| ID LPAREN expression COMMA expression RPAREN # functionCall2Expression
| ID LPAREN expression COMMA expression COMMA expression RPAREN # functionCall3Expression
| ID LPAREN expression COMMA expression COMMA expression COMMA expression RPAREN # functionCall4Expression
| ID LPAREN expression COMMA expression COMMA expression COMMA expression COMMA expression RPAREN # functionCall5Expression
| ID # valueReference
| LPAREN expression RPAREN # parenExpression
| MINUS expression # minusExpression
| expression operator=(DIVISION|ASTERISK|PERCENTAGE) expression # binaryOperation1
| expression operator=(PLUS|MINUS) expression # binaryOperation2;
type : DECIMAL # decimal
| INT # integer
| STRING # string ;

View File

@@ -1,397 +0,0 @@
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.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.*
import kotlin.math.*
typealias Function0 = () -> Double
typealias Function1 = (Double) -> Double
typealias Function2 = (Double, Double) -> Double
typealias Function3 = (Double, Double, Double) -> Double
typealias Function4 = (Double, Double, Double, Double) -> Double
typealias Function5 = (Double, Double, Double, Double, Double) -> Double
class FunctionExtensions(
val functions0: Map<String, Function0> = emptyMap(),
val functions1: Map<String, Function1> = emptyMap(),
val functions2: Map<String, Function2> = emptyMap(),
val functions3: Map<String, Function3> = emptyMap(),
val functions4: Map<String, Function4> = emptyMap(),
val functions5: Map<String, Function5> = emptyMap()
) {
companion object {
val EMPTY = FunctionExtensions()
}
}
internal enum class IDType {
VARIABLE,
FUNCTION0,
FUNCTION1,
FUNCTION2,
FUNCTION3,
FUNCTION4,
FUNCTION5
}
internal class ExpressionListener(val functions: FunctionExtensions = FunctionExtensions.EMPTY) :
KeyLangParserBaseListener() {
val doubleStack = Stack<Double>()
val functionStack = Stack<(DoubleArray) -> Double>()
val variables = mutableMapOf<String, Double>()
val idTypeStack = Stack<IDType>()
var lastExpressionResult: Double? = null
val exceptionStack = Stack<ExpressionException>()
override fun exitExpressionStatement(ctx: KeyLangParser.ExpressionStatementContext) {
ifError {
throw ExpressionException("error in evaluation of '${ctx.text}': ${it.message ?: ""}")
}
val result = doubleStack.pop()
lastExpressionResult = result
}
override fun exitAssignment(ctx: KeyLangParser.AssignmentContext) {
val value = doubleStack.pop()
variables[ctx.ID()?.text ?: error("buh")] = value
}
override fun exitMinusExpression(ctx: KeyLangParser.MinusExpressionContext) {
val op = doubleStack.pop()
doubleStack.push(-op)
}
override fun exitBinaryOperation1(ctx: KeyLangParser.BinaryOperation1Context) {
ifError {
pushError(it.message ?: "")
return
}
val right = doubleStack.pop()
val left = doubleStack.pop()
val result = when (val operator = ctx.operator?.type) {
KeyLangParser.PLUS -> left + right
KeyLangParser.MINUS -> left - right
KeyLangParser.ASTERISK -> left * right
KeyLangParser.DIVISION -> left / right
KeyLangParser.PERCENTAGE -> mod(left, right)
else -> error("operator '$operator' not implemented")
}
doubleStack.push(result)
}
override fun exitBinaryOperation2(ctx: KeyLangParser.BinaryOperation2Context) {
ifError {
pushError(it.message ?: "")
return
}
val left = doubleStack.pop()
val right = doubleStack.pop()
val result = when (val operator = ctx.operator?.type) {
KeyLangParser.PLUS -> left + right
KeyLangParser.MINUS -> right - left
KeyLangParser.ASTERISK -> left * right
KeyLangParser.DIVISION -> left / right
else -> error("operator '$operator' not implemented")
}
doubleStack.push(result)
}
override fun enterValueReference(ctx: KeyLangParser.ValueReferenceContext) {
idTypeStack.push(IDType.VARIABLE)
}
override fun enterFunctionCall0Expression(ctx: KeyLangParser.FunctionCall0ExpressionContext) {
idTypeStack.push(IDType.FUNCTION0)
}
override fun exitFunctionCall0Expression(ctx: KeyLangParser.FunctionCall0ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val result = function.invoke(doubleArrayOf())
doubleStack.push(result)
}
override fun enterFunctionCall1Expression(ctx: KeyLangParser.FunctionCall1ExpressionContext) {
idTypeStack.push(IDType.FUNCTION1)
}
override fun exitFunctionCall1Expression(ctx: KeyLangParser.FunctionCall1ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument = doubleStack.pop()
val result = function.invoke(doubleArrayOf(argument))
doubleStack.push(result)
}
override fun enterFunctionCall2Expression(ctx: KeyLangParser.FunctionCall2ExpressionContext) {
idTypeStack.push(IDType.FUNCTION2)
}
override fun exitFunctionCall2Expression(ctx: KeyLangParser.FunctionCall2ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument1 = doubleStack.pop()
val argument0 = doubleStack.pop()
val result = function.invoke(doubleArrayOf(argument0, argument1))
doubleStack.push(result)
}
override fun enterFunctionCall3Expression(ctx: KeyLangParser.FunctionCall3ExpressionContext) {
idTypeStack.push(IDType.FUNCTION3)
}
override fun exitFunctionCall3Expression(ctx: KeyLangParser.FunctionCall3ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument2 = doubleStack.pop()
val argument1 = doubleStack.pop()
val argument0 = doubleStack.pop()
val result = function.invoke(doubleArrayOf(argument0, argument1, argument2))
doubleStack.push(result)
}
override fun enterFunctionCall4Expression(ctx: KeyLangParser.FunctionCall4ExpressionContext) {
idTypeStack.push(IDType.FUNCTION4)
}
override fun exitFunctionCall4Expression(ctx: KeyLangParser.FunctionCall4ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument3 = doubleStack.pop()
val argument2 = doubleStack.pop()
val argument1 = doubleStack.pop()
val argument0 = doubleStack.pop()
val result = function.invoke(doubleArrayOf(argument0, argument1, argument2, argument3))
doubleStack.push(result)
}
override fun enterFunctionCall5Expression(ctx: KeyLangParser.FunctionCall5ExpressionContext) {
idTypeStack.push(IDType.FUNCTION5)
}
override fun exitFunctionCall5Expression(ctx: KeyLangParser.FunctionCall5ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument4 = doubleStack.pop()
val argument3 = doubleStack.pop()
val argument2 = doubleStack.pop()
val argument1 = doubleStack.pop()
val argument0 = doubleStack.pop()
val result = function.invoke(doubleArrayOf(argument0, argument1, argument2, argument3, argument4))
doubleStack.push(result)
}
private fun <T> errorValue(message: String, value: T): T {
pushError(message)
return value
}
private fun pushError(message: String) {
exceptionStack.push(ExpressionException(message))
}
private inline fun ifError(f: (e: Throwable) -> Unit) {
if (exceptionStack.isNotEmpty()) {
val e = exceptionStack.pop()
f(e)
}
}
override fun visitTerminal(node: TerminalNode) {
val type = node.symbol?.type
if (type == KeyLangParser.INTLIT) {
doubleStack.push(node.text.toDouble())
}
if (type == KeyLangParser.DECLIT) {
doubleStack.push(node.text.toDouble())
}
if (type == KeyLangParser.ID) {
val name = node.text.replace("`","")
@Suppress("DIVISION_BY_ZERO")
when (val idType = idTypeStack.pop()) {
IDType.VARIABLE -> doubleStack.push(
when (name) {
"PI" -> PI
else -> variables[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0)
}
)
IDType.FUNCTION0 -> {
val function: (DoubleArray) -> Double =
when (name) {
"random" -> { _ -> Double.uniform(0.0, 1.0) }
else -> functions.functions0[name]?.let { { _: DoubleArray -> it.invoke() } }
?: errorValue(
"unresolved function: '${name}()'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION1 -> {
val function: (DoubleArray) -> Double =
when (name) {
"sqrt" -> { x -> sqrt(x[0]) }
"radians" -> { x -> Math.toRadians(x[0]) }
"degrees" -> { x -> Math.toDegrees(x[0]) }
"cos" -> { x -> cos(x[0]) }
"sin" -> { x -> sin(x[0]) }
"tan" -> { x -> tan(x[0]) }
"atan" -> { x -> atan(x[0]) }
"acos" -> { x -> acos(x[0]) }
"asin" -> { x -> asin(x[0]) }
"exp" -> { x -> exp(x[0]) }
"abs" -> { x -> abs(x[0]) }
"floor" -> { x -> floor(x[0]) }
"round" -> { x -> round(x[0]) }
"ceil" -> { x -> ceil(x[0]) }
"saturate" -> { x -> x[0].coerceIn(0.0, 1.0) }
else -> functions.functions1[name]?.let { { x: DoubleArray -> it.invoke(x[0]) } }
?: errorValue(
"unresolved function: '${name}(x0)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION2 -> {
val function: (DoubleArray) -> Double =
when (name) {
"max" -> { x -> max(x[0], x[1]) }
"min" -> { x -> min(x[0], x[1]) }
"pow" -> { x -> x[0].pow(x[1]) }
"mod" -> { x -> x[0].mod(x[1]) }
"atan2" -> { x -> atan2(x[0], x[1]) }
"random" -> { x -> Double.uniform(x[0], x[1]) }
"length" -> { x -> Vector2(x[0], x[1]).length }
else -> functions.functions2[name]?.let { { x: DoubleArray -> it.invoke(x[0], x[1]) } }
?: errorValue(
"unresolved function: '${name}(x0, x1)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION3 -> {
val function: (DoubleArray) -> Double =
when (name) {
"mix" -> { x -> mix(x[0], x[1], x[2]) }
"min" -> { x -> x.minOrNull()!! }
"max" -> { x -> x.maxOrNull()!! }
"sum" -> { x -> x.sum() }
"smoothstep" -> { x -> smoothstep(x[0], x[1], x[2]) }
"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]) } }
?: errorValue(
"unresolved function: '${name}(x0, x1, x2)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION4 -> {
val function: (DoubleArray) -> Double =
when (name) {
"min" -> { x -> x.minOrNull()!! }
"max" -> { x -> x.maxOrNull()!! }
"sum" -> { x -> x.sum() }
else -> functions.functions4[name]?.let { { x: DoubleArray -> it.invoke(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)
}
IDType.FUNCTION5 -> {
val function: (DoubleArray) -> Double =
when (name) {
"min" -> { x -> x.minOrNull()!! }
"max" -> { x -> x.maxOrNull()!! }
"sum" -> { x -> x.sum() }
"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]) } }
?: errorValue(
"unresolved function: '${name}(x0, x1, x2, x3, x4)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
else -> error("unsupported id-type $idType")
}
}
}
}
class ExpressionException(message: String) : RuntimeException(message)
fun evaluateExpression(
input: String,
variables: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
): Double? {
val lexer = KeyLangLexer(CharStreams.fromString(input))
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: '$input'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]")
}
})
val root = parser.keyLangFile()
val listener = ExpressionListener(functions)
listener.variables.putAll(variables)
try {
ParseTreeWalker.DEFAULT.walk(listener, root)
} catch (e: ExpressionException) {
throw ExpressionException(e.message ?: "")
}
return listener.lastExpressionResult
}

View File

@@ -6,6 +6,9 @@ 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
@@ -64,41 +67,41 @@ open class Keyframer {
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)
}
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))
}
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 =
Vector3(getValue(0), getValue(1), getValue(2))
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)) {
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))
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)) {
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))
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)) {
CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b)) {
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)) :
CompoundChannel(keys, defaultValue.toTypedArray()) {
CompoundChannel(keys, defaultValue.toTypedArray()) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): DoubleArray {
val result = DoubleArray(keys.size)
for (i in keys.indices) {
@@ -108,14 +111,13 @@ open class Keyframer {
}
}
val channels = mutableMapOf<String, KeyframerChannel>()
fun loadFromJson(
file: File,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
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."
@@ -128,10 +130,10 @@ open class Keyframer {
}
fun loadFromJson(
url: URL,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
url: URL,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
try {
loadFromJsonString(url.readText(), format, parameters, functions)
@@ -143,10 +145,10 @@ open class Keyframer {
}
fun loadFromJsonString(
json: String,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
json: String,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
when (format) {
KeyframerFormat.SIMPLE -> {
@@ -160,6 +162,7 @@ open class Keyframer {
error("Error parsing simple Keyframer data: ${e.cause?.message}")
}
}
KeyframerFormat.FULL -> {
try {
val type = object : TypeToken<Map<String, Any>>() {}.type
@@ -176,9 +179,9 @@ open class Keyframer {
private val prototypes = mutableMapOf<String, Map<String, Any>>()
fun loadFromObjects(
dict: Map<String, Any>,
externalParameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
dict: Map<String, Any>,
externalParameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
this.parameters.clear()
this.parameters.putAll(externalParameters)
@@ -191,7 +194,8 @@ open class Keyframer {
when (val candidate = entry.value) {
is Double -> candidate
is String -> evaluateExpression(candidate, parameters, functions)
?: error("could not evaluate expression: '$candidate'")
?: error("could not evaluate expression: '$candidate'")
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
else -> error("unknown type for parameter '${entry.key}'")
@@ -226,9 +230,9 @@ open class Keyframer {
}
fun loadFromKeyObjects(
keys: List<Map<String, Any>>,
externalParameters: Map<String, Double>,
functions: FunctionExtensions
keys: List<Map<String, Any>>,
externalParameters: Map<String, Double>,
functions: FunctionExtensions
) {
if (externalParameters !== parameters) {
parameters.clear()
@@ -238,12 +242,12 @@ open class Keyframer {
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) }
.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 }
@@ -310,7 +314,8 @@ open class Keyframer {
when (val candidate = computed["time"]) {
null -> lastTime
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" }
?: error { "unknown value format for time : $candidate" }
is Double -> candidate
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
@@ -324,7 +329,8 @@ open class Keyframer {
when (val candidate = computed["duration"]) {
null -> 0.0
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 Float -> candidate.toDouble()
is Double -> candidate
@@ -380,7 +386,8 @@ open class Keyframer {
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")
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
@@ -395,11 +402,11 @@ open class Keyframer {
}
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")
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 {
@@ -407,7 +414,8 @@ open class Keyframer {
null -> null
is Double -> candidate
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()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
@@ -417,7 +425,12 @@ open class Keyframer {
if (dictDuration != null) {
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)
} else {
channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope)
@@ -432,7 +445,8 @@ open class Keyframer {
when (val candidate = channelCandidate.value) {
is Double -> candidate
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()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}

View File

@@ -1,52 +0,0 @@
import org.amshove.kluent.`should throw`
import org.amshove.kluent.`with message`
import org.amshove.kluent.invoking
import org.openrndr.extra.keyframer.ExpressionException
import org.openrndr.extra.keyframer.evaluateExpression
import kotlin.test.Test
class TestExpressionErrors {
@Test
fun `an expression with non-sensible writing`() {
val expression = ")("
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: ')('; [line: 1, character: 0 , near: [@0,0:0=')',<21>,1:0] ]"
}
@Test
fun `an expression with equality instead of assign`() {
val expression = "a == 5"
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: 'a == 5'; [line: 1, character: 3 , near: [@3,3:3='=',<19>,1:3] ]"
}
@Test
fun `an expression trying to reassign a number`() {
val expression = "3 = 5"
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: '3 = 5'; [line: 1, character: 2 , near: [@2,2:2='=',<19>,1:2] ]"
}
@Test
fun `an expression that uses non-existing functions`() {
val expression = "notExisting(5)"
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "error in evaluation of 'notExisting(5)': unresolved function: 'notExisting(x0)'"
}
@Test
fun `an expression that uses non-existing variables`() {
val expression = "notExisting + 4"
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "error in evaluation of 'notExisting+4': unresolved variable: 'notExisting'"
}
}

View File

@@ -1,138 +0,0 @@
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNear
import org.openrndr.extra.keyframer.FunctionExtensions
import org.openrndr.extra.keyframer.evaluateExpression
import org.openrndr.math.map
import kotlin.test.Test
class TestExpressions {
@Test
fun `a value reference`() {
val expression = "someValue"
val result = evaluateExpression(expression, variables= mapOf("someValue" to 5.0))
result?.shouldBeEqualTo(5.0)
}
@Test
fun `a backticked value reference`() {
val expression = "`some-value`"
val result = evaluateExpression(expression, variables= mapOf("some-value" to 5.0))
result?.shouldBeEqualTo(5.0)
}
@Test
fun `a function call`() {
val expression = "sqrt(4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(2.0, 10E-6)
}
@Test
fun `a function call with the name in backticks`() {
val expression = "`sqrt`(4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(2.0, 10E-6)
}
@Test
fun `two function calls`() {
val expression = "sqrt(4.0) * sqrt(4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
@Test
fun `two argument max function call`() {
val expression = "max(0.0, 4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
@Test
fun `two argument min function call`() {
val expression = "min(8.0, 4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
@Test
fun `three argument function call`() {
val expression = "mix(8.0, 4.0, 0.5)"
val result = evaluateExpression(expression)
result?.shouldBeNear(6.0, 10E-6)
}
@Test
fun `five argument function call`() {
val expression = "map(0.0, 1.0, 0.0, 8.0, 0.5)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
@Test
fun `two argument function call, where argument order matters`() {
val expression = "pow(2.0, 3.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(8.0, 10E-6)
}
@Test
fun `nested function call`() {
val expression = "sqrt(min(8.0, 4.0))"
val result = evaluateExpression(expression)
result?.shouldBeNear(2.0, 10E-6)
}
@Test
fun `extension function0 call`() {
val expression = "extension()"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions0 = mapOf("extension" to { 2.0 })))
result?.shouldBeNear(2.0, 10E-6)
}
@Test
fun `extension function1 call`(){
val expression = "extension(1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions1 = mapOf("extension" to { x -> x * 2.0 })))
result?.shouldBeNear(2.0, 10E-6)
}
@Test
fun `extension function1 call with dashed name in backticks`(){
val expression = "`extension-function`(1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions1 = mapOf("extension-function" to { x -> x * 2.0 })))
result?.shouldBeNear(2.0, 10E-6)
}
@Test
fun `extension function2 call`() {
val expression = "extension(1.0, 1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions2 = mapOf("extension" to { x, y -> x + y })))
result?.shouldBeNear(2.0, 10E-6)
}
@Test
fun `extension function3 call`() {
val expression = "extension(1.0, 1.0, 1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions3 = mapOf("extension" to { x, y, z -> x + y + z})))
result?.shouldBeNear(3.0, 10E-6)
}
@Test
fun `extension function4 call`() {
val expression = "extension(1.0, 1.0, 1.0, 1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions4 = mapOf("extension" to { x, y, z, w -> x + y + z + w})))
result?.shouldBeNear(4.0, 10E-6)
}
@Test
fun `extension function5 call`() {
val expression = "extension(1.0, 1.0, 1.0, 1.0, 1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions5 = mapOf("extension" to { x, y, z, w, u -> x + y + z + w + u})))
result?.shouldBeNear(5.0, 10E-6)
}
}

View File

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

View File

@@ -1,53 +0,0 @@
import org.amshove.kluent.shouldBeNear
import org.openrndr.extra.keyframer.evaluateExpression
import kotlin.test.Test
class TestOperators {
@Test
fun `an addition operation`() {
val result = evaluateExpression("1 + 2")
result?.shouldBeNear(3.0, 10E-6)
}
@Test
fun `a subtraction operation`() {
val result = evaluateExpression("1 - 2")
result?.shouldBeNear(-1.0, 10E-6)
}
@Test
fun `a modulus operation`() {
val result = evaluateExpression("4 % 2")
result?.shouldBeNear(0.0, 10E-6)
}
@Test
fun `a multiplication operation`() {
val result = evaluateExpression("4 * 2")
result?.shouldBeNear(8.0, 10E-6)
}
@Test
fun `a division operation`() {
val result = evaluateExpression("4 / 2")
result?.shouldBeNear(2.0, 10E-6)
}
@Test
fun `a multiplication and addition operation`() {
val result = evaluateExpression("4 * 2 + 1")
result?.shouldBeNear(9.0, 10E-6)
}
@Test
fun `an addition and multiplication`() {
val result = evaluateExpression("4 + 2 * 3")
result?.shouldBeNear(10.0, 10E-6)
}
@Test
fun `unary minus`() {
val result = evaluateExpression("-4.0")
result?.shouldBeNear(-4.0, 10E-6)
}
}