[orx-jvm] Move panel, gui, dnk3, keyframer, triangulation to orx-jvm

This commit is contained in:
Edwin Jakobs
2021-06-27 21:32:24 +02:00
parent 5814acef8f
commit 874d49779f
159 changed files with 22 additions and 21 deletions

View File

@@ -0,0 +1,24 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
suspend fun main() = application {
program {
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-envelope-01.json")))
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend {
animation(seconds)
drawer.circle(animation.position, 100.0)
}
}
}

View File

@@ -0,0 +1,28 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.extra.keyframer.KeyframerFormat
import org.openrndr.resourceUrl
import java.net.URL
suspend 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)
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend {
animation(seconds)
drawer.fill = animation.color
drawer.circle(animation.position, animation.radius)
}
}
}

View File

@@ -0,0 +1,52 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
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
suspend 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
}
}
}
}
}
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
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,24 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
suspend 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")))
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend {
animation(seconds)
drawer.circle(animation.position, 100.0)
}
}
}

View File

@@ -0,0 +1,27 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
suspend 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")))
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend {
animation(seconds)
drawer.fill = animation.color
drawer.circle(animation.position, animation.radius)
}
}
}

View File

@@ -0,0 +1,27 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
suspend 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))
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend {
animation(seconds)
drawer.circle(animation.position, animation.radius)
}
}
}

View File

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

View File

@@ -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,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,386 @@
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) {
@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]) }
"length" -> { x -> Vector2(x[0], x[1]).length }
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]) }
"length" -> { x -> Vector3(x[0], x[1], x[2]).length }
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))
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,75 @@
package org.openrndr.extra.keyframer
import org.openrndr.extras.easing.Easing
import org.openrndr.extras.easing.EasingFunction
import org.openrndr.math.map
internal val defaultEnvelope = doubleArrayOf(0.0, 1.0)
class Key(val time: Double, val value: Double, val easing: EasingFunction, val envelope: DoubleArray = defaultEnvelope)
class KeyframerChannel {
val keys = mutableListOf<Key>()
operator fun invoke() : Double {
return 0.0
}
fun add(
time: Double,
value: Double?,
easing: EasingFunction = Easing.Linear.function,
envelope: DoubleArray = defaultEnvelope
) {
require(envelope.size >= 2) {
"envelope should contain at least 2 entries"
}
value?.let {
keys.add(Key(time, it, easing, envelope))
}
}
fun lastValue(): Double? {
return keys.lastOrNull()?.value
}
fun lastTime(): Double? {
return keys.lastOrNull()?.time
}
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 te = t0.map(rightKey.envelope[0], rightKey.envelope[1], 0.0, 1.0, clamp = true)
val e0 = rightKey.easing(te, 0.0, 1.0, 1.0)
leftKey.value * (1.0 - e0) + rightKey.value * (e0)
}
}
}

View File

@@ -0,0 +1,60 @@
package org.openrndr.extra.keyframer
import org.openrndr.extras.easing.Easing
import org.openrndr.extras.easing.EasingFunction
import org.openrndr.math.Quaternion
import org.openrndr.math.slerp
class KeyQuaternion(val time: Double, val value: Quaternion, val easing: EasingFunction)
class KeyframerChannelQuaternion {
val keys = mutableListOf<KeyQuaternion>()
operator fun invoke() : Double {
return 0.0
}
fun add(time: Double, value: Quaternion?, easing: EasingFunction = Easing.Linear.function) {
value?.let {
keys.add(KeyQuaternion(time, it, easing))
}
}
fun lastValue(): Quaternion? {
return keys.lastOrNull()?.value
}
fun duration(): Double {
return keys.last().time
}
fun value(time: Double): Quaternion? {
if (keys.size == 0) {
return null
}
if (keys.size == 1) {
return if (time < keys.first().time) {
keys[0].value.normalized
} else {
keys[0].value.normalized
}
}
if (time < keys.first().time) {
return null
}
val rightIndex = keys.indexOfFirst { it.time > time }
return if (rightIndex == -1) {
keys.last().value.normalized
} 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)
slerp(leftKey.value, rightKey.value, e0).normalized
}
}
}

View File

@@ -0,0 +1,58 @@
package org.openrndr.extra.keyframer
import org.openrndr.extras.easing.Easing
import org.openrndr.extras.easing.EasingFunction
import org.openrndr.math.Vector3
class KeyVector3(val time: Double, val value: Vector3, val easing: EasingFunction)
class KeyframerChannelVector3 {
val keys = mutableListOf<KeyVector3>()
operator fun invoke() : Double {
return 0.0
}
fun add(time: Double, value: Vector3?, easing: EasingFunction = Easing.Linear.function) {
value?.let {
keys.add(KeyVector3(time, it, easing))
}
}
fun lastValue(): Vector3? {
return keys.lastOrNull()?.value
}
fun duration(): Double {
return keys.last().time
}
fun value(time: Double): Vector3? {
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,454 @@
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.extras.easing.EasingFunction
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import java.io.File
import java.lang.IllegalStateException
import java.lang.NullPointerException
import java.net.URL
import kotlin.math.max
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
enum class KeyframerFormat {
SIMPLE,
FULL
}
open class Keyframer {
private var currentTime = 0.0
operator fun invoke(time: Double) {
currentTime = time
}
open inner class CompoundChannel(val keys: Array<String>, private val defaultValues: Array<Double>) {
private var channelTimes: Array<Double> = Array(keys.size) { Double.NEGATIVE_INFINITY }
private var compoundChannels: Array<KeyframerChannel?> = Array(keys.size) { null }
private var cachedValues: Array<Double?> = Array(keys.size) { null }
open fun reset() {
for (i in channelTimes.indices) {
channelTimes[i] = Double.NEGATIVE_INFINITY
}
}
fun getValue(compound: Int): Double {
if (compoundChannels[compound] == null) {
compoundChannels[compound] = channels[keys[compound]]
}
return if (compoundChannels[compound] != null) {
if (channelTimes[compound] == currentTime && cachedValues[compound] != null) {
cachedValues[compound] ?: defaultValues[compound]
} else {
val value = compoundChannels[compound]?.value(currentTime) ?: defaultValues[compound]
cachedValues[compound] = value
value
}
} else {
defaultValues[compound]
}
}
}
val duration: Double
get() = channels.values.maxByOrNull { it.duration() }?.duration() ?: 0.0
inner class DoubleChannel(key: String, defaultValue: Double = 0.0) :
CompoundChannel(arrayOf(key), arrayOf(defaultValue)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Double = getValue(0)
}
inner class Vector2Channel(keys: Array<String>, defaultValue: Vector2 = Vector2.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector2 = Vector2(getValue(0), getValue(1))
}
inner class Vector3Channel(keys: Array<String>, defaultValue: Vector3 = Vector3.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector3 =
Vector3(getValue(0), getValue(1), getValue(2))
}
inner class Vector4Channel(keys: Array<String>, defaultValue: Vector4 = Vector4.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z, defaultValue.w)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector4 =
Vector4(getValue(0), getValue(1), getValue(2), getValue(3))
}
inner class RGBaChannel(keys: Array<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) :
CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b, defaultValue.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))
}
inner class DoubleArrayChannel(keys: Array<String>, defaultValue: DoubleArray = DoubleArray(keys.size)) :
CompoundChannel(keys, defaultValue.toTypedArray()) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): DoubleArray {
val result = DoubleArray(keys.size)
for (i in keys.indices) {
result[i] = getValue(i)
}
return result
}
}
val channels = mutableMapOf<String, KeyframerChannel>()
fun loadFromJson(
file: File,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
require(file.exists()) {
"failed to load keyframer from json: '${file.absolutePath}' does not exist."
}
try {
loadFromJsonString(file.readText(), format, parameters, functions)
} catch (e: ExpressionException) {
throw ExpressionException("Error loading from '${file.path}': ${e.message ?: ""}")
}
}
fun loadFromJson(
url: URL,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
try {
loadFromJsonString(url.readText(), format, parameters, functions)
} catch (e: ExpressionException) {
throw ExpressionException("Error loading $format from '${url}': ${e.message ?: ""}")
} catch (e: IllegalStateException) {
throw ExpressionException("Error loading $format from '${url}': ${e.message ?: ""}")
}
}
fun loadFromJsonString(
json: String,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
when (format) {
KeyframerFormat.SIMPLE -> {
try {
val type = object : TypeToken<List<Map<String, Any>>>() {}.type
val keys: List<MutableMap<String, Any>> = Gson().fromJson(json, type)
loadFromKeyObjects(keys, parameters, functions)
} catch (e: JsonSyntaxException) {
error("Error parsing simple Keyframer data: ${e.cause?.message}")
} catch (e: NullPointerException) {
error("Error parsing simple Keyframer data: ${e.cause?.message}")
}
}
KeyframerFormat.FULL -> {
try {
val type = object : TypeToken<Map<String, Any>>() {}.type
val keys: Map<String, Any> = Gson().fromJson(json, type)
loadFromObjects(keys, parameters, functions)
} catch (e: JsonSyntaxException) {
error("Error parsing full Keyframer data: ${e.cause?.message}")
}
}
}
}
private val parameters = mutableMapOf<String, Double>()
private val prototypes = mutableMapOf<String, Map<String, Any>>()
fun loadFromObjects(
dict: Map<String, Any>,
externalParameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
this.parameters.clear()
this.parameters.putAll(externalParameters)
prototypes.clear()
@Suppress("UNCHECKED_CAST")
(dict["parameters"] as? Map<String, Any>)?.let { lp ->
for (entry in lp) {
this.parameters[entry.key] = try {
when (val candidate = entry.value) {
is Double -> candidate
is String -> evaluateExpression(candidate, parameters, functions)
?: error("could not evaluate expression: '$candidate'")
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
else -> error("unknown type for parameter '${entry.key}'")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in 'parameters': ${e.message ?: ""} ")
}
}
}
this.parameters.putAll(externalParameters)
@Suppress("UNCHECKED_CAST")
(dict["prototypes"] as? Map<String, Map<String, Any>>)?.let {
prototypes.putAll(it)
}
@Suppress("UNCHECKED_CAST")
(dict["keys"] as? List<Map<String, Any>>)?.let { keys ->
loadFromKeyObjects(keys, parameters, functions)
}
}
private fun resolvePrototype(prototypeNames: String): Map<String, Any> {
val prototypeTokens = prototypeNames.split(" ").map { it.trim() }.filter { it.isNotBlank() }
val prototypeRefs = prototypeTokens.mapNotNull { prototypes[it] }
val computed = mutableMapOf<String, Any>()
for (ref in prototypeRefs) {
computed.putAll(ref)
}
return computed
}
fun loadFromKeyObjects(
keys: List<Map<String, Any>>,
externalParameters: Map<String, Double>,
functions: FunctionExtensions
) {
if (externalParameters !== parameters) {
parameters.clear()
parameters.putAll(externalParameters)
}
var lastTime = 0.0
val channelDelegates = this::class.memberProperties
.mapNotNull {
@Suppress("UNCHECKED_CAST")
it as? KProperty1<Keyframer, Any>
}
.filter { it.isAccessible = true; it.getDelegate(this) is CompoundChannel }
.associate { Pair(it.name, it.getDelegate(this) as CompoundChannel) }
val channelKeys = channelDelegates.values.flatMap { channel ->
channel.keys.map { it }
}.toSet()
for (delegate in channelDelegates.values) {
delegate.reset()
}
val expressionContext = mutableMapOf<String, Double>()
expressionContext.putAll(parameters)
expressionContext["t"] = 0.0
fun easingFunctionFromName(easingCandidate: String): EasingFunction {
return when (easingCandidate) {
"linear" -> Easing.Linear.function
"back-in" -> Easing.BackIn.function
"back-out" -> Easing.BackOut.function
"back-in-out" -> Easing.BackInOut.function
"bounce-in" -> Easing.BounceIn.function
"bounce-out" -> Easing.BounceOut.function
"bounce-in-out" -> Easing.BackInOut.function
"circ-in" -> Easing.CircIn.function
"circ-out" -> Easing.CircOut.function
"circ-in-out" -> Easing.CircInOut.function
"cubic-in" -> Easing.CubicIn.function
"cubic-out" -> Easing.CubicOut.function
"cubic-in-out" -> Easing.CubicInOut.function
"elastic-in" -> Easing.ElasticIn.function
"elastic-out" -> Easing.ElasticInOut.function
"elastic-in-out" -> Easing.ElasticOut.function
"expo-in" -> Easing.ExpoIn.function
"expo-out" -> Easing.ExpoOut.function
"expo-in-out" -> Easing.ExpoInOut.function
"quad-in" -> Easing.QuadIn.function
"quad-out" -> Easing.QuadOut.function
"quad-in-out" -> Easing.QuadInOut.function
"quart-in" -> Easing.QuartIn.function
"quart-out" -> Easing.QuartOut.function
"quart-in-out" -> Easing.QuartInOut.function
"quint-in" -> Easing.QuintIn.function
"quint-out" -> Easing.QuintOut.function
"quint-in-out" -> Easing.QuintInOut.function
"sine-in" -> Easing.SineIn.function
"sine-out" -> Easing.SineOut.function
"sine-in-out" -> Easing.SineInOut.function
"one" -> Easing.One.function
"zero" -> Easing.Zero.function
else -> error("unknown easing name '$easingCandidate'")
}
}
fun handleKey(key: Map<String, Any>, path: String) {
val prototype = (key["prototypes"] as? String)?.let {
resolvePrototype(it)
} ?: emptyMap()
val computed = mutableMapOf<String, Any>()
computed.putAll(prototype)
computed.putAll(key)
val time = try {
when (val candidate = computed["time"]) {
null -> lastTime
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" }
is Double -> candidate
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
else -> error("unknown time format for '$candidate'")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'time': ${e.message ?: ""}")
}
val duration = try {
when (val candidate = computed["duration"]) {
null -> 0.0
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" }
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
is Double -> candidate
else -> error("unknown duration type for '$candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'duration': ${e.message ?: ""}")
}
val easing = try {
when (val easingCandidate = computed["easing"]) {
null -> Easing.Linear.function
is String -> easingFunctionFromName(easingCandidate)
else -> error("unknown easing for '$easingCandidate'")
}
} catch (e: IllegalStateException) {
throw ExpressionException("error in $path.'easing': ${e.message ?: ""}")
}
val envelope = try {
when (val candidate = computed["envelope"]) {
null -> defaultEnvelope
is DoubleArray -> candidate
is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
else -> error("unknown envelope for '$candidate")
}
} catch (e: IllegalStateException) {
throw ExpressionException("error in $path.'envelope': ${e.message ?: ""}")
}
val reservedKeys = setOf("time", "easing", "envelope")
for (channelCandidate in computed.filter { it.key !in reservedKeys }) {
if (channelCandidate.key in channelKeys) {
val channel = channels.getOrPut(channelCandidate.key) {
KeyframerChannel()
}
val lastValue = channel.lastValue() ?: 0.0
expressionContext["v"] = lastValue
val lastTime = (channel.lastTime()) ?: 0.0
expressionContext["d"] = time - lastTime
if (channelCandidate.value is Map<*, *>) {
@Suppress("UNCHECKED_CAST")
val valueMap = channelCandidate.value as Map<String, Any>
val value = try {
when (val candidate = valueMap["value"]) {
null -> error("no value for '${channelCandidate.key}'")
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
}
val dictEasing = when (val candidate = valueMap["easing"]) {
null -> easing
is String -> easingFunctionFromName(candidate)
else -> error("unknown easing for '$candidate'")
}
val dictEnvelope = when (val candidate = valueMap["envelope"]) {
null -> envelope
is DoubleArray -> candidate
is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
else -> error("unknown envelope for '$candidate")
}
val dictDuration = try {
when (val candidate = valueMap["duration"]) {
null -> null
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
}
if (dictDuration != null) {
if (dictDuration <= 0.0) {
channel.add(max(lastTime, time + dictDuration), lastValue, Easing.Linear.function, defaultEnvelope)
channel.add(time, value, dictEasing, dictEnvelope)
} else {
channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope)
channel.add(time + dictDuration, value, dictEasing, dictEnvelope)
}
} else {
channel.add(time, value, dictEasing, dictEnvelope)
}
} else {
val value = try {
when (val candidate = channelCandidate.value) {
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
}
channel.add(time, value, easing, envelope)
}
}
}
lastTime = time + duration
expressionContext["t"] = lastTime
}
for ((index, key) in keys.withIndex()) {
handleKey(key, "keys[$index]")
}
}
}

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,110 @@
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"
}).replace("/", File.separator)
}
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] ]"
}
}
// Paths.sep
//
//Expected <Error loading from 'orx-keyframer/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] ]>,
// actual <Error loading from 'orx-keyframer\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,39 @@
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)
}
describe("unary minus") {
val result = evaluateExpression("-4.0")
result?.shouldBeNear(-4.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"
}
]