Add orx-keyframer

This commit is contained in:
Edwin Jakobs
2020-04-07 14:28:09 +02:00
parent abde3fbe41
commit 8714b47102
27 changed files with 1807 additions and 0 deletions

99
orx-keyframer/README.md Normal file
View File

@@ -0,0 +1,99 @@
# orx-keyframer
A highly reusable keyframer.
This POC relies on JSON files, but that's not a hard dependency, it can be replaced with any deserializer scheme.
What this allows you to do:
1. Create a keyframed animation in a json file.
```json
[
{
"time": 0.0,
"easing": "cubic-in-out",
"x": 3.0,
"y": 4.0,
"z": 9.0,
"r": 0.1,
"g": 0.5,
"b": 0.2,
"radius": 50
},
{
"time": 2.0,
"easing": "cubic-in-out",
"r": 0.6,
"g": 0.5,
"b": 0.1
},
{
"time": 4.0,
"easing": "cubic-in-out",
"x": 10.0,
"y": 4.0,
"radius": 400
},
{
"time": 5.0,
"easing": "cubic-in-out",
"x": 100.0,
"y": 320.0,
"radius": 400
},
{
"time": 5.3,
"easing": "cubic-in-out",
"x": 100.0,
"y": 320.0,
"radius": 40
}
]
```
2. Map the animation data to Kotlin types:
```kotlin
class Animation : Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
val radius by DoubleChannel("radius")
val color by RGBChannel(arrayOf("r", "g", "b"))
}
val animation = Animation()
animation.loadFromJson(File("data/keyframes/animation.json"))
```
3. Animate! (from an OPENRNDR program)
```kotlin
extend {
animation(seconds)
drawer.fill = animation.color
drawer.circle(animation.position, animation.radius)
}
```
## Advanced features
orx-keyframer uses two file formats. A `SIMPLE` format and a `FULL` format. For reference check the [example full format .json](src/demo/resources/demo-full-01.json) and the [example program](src/demo/kotlin/DemoFull01.kt).
The full format adds a `parameters` block and a `prototypes` block.
[Repeats](src/demo/resources/demo-simple-repetitions-01.json), simple key repeating mechanism
[Expressions](src/demo/resources/demo-simple-expressions-01.json), expression mechanism. Currently uses values `r` to indicate repeat index and `t` the last used key time, `v` the last used value (for the animated attribute).
Supported functions in expressions:
- `min(x, y)`, `max(x, y)`
- `cos(x)`, `sin(x)`, `acos(x)`, `asin(x)`, `tan(x)`, `atan(x)`, `atan2(y, x)`
- `abs(x)`, `saturate(x)`
- `degrees(x)`, `radians(x)`
- `pow(x,y)`, `sqrt(x)`, `exp(x)`
- `mix(left, right, x)`
- `smoothstep(t0, t1, x)`
- `map(leftBefore, rightBefore, leftAfter, rightAfter, x)`
- `random()`, `random(min, max)`
[Parameters and prototypes](src/demo/resources/demo-full-01.json)

View File

@@ -0,0 +1,47 @@
//plugins {
// id 'antlr'
//}
apply plugin: 'antlr'
sourceSets {
demo {
java {
srcDirs = ["src/demo/kotlin"]
compileClasspath += main.getCompileClasspath()
runtimeClasspath += main.getRuntimeClasspath()
}
}
main {
java {
srcDir("src/main/java")
srcDir("src/main/kotlin")
srcDir("build/generated-src/antlr")
}
}
}
generateGrammarSource {
maxHeapSize = "64m"
arguments += ["-visitor", "-long-messages"]
outputDirectory = file("${project.buildDir}/generated-src/antlr/org/openrndr/extra/keyframer/antlr".toString())
}
dependencies {
antlr("org.antlr:antlr4:$antlrVersion")
implementation("org.antlr:antlr4-runtime:$antlrVersion")
implementation(project(":orx-noise"))
implementation(project(":orx-easing"))
implementation "com.google.code.gson:gson:$gsonVersion"
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
demoImplementation(project(":orx-camera"))
demoImplementation(project(":orx-panel"))
demoImplementation("org.openrndr:openrndr-core:$openrndrVersion")
demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion")
demoImplementation(sourceSets.getByName("main").output)
}

View File

@@ -0,0 +1,23 @@
import org.openrndr.application
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.extra.keyframer.KeyframerFormat
import org.openrndr.resourceUrl
import java.net.URL
fun main() = application {
program {
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
val radius by DoubleChannel("radius")
val color by RGBChannel(arrayOf("r", "g", "b"))
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-full-01.json")), format = KeyframerFormat.FULL)
extend {
animation(seconds)
drawer.fill = animation.color
drawer.circle(animation.position, animation.radius)
}
}
}

View File

@@ -0,0 +1,47 @@
import org.openrndr.application
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.panel.controlManager
import org.openrndr.panel.elements.Range
import org.openrndr.panel.elements.Slider
import org.openrndr.panel.elements.slider
import org.openrndr.resourceUrl
import java.net.URL
fun main() = application {
program {
// -- replace the default clock with an offset clock
var clockOffset = 0.0
val oldClock = clock
clock = { oldClock() - clockOffset }
var clockSlider: Slider? = null
// -- setup a simple UI
val cm = controlManager {
layout {
clockSlider = slider {
range = Range(0.0, 30.0)
events.valueChanged.listen {
if (it.interactive) {
clockOffset = oldClock() - it.newValue
}
}
}
}
}
extend(cm)
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-simple-01.json")))
extend {
// -- update the slider
clockSlider?.value = seconds
animation(seconds)
drawer.circle(animation.position, 100.0)
}
}
}

View File

@@ -0,0 +1,19 @@
import org.openrndr.application
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
fun main() = application {
program {
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-simple-01.json")))
extend {
animation(seconds)
drawer.circle(animation.position, 100.0)
}
}
}

View File

@@ -0,0 +1,21 @@
import org.openrndr.application
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
fun main() = application {
program {
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
val radius by DoubleChannel("radius")
val color by RGBChannel(arrayOf("r", "g", "b"))
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-simple-02.json")))
extend {
animation(seconds)
drawer.fill = animation.color
drawer.circle(animation.position, animation.radius)
}
}
}

View File

@@ -0,0 +1,22 @@
import org.openrndr.application
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
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-expressions-01.json")),
parameters = mapOf("cycleDuration" to 2.0))
extend {
animation(seconds)
drawer.circle(animation.position, animation.radius)
}
}
}

View File

@@ -0,0 +1,20 @@
import org.openrndr.application
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
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")))
extend {
animation(seconds)
drawer.circle(animation.position, animation.radius)
}
}
}

View File

@@ -0,0 +1,74 @@
{
// this is breaking with proper json but.. gson accepts comments and they are invaluable
// in the parameters block you can add custom values, which can be used in expressions
"parameters": {
"smallRadius": 5.0,
"repetitionCount": 10,
"width": 640.0,
"height": 480.0,
// you can have expressions inside parameters too, they are evaluated once, on load
"resolvedOnLoad" : "width * 2.0"
},
// in the prototypes you can set up key prototypes
"prototypes": {
"red": {
"r": 1.0,
"g": 0.0,
"b": 0.0
},
"blue": {
"r": 0.0,
"g": 0.0,
"b": 1.0
},
"center": {
// prototypes can have expressions too, they are evaluated as late as possible
// thus, they are evaluated more than once
"x": "width / 2",
"y": "height / 2"
},
"small": {
"radius": "smallRadius"
},
"large": {
"radius": "smallRadius * 10.0"
}
},
"keys": [
{
"time": 0.0,
"easing": "cubic-in-out",
"x": 3.0,
"y": 4.0,
"z": 9.0,
"r": 0.0,
"g": 1.0,
"b": 0.0,
"radius": 50,
"foo" : 0.0
},
{
"time": 2.0,
"easing": "cubic-in-out",
// here we apply the prototypes in cascading fashion from left to right
"prototypes": "red center small"
},
{
"time": 3.0,
"repeat": {
"count": "repetitionCount",
"keys": [
{
"time": "(rep * 2.0) + 3.0",
"prototypes": "blue large",
"easing": "cubic-in-out"
},
{
"time": "t + 1.0",
"prototypes": "red small"
}
]
}
}
]
}

View File

@@ -0,0 +1,20 @@
[
{
"time": 0.0,
"x": 320.0,
"y": 240.0
},
{
"time": 10.0,
"x": 0.0,
"y": 0.0,
"easing": "cubic-in-out"
},
{
"time": 20.0,
"x": 640.0,
"y": 480.0,
"easing": "cubic-in-out"
}
]

View File

@@ -0,0 +1,32 @@
[
{
"time": 0.0,
"x": 320.0,
"y": 240.0,
"radius": 0.0,
"r": 1.0,
"g": 1.0,
"b": 1.0
},
{
"time": 5.0,
"radius": 200.0,
"r": 0.0
},
{
"time": 10.0,
"g": 0.0,
"x": 0.0,
"y": 0.0,
"easing": "cubic-in-out"
},
{
"time": 20.0,
"x": 640.0,
"y": 480.0,
"radius": 50.0,
"easing": "cubic-in-out",
"g": 1.0,
"b": 0.0
}
]

View File

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

View File

@@ -0,0 +1,30 @@
[
{
"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

@@ -0,0 +1,90 @@
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_]* ;
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 ;
INTERPOLATION_OPEN : '#{' -> pushMode(MODE_IN_INTERPOLATION) ;
STRING_CONTENT : ~["\n\r\t\\#]+ ;
STR_UNMATCHED : . -> type(UNMATCHED) ;
mode MODE_IN_INTERPOLATION;
INTERPOLATION_CLOSE : '}' -> popMode ;
INTERP_WS : [\t ]+ -> channel(WHITESPACE), type(WS) ;
// Keywords
INTERP_AS : 'as'-> type(AS) ;
INTERP_INT : 'Int'-> type(INT) ;
INTERP_DECIMAL : 'Decimal'-> type(DECIMAL) ;
INTERP_STRING : 'String'-> type(STRING) ;
// Literals
INTERP_INTLIT : ('0'|[1-9][0-9]*) -> type(INTLIT) ;
INTERP_DECLIT : ('0'|[1-9][0-9]*) '.' [0-9]+ -> type(DECLIT) ;
// Operators
INTERP_PLUS : '+' -> type(PLUS) ;
INTERP_MINUS : '-' -> type(MINUS) ;
INTERP_ASTERISK : '*' -> type(ASTERISK) ;
INTERP_DIVISION : '/' -> type(DIVISION) ;
INTERP_PERCENTAGE : '%' -> type(PERCENTAGE) ;
INTERP_ASSIGN : '=' -> type(ASSIGN) ;
INTERP_LPAREN : '(' -> type(LPAREN) ;
INTERP_RPAREN : ')' -> type(RPAREN) ;
// Identifiers
INTERP_ID : [_]*[a-z][A-Za-z0-9_]* -> type(ID);
INTERP_STRING_OPEN : '"' -> type(STRING_OPEN), pushMode(MODE_IN_STRING);
INTERP_UNMATCHED : . -> type(UNMATCHED) ;

View File

@@ -0,0 +1,46 @@
parser grammar KeyLangParser;
@header {
package org.openrndr.extra.keyframer.antlr;
}
options { tokenVocab=KeyLangLexer; }
miniCalcFile : 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

@@ -0,0 +1,398 @@
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.noise.uniform
import org.openrndr.math.map
import org.openrndr.math.mix
import org.openrndr.math.mod
import org.openrndr.math.smoothstep
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) {
super.exitMinusExpression(ctx)
}
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) {
@Suppress("DIVISION_BY_ZERO")
when (val idType = idTypeStack.pop()) {
IDType.VARIABLE -> doubleStack.push(
when (val name = node.text) {
"PI" -> PI
else -> variables[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0)
}
)
IDType.FUNCTION0 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
"random" -> { _ -> Double.uniform(0.0, 1.0) }
else -> functions.functions0[candidate]?.let { { _:DoubleArray -> it.invoke() } }
?: errorValue(
"unresolved function: '${candidate}()'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION1 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
"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]) }
"ceil" -> { x -> ceil(x[0]) }
"saturate" -> { x -> x[0].coerceIn(0.0, 1.0) }
else -> functions.functions1[candidate]?.let { { x:DoubleArray -> it.invoke(x[0]) } }
?: errorValue(
"unresolved function: '${candidate}(x0)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION2 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
"max" -> { x -> max(x[0], x[1]) }
"min" -> { x -> min(x[0], x[1]) }
"pow" -> { x -> x[0].pow(x[1]) }
"atan2" -> { x -> atan2(x[0], x[1]) }
"random" -> { x -> Double.uniform(x[0], x[1]) }
else -> functions.functions2[candidate]?.let { { x:DoubleArray -> it.invoke(x[0], x[1]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION3 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
"mix" -> { x -> mix(x[0], x[1], x[2]) }
"smoothstep" -> { x -> smoothstep(x[0], x[1], x[2]) }
else -> functions.functions3[candidate]?.let { { x:DoubleArray -> it.invoke(x[0], x[1], x[2]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1, x2)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION4 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
else -> functions.functions4[candidate]?.let { { x:DoubleArray -> it.invoke(x[0], x[1], x[2], x[3]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1, x2, x3)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION5 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
"map" -> { x -> map(x[0], x[1], x[2], x[3], x[4]) }
else -> functions.functions5[candidate]?.let { { x:DoubleArray -> it.invoke(x[0], x[1], x[2], x[3], x[4]) } }
?: errorValue(
"unresolved function: '${candidate}(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))
//
// 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() {
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.miniCalcFile()
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

@@ -0,0 +1,68 @@
package org.openrndr.extra.keyframer
import org.openrndr.extras.easing.Easing
import org.openrndr.extras.easing.EasingFunction
class Key(val time: Double, val value: Double, val easing: EasingFunction)
enum class Hold {
HoldNone,
HoldSet,
HoldAll
}
class KeyframerChannel {
val keys = mutableListOf<Key>()
operator fun invoke() : Double {
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))
}
}
value?.let {
keys.add(Key(time, it, easing))
}
}
fun lastValue(): Double? {
return keys.lastOrNull()?.value
}
fun duration(): Double {
return keys.last().time
}
fun value(time: Double): Double? {
if (keys.size == 0) {
return null
}
if (keys.size == 1) {
return if (time < keys.first().time) {
null
} else {
keys[0].value
}
}
if (time < keys.first().time) {
return null
}
val rightIndex = keys.indexOfFirst { it.time > time }
return if (rightIndex == -1) {
keys.last().value
} else {
val leftIndex = (rightIndex - 1).coerceAtLeast(0)
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)
leftKey.value * (1.0 - e0) + rightKey.value * (e0)
}
}
}

View File

@@ -0,0 +1,375 @@
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.extras.easing.Easing
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.net.URL
import kotlin.math.roundToInt
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.maxBy { 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.a)) {
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))
}
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}")
}
}
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 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 -> when (easingCandidate) {
"linear" -> Easing.Linear.function
"cubic-in" -> Easing.CubicIn.function
"cubic-out" -> Easing.CubicOut.function
"cubic-in-out" -> Easing.CubicInOut.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
"expo-in" -> Easing.ExpoIn.function
"expo-out" -> Easing.ExpoOut.function
"expo-in-out" -> Easing.ExpoInOut.function
"one" -> Easing.One.function
"zero" -> Easing.Zero.function
else -> error("unknown easing name '$easingCandidate'")
}
else -> error("unknown easing for '$easingCandidate'")
}
} catch (e: IllegalStateException) {
throw ExpressionException("error in $path.'easing': ${e.message ?: ""}")
}
val hold = Hold.HoldNone
val reservedKeys = setOf("time", "easing", "hold")
for (channelCandidate in computed.filter { it.key !in reservedKeys }) {
if (channelCandidate.key in channelKeys) {
val channel = channels.getOrPut(channelCandidate.key) {
KeyframerChannel()
}
expressionContext["v"] = channel.lastValue() ?: 0.0
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, hold)
}
}
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()) {
handleKey(key, "keys[$index]")
}
}
}

View File

@@ -0,0 +1,57 @@
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 org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import java.lang.IllegalStateException
object TestExpressionErrors : Spek({
describe("an expression with non-sensible writing") {
val expression = ")("
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: ')('; [line: 1, character: 0 , near: [@0,0:0=')',<21>,1:0] ]"
}
}
describe("an expression with equality instead of assign") {
val expression = "a == 5"
it("should cause an exception to be thrown when evaluated") {
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] ]"
}
}
describe("an expression trying to reassign a number") {
val expression = "3 = 5"
it("should cause an exception to be thrown when evaluated") {
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] ]"
}
}
describe("an expression that uses non-existing functions") {
val expression = "notExisting(5)"
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "error in evaluation of 'notExisting(5)': unresolved function: 'notExisting(x0)'"
}
}
describe("an expression that uses non-existing variables") {
val expression = "notExisting + 4"
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "error in evaluation of 'notExisting+4': unresolved variable: 'notExisting'"
}
}
})

View File

@@ -0,0 +1,92 @@
import org.amshove.kluent.shouldBeNear
import org.openrndr.extra.keyframer.FunctionExtensions
import org.openrndr.extra.keyframer.evaluateExpression
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
object TestFunctionCall : Spek({
describe("a function call") {
val expression = "sqrt(4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(2.0, 10E-6)
}
describe("two function calls") {
val expression = "sqrt(4.0) * sqrt(4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
describe("two argument function call") {
val expression = "max(0.0, 4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
describe("two argument function call") {
val expression = "min(8.0, 4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
describe("three argument function call") {
val expression = "mix(8.0, 4.0, 0.5)"
val result = evaluateExpression(expression)
result?.shouldBeNear(6.0, 10E-6)
}
describe("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)
}
describe("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)
}
describe("nested function call") {
val expression = "sqrt(min(8.0, 4.0))"
val result = evaluateExpression(expression)
result?.shouldBeNear(2.0, 10E-6)
}
describe("extension function0 call") {
val expression = "extension()"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions0 = mapOf("extension" to { 2.0 })))
result?.shouldBeNear(2.0, 10E-6)
}
describe("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)
}
describe("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)
}
describe("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)
}
describe("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)
}
describe("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

@@ -0,0 +1,35 @@
import org.amshove.kluent.`should be`
import org.amshove.kluent.shouldBeNear
import org.openrndr.extra.keyframer.KeyframerChannel
import org.openrndr.extras.easing.Easing
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
object TestKeyframerChannel : Spek({
describe("a keyframer channel without keys") {
val kfc = KeyframerChannel()
it ("should return null when asking for value before first key time") {
kfc.value(0.0) `should be` null
}
}
describe("a keyframer channel with a single key") {
val kfc = KeyframerChannel()
kfc.add(0.0, 1.0, Easing.Linear.function)
kfc.value(0.0)?.shouldBeNear(1.0, 10E-6)
it ("should return null when asking for value before first key time") {
kfc.value(-1.0) `should be` null
}
}
describe("a keyframer channel with two keys") {
val kfc = KeyframerChannel()
kfc.add(0.0, 1.0, Easing.Linear.function)
kfc.add(1.0, 2.0, Easing.Linear.function)
kfc.value(0.0)?.shouldBeNear(1.0, 10E-6)
it ("should return null when asking for value before first key time") {
kfc.value(-1.0) `should be` null
}
}
})

View File

@@ -0,0 +1,105 @@
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.Keyframer
import org.openrndr.extra.keyframer.KeyframerFormat
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import java.io.File
import kotlin.IllegalStateException
private fun testFile(path: String) : File {
val test = File(".")
return if (test.absolutePath.endsWith("orx-keyframer/.")) {
File(path)
} else {
File("orx-keyframer/$path")
}
}
private fun testName(path: String) : String {
val test = File(".")
return if (test.absolutePath.endsWith("orx-keyframer/.")) {
path
} else {
"orx-keyframer/$path"
}
}
object TestKeyframerErrors : Spek({
class Animation : Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
}
describe("loading a faulty json") {
val animation = Animation()
val json = """
"""
it("should throw an exception") {
invoking { animation.loadFromJsonString(json) } `should throw` (IllegalStateException::class)
}
}
describe("loading a non existing json") {
val animation = Animation()
it("should throw an exception") {
invoking { animation.loadFromJson(testFile("this-does-not-exist")) } `should throw` (IllegalArgumentException::class)
}
}
describe("loading a json with a faulty time expression (1)") {
File(".").apply {
println(this.absolutePath)
}
val animation = Animation()
it("should throw an exception") {
invoking {
animation.loadFromJson(
testFile("src/test/resources/error-reporting/time-01.json"),
format = KeyframerFormat.SIMPLE
)
} `should throw` ExpressionException::class `with message` "Error loading from '${testName("src/test/resources/error-reporting/time-01.json")}': error in keys[0].'time': parser error in expression: ')('; [line: 1, character: 0 , near: [@0,0:0=')',<21>,1:0] ]"
}
}
describe("loading a json with a faulty time expression (2) ") {
val animation = Animation()
it("should throw an exception") {
invoking {
animation.loadFromJson(
testFile("src/test/resources/error-reporting/time-02.json"),
format = KeyframerFormat.SIMPLE
)
} `should throw` ExpressionException::class `with message` "Error loading from '${testName("src/test/resources/error-reporting/time-02.json")}': error in keys[0].'time': error in evaluation of 'doesNotExist': unresolved variable: 'doesNotExist'"
}
}
describe("loading a json with a non-existing easing") {
val animation = Animation()
it("should throw an exception") {
invoking {
animation.loadFromJson(
testFile("src/test/resources/error-reporting/easing.json"),
format = KeyframerFormat.SIMPLE
)
} `should throw` ExpressionException::class `with message` "Error loading from '${testName("src/test/resources/error-reporting/easing.json")}': error in keys[0].'easing': unknown easing name 'garble'"
}
}
describe("loading a json with a faulty value (1)") {
val animation = Animation()
it("should throw an exception") {
invoking {
animation.loadFromJson(
testFile("src/test/resources/error-reporting/value-01.json"),
format = KeyframerFormat.SIMPLE
)
} `should throw` ExpressionException::class `with message` "Error loading from '${testName("src/test/resources/error-reporting/value-01.json")}': error in keys[0].'x': error in evaluation of 'garble': unresolved variable: 'garble'"
}
}
})

View File

@@ -0,0 +1,35 @@
import org.amshove.kluent.shouldBeNear
import org.openrndr.extra.keyframer.evaluateExpression
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
object TestOperators : Spek({
describe("an addition operation") {
val result = evaluateExpression("1 + 2")
result?.shouldBeNear(3.0, 10E-6)
}
describe("a subtraction operation") {
val result = evaluateExpression("1 - 2")
result?.shouldBeNear(-1.0, 10E-6)
}
describe("a modulus operation") {
val result = evaluateExpression("4 % 2")
result?.shouldBeNear(0.0, 10E-6)
}
describe("a multiplication operation") {
val result = evaluateExpression("4 * 2")
result?.shouldBeNear(8.0, 10E-6)
}
describe("a division operation") {
val result = evaluateExpression("4 / 2")
result?.shouldBeNear(2.0, 10E-6)
}
describe("a multiplication/addition operation") {
val result = evaluateExpression("4 * 2 + 1")
result?.shouldBeNear(9.0, 10E-6)
}
describe("an addition/multiplication") {
val result = evaluateExpression("4 + 2 * 3")
result?.shouldBeNear(10.0, 10E-6)
}
})

View File

@@ -0,0 +1,5 @@
[
{
"easing": "garble"
}
]

View File

@@ -0,0 +1,5 @@
[
{
"time": ")("
}
]

View File

@@ -0,0 +1,5 @@
[
{
"time": "doesNotExist"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"time": "0.0",
"x": "garble",
"y": "garble"
}
]