[orx-jvm] Move panel, gui, dnk3, keyframer, triangulation to orx-jvm
This commit is contained in:
220
orx-jvm/orx-keyframer/README.md
Normal file
220
orx-jvm/orx-keyframer/README.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# orx-keyframer
|
||||
|
||||
Create animated timelines by specifying properties and times in keyframes, then play it back at any speed (even
|
||||
backwards) automatically interpolating properties. Save, load, use mathematical expressions and callbacks. Powerful and
|
||||
highly reusable.
|
||||
|
||||
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": {
|
||||
"value": 50.0,
|
||||
"easing": "linear"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
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)
|
||||
}
|
||||
```
|
||||
|
||||
## Easing
|
||||
|
||||
All the easing functions of orx-easing are available
|
||||
|
||||
- linear
|
||||
- back-in
|
||||
- back-out
|
||||
- back-in-out
|
||||
- bounce-in
|
||||
- bounce-out
|
||||
- bounce-in-out
|
||||
- circ-in
|
||||
- circ-out
|
||||
- circ-in-out
|
||||
- cubic-in
|
||||
- cubic-out
|
||||
- cubic-in-out
|
||||
- elastic-in
|
||||
- elastic-out
|
||||
- elastic-in-out
|
||||
- expo-in
|
||||
- expo-out
|
||||
- expo-in-out
|
||||
- quad-in
|
||||
- quad-out
|
||||
- quad-in-out
|
||||
- quart-in
|
||||
- quart-out
|
||||
- quart-in-out
|
||||
- quint-in
|
||||
- quint-out
|
||||
- quint-in-out
|
||||
- sine-in
|
||||
- sine-out
|
||||
- sine-in-out
|
||||
- one
|
||||
- zero
|
||||
|
||||
## More expressive interface
|
||||
|
||||
orx-keyframer has two ways of programming key frames. The first is the `"x": <number>` style we have seen before. The
|
||||
second way uses a dictionary instead of a number value.
|
||||
|
||||
For example:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"time": 0.0,
|
||||
"x": 320.0,
|
||||
"y": 240.0
|
||||
},
|
||||
{
|
||||
"time": 10.0,
|
||||
"easing": "cubic-out",
|
||||
"x": {
|
||||
"easing": "cubic-in-out",
|
||||
"value": 0.0
|
||||
},
|
||||
"y": {
|
||||
"duration": -5.0,
|
||||
"easing": "cubic-in",
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": 20.0,
|
||||
"x": 640.0,
|
||||
"y": 480.0,
|
||||
"easing": "cubic-in-out"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Inside the value dictionary one can set `value`, `easing`, `duration` and `envelope`.
|
||||
|
||||
* `value` the target value, required value
|
||||
* `easing` easing method that overrides the key's easing method, optional value
|
||||
* `duration` an optional duration for the animation, set to `0` to jump from the previous
|
||||
value to the new value, a negative value will start the interpolation before `time`. A positive value
|
||||
wil start the interpolation at `time` and end at `time + duration`
|
||||
* `envelope` optional 2-point envelope that modifies the playback of the animation. The default envelope is
|
||||
`[0.0, 1.0]`. Reverse playback is achieved by supplying `[1.0, 0.0]`. To start the animation later try `[0.1, 1.0]`,
|
||||
to end the animation earlier try `[0.0, 0.9]`
|
||||
|
||||
## 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.
|
||||
|
||||
[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)
|
||||
|
||||
<!-- __demos__ -->
|
||||
## Demos
|
||||
### DemoEvelope01
|
||||
[source code](src/demo/kotlin/DemoEvelope01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoFull01
|
||||
[source code](src/demo/kotlin/DemoFull01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoScrub01
|
||||
[source code](src/demo/kotlin/DemoScrub01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoSimple01
|
||||
[source code](src/demo/kotlin/DemoSimple01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoSimple02
|
||||
[source code](src/demo/kotlin/DemoSimple02.kt)
|
||||
|
||||

|
||||
|
||||
### DemoSimpleExpressions01
|
||||
[source code](src/demo/kotlin/DemoSimpleExpressions01.kt)
|
||||
|
||||

|
||||
51
orx-jvm/orx-keyframer/build.gradle
Normal file
51
orx-jvm/orx-keyframer/build.gradle
Normal file
@@ -0,0 +1,51 @@
|
||||
//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-jvm:orx-panel"))
|
||||
demoImplementation("org.openrndr:openrndr-application:$openrndrVersion")
|
||||
demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion")
|
||||
demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
|
||||
demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion")
|
||||
demoImplementation(sourceSets.getByName("main").output)
|
||||
}
|
||||
|
||||
tasks.getByName("compileKotlin").dependsOn("generateGrammarSource")
|
||||
24
orx-jvm/orx-keyframer/src/demo/kotlin/DemoEvelope01.kt
Normal file
24
orx-jvm/orx-keyframer/src/demo/kotlin/DemoEvelope01.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
orx-jvm/orx-keyframer/src/demo/kotlin/DemoFull01.kt
Normal file
28
orx-jvm/orx-keyframer/src/demo/kotlin/DemoFull01.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
52
orx-jvm/orx-keyframer/src/demo/kotlin/DemoScrub01.kt
Normal file
52
orx-jvm/orx-keyframer/src/demo/kotlin/DemoScrub01.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
24
orx-jvm/orx-keyframer/src/demo/kotlin/DemoSimple01.kt
Normal file
24
orx-jvm/orx-keyframer/src/demo/kotlin/DemoSimple01.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
orx-jvm/orx-keyframer/src/demo/kotlin/DemoSimple02.kt
Normal file
27
orx-jvm/orx-keyframer/src/demo/kotlin/DemoSimple02.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
74
orx-jvm/orx-keyframer/src/demo/resources/demo-full-01.json
Normal file
74
orx-jvm/orx-keyframer/src/demo/resources/demo-full-01.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
20
orx-jvm/orx-keyframer/src/demo/resources/demo-simple-01.json
Normal file
20
orx-jvm/orx-keyframer/src/demo/resources/demo-simple-01.json
Normal 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"
|
||||
}
|
||||
|
||||
]
|
||||
32
orx-jvm/orx-keyframer/src/demo/resources/demo-simple-02.json
Normal file
32
orx-jvm/orx-keyframer/src/demo/resources/demo-simple-02.json
Normal 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
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
90
orx-jvm/orx-keyframer/src/main/antlr/KeyLangLexer.g4
Normal file
90
orx-jvm/orx-keyframer/src/main/antlr/KeyLangLexer.g4
Normal 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) ;
|
||||
46
orx-jvm/orx-keyframer/src/main/antlr/KeyLangParser.g4
Normal file
46
orx-jvm/orx-keyframer/src/main/antlr/KeyLangParser.g4
Normal 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 ;
|
||||
|
||||
|
||||
386
orx-jvm/orx-keyframer/src/main/kotlin/Expressions.kt
Normal file
386
orx-jvm/orx-keyframer/src/main/kotlin/Expressions.kt
Normal 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
|
||||
}
|
||||
75
orx-jvm/orx-keyframer/src/main/kotlin/Key.kt
Normal file
75
orx-jvm/orx-keyframer/src/main/kotlin/Key.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
60
orx-jvm/orx-keyframer/src/main/kotlin/KeyQuaternion.kt
Normal file
60
orx-jvm/orx-keyframer/src/main/kotlin/KeyQuaternion.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
58
orx-jvm/orx-keyframer/src/main/kotlin/KeyVector3.kt
Normal file
58
orx-jvm/orx-keyframer/src/main/kotlin/KeyVector3.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
454
orx-jvm/orx-keyframer/src/main/kotlin/Keyframer.kt
Normal file
454
orx-jvm/orx-keyframer/src/main/kotlin/Keyframer.kt
Normal 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]")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'"
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
92
orx-jvm/orx-keyframer/src/test/kotlin/TestFunctionCall.kt
Normal file
92
orx-jvm/orx-keyframer/src/test/kotlin/TestFunctionCall.kt
Normal 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)
|
||||
}
|
||||
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
110
orx-jvm/orx-keyframer/src/test/kotlin/TestKeyframerErrors.kt
Normal file
110
orx-jvm/orx-keyframer/src/test/kotlin/TestKeyframerErrors.kt
Normal 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'"
|
||||
}
|
||||
}
|
||||
})
|
||||
39
orx-jvm/orx-keyframer/src/test/kotlin/TestOperators.kt
Normal file
39
orx-jvm/orx-keyframer/src/test/kotlin/TestOperators.kt
Normal 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)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"easing": "garble"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"time": ")("
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"time": "doesNotExist"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"time": "0.0",
|
||||
"x": "garble",
|
||||
"y": "garble"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user