[orx-keyframer] Add envelopes, remove repetitions
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
# orx-keyframer
|
# orx-keyframer
|
||||||
|
|
||||||
Create animated timelines by specifying properties and times in keyframes,
|
Create animated timelines by specifying properties and times in keyframes, then play it back at any speed (even
|
||||||
then play it back at any speed (even backwards) automatically interpolating properties.
|
backwards) automatically interpolating properties. Save, load, use mathematical expressions and callbacks. Powerful and
|
||||||
Save, load, use mathematical expressions and callbacks. Powerful and highly reusable.
|
highly reusable.
|
||||||
|
|
||||||
What this allows you to do:
|
What this allows you to do:
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ What this allows you to do:
|
|||||||
"radius": {
|
"radius": {
|
||||||
"value": 50.0,
|
"value": 50.0,
|
||||||
"easing": "linear"
|
"easing": "linear"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@@ -77,95 +77,152 @@ extend {
|
|||||||
drawer.circle(animation.position, animation.radius)
|
drawer.circle(animation.position, animation.radius)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Easing
|
## Easing
|
||||||
|
|
||||||
All the easing functions of orx-easing are available
|
All the easing functions of orx-easing are available
|
||||||
|
|
||||||
- linear
|
- linear
|
||||||
- back-in
|
- back-in
|
||||||
- back-out
|
- back-out
|
||||||
- back-in-out
|
- back-in-out
|
||||||
- bounce-in
|
- bounce-in
|
||||||
- bounce-out
|
- bounce-out
|
||||||
- bounce-in-out
|
- bounce-in-out
|
||||||
- circ-in
|
- circ-in
|
||||||
- circ-out
|
- circ-out
|
||||||
- circ-in-out
|
- circ-in-out
|
||||||
- cubic-in
|
- cubic-in
|
||||||
- cubic-out
|
- cubic-out
|
||||||
- cubic-in-out
|
- cubic-in-out
|
||||||
- elastic-in
|
- elastic-in
|
||||||
- elastic-out
|
- elastic-out
|
||||||
- elastic-in-out
|
- elastic-in-out
|
||||||
- expo-in
|
- expo-in
|
||||||
- expo-out
|
- expo-out
|
||||||
- expo-in-out
|
- expo-in-out
|
||||||
- quad-in
|
- quad-in
|
||||||
- quad-out
|
- quad-out
|
||||||
- quad-in-out
|
- quad-in-out
|
||||||
- quart-in
|
- quart-in
|
||||||
- quart-out
|
- quart-out
|
||||||
- quart-in-out
|
- quart-in-out
|
||||||
- quint-in
|
- quint-in
|
||||||
- quint-out
|
- quint-out
|
||||||
- quint-in-out
|
- quint-in-out
|
||||||
- sine-in
|
- sine-in
|
||||||
- sine-out
|
- sine-out
|
||||||
- sine-in-out
|
- sine-in-out
|
||||||
- one
|
- one
|
||||||
- zero
|
- 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
|
## 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).
|
orx-keyframer uses two file formats. A `SIMPLE` format and a `FULL` format. For reference check
|
||||||
The full format adds a `parameters` block and a `prototypes` block.
|
the [example full format .json](src/demo/resources/demo-full-01.json) and
|
||||||
|
the [example program](src/demo/kotlin/DemoFull01.kt). The full format adds a `parameters` block and a `prototypes`
|
||||||
|
block.
|
||||||
|
|
||||||
[Repeats](src/demo/resources/demo-simple-repetitions-01.json), simple key repeating mechanism
|
[Expressions](src/demo/resources/demo-simple-expressions-01.json), expression mechanism. Currently uses values `r` to
|
||||||
|
indicate repeat index and `t` the last used key time, `v` the last used value (for the animated attribute).
|
||||||
[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:
|
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)`
|
- `min(x, y)`, `max(x, y)`
|
||||||
- `abs(x)`, `saturate(x)`
|
- `cos(x)`, `sin(x)`, `acos(x)`, `asin(x)`, `tan(x)`, `atan(x)`, `atan2(y, x)`
|
||||||
- `degrees(x)`, `radians(x)`
|
- `abs(x)`, `saturate(x)`
|
||||||
- `pow(x,y)`, `sqrt(x)`, `exp(x)`
|
- `degrees(x)`, `radians(x)`
|
||||||
- `mix(left, right, x)`
|
- `pow(x, y)`, `sqrt(x)`, `exp(x)`
|
||||||
- `smoothstep(t0, t1, x)`
|
- `mix(left, right, x)`
|
||||||
- `map(leftBefore, rightBefore, leftAfter, rightAfter, x)`
|
- `smoothstep(t0, t1, x)`
|
||||||
- `random()`, `random(min, max)`
|
- `map(leftBefore, rightBefore, leftAfter, rightAfter, x)`
|
||||||
|
- `random()`, `random(min, max)`
|
||||||
|
|
||||||
[Parameters and prototypes](src/demo/resources/demo-full-01.json)
|
[Parameters and prototypes](src/demo/resources/demo-full-01.json)
|
||||||
|
|
||||||
<!-- __demos__ -->
|
<!-- __demos__ -->
|
||||||
|
|
||||||
## Demos
|
## Demos
|
||||||
|
|
||||||
### DemoFull01
|
### DemoFull01
|
||||||
|
|
||||||
[source code](src/demo/kotlin/DemoFull01.kt)
|
[source code](src/demo/kotlin/DemoFull01.kt)
|
||||||
|
|
||||||

|

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

|

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

|

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

|

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

|

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

|

|
||||||
|
|||||||
@@ -8,10 +8,9 @@ fun main() = application {
|
|||||||
program {
|
program {
|
||||||
class Animation: Keyframer() {
|
class Animation: Keyframer() {
|
||||||
val position by Vector2Channel(arrayOf("x", "y"))
|
val position by Vector2Channel(arrayOf("x", "y"))
|
||||||
val radius by DoubleChannel("x")
|
|
||||||
}
|
}
|
||||||
val animation = Animation()
|
val animation = Animation()
|
||||||
animation.loadFromJson(URL(resourceUrl("/demo-simple-repetitions-01.json")))
|
animation.loadFromJson(URL(resourceUrl("/demo-envelope-01.json")))
|
||||||
if (System.getProperty("takeScreenshot") == "true") {
|
if (System.getProperty("takeScreenshot") == "true") {
|
||||||
extend(SingleScreenshot()) {
|
extend(SingleScreenshot()) {
|
||||||
this.outputFile = System.getProperty("screenshotPath")
|
this.outputFile = System.getProperty("screenshotPath")
|
||||||
@@ -19,7 +18,7 @@ fun main() = application {
|
|||||||
}
|
}
|
||||||
extend {
|
extend {
|
||||||
animation(seconds)
|
animation(seconds)
|
||||||
drawer.circle(animation.position, animation.radius)
|
drawer.circle(animation.position, 100.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
25
orx-keyframer/src/demo/resources/demo-envelope-01.json
Normal file
25
orx-keyframer/src/demo/resources/demo-envelope-01.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"time": 0.0,
|
|
||||||
"x": 320.0,
|
|
||||||
"y": 240.0,
|
|
||||||
"radius": 0.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"time": 3.0,
|
|
||||||
"repeat": {
|
|
||||||
"count": 5,
|
|
||||||
"keys": [
|
|
||||||
{
|
|
||||||
"duration": 1.0,
|
|
||||||
"easing": "cubic-in-out",
|
|
||||||
"x": 10.0,
|
|
||||||
"y": 4.0,
|
|
||||||
"radius": 400
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"duration": 1.0,
|
|
||||||
"easing": "cubic-in-out",
|
|
||||||
"x": 630.0,
|
|
||||||
"y": 470.0,
|
|
||||||
"radius": 40
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -20,8 +20,8 @@ DECIMAL : 'Decimal';
|
|||||||
STRING : 'String';
|
STRING : 'String';
|
||||||
|
|
||||||
// Identifiers
|
// Identifiers
|
||||||
ID : [_]*[a-zA-Z][A-Za-z0-9_]* ;
|
ID : [$_]*[a-zA-Z][A-Za-z0-9_]* ;
|
||||||
FUNCTION_ID : [_]*[a-z][A-Za-z0-9_]* ;
|
FUNCTION_ID : [$_]*[a-z][A-Za-z0-9_]* ;
|
||||||
|
|
||||||
// Literals
|
// Literals
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package org.openrndr.extra.keyframer
|
|||||||
import org.antlr.v4.runtime.*
|
import org.antlr.v4.runtime.*
|
||||||
import org.antlr.v4.runtime.tree.ParseTreeWalker
|
import org.antlr.v4.runtime.tree.ParseTreeWalker
|
||||||
import org.antlr.v4.runtime.tree.TerminalNode
|
import org.antlr.v4.runtime.tree.TerminalNode
|
||||||
import org.openrndr.extra.keyframer.antlr.*
|
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.extra.noise.uniform
|
||||||
import org.openrndr.math.*
|
import org.openrndr.math.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -357,20 +359,6 @@ fun evaluateExpression(
|
|||||||
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
||||||
): Double? {
|
): Double? {
|
||||||
val lexer = KeyLangLexer(CharStreams.fromString(input))
|
val lexer = KeyLangLexer(CharStreams.fromString(input))
|
||||||
//
|
|
||||||
// lexer.removeErrorListeners()
|
|
||||||
// lexer.addErrorListener(object : BaseErrorListener() {
|
|
||||||
// override fun syntaxError(
|
|
||||||
// recognizer: Recognizer<*, *>?,
|
|
||||||
// offendingSymbol: Any?,
|
|
||||||
// line: Int,
|
|
||||||
// charPositionInLine: Int,
|
|
||||||
// msg: String?,
|
|
||||||
// e: RecognitionException?
|
|
||||||
// ) {
|
|
||||||
// println("syntax error!")
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
val parser = KeyLangParser(CommonTokenStream(lexer))
|
val parser = KeyLangParser(CommonTokenStream(lexer))
|
||||||
parser.removeErrorListeners()
|
parser.removeErrorListeners()
|
||||||
parser.addErrorListener(object : BaseErrorListener() {
|
parser.addErrorListener(object : BaseErrorListener() {
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ package org.openrndr.extra.keyframer
|
|||||||
|
|
||||||
import org.openrndr.extras.easing.Easing
|
import org.openrndr.extras.easing.Easing
|
||||||
import org.openrndr.extras.easing.EasingFunction
|
import org.openrndr.extras.easing.EasingFunction
|
||||||
|
import org.openrndr.math.map
|
||||||
|
|
||||||
class Key(val time: Double, val value: Double, val easing: EasingFunction)
|
internal val defaultEnvelope = doubleArrayOf(0.0, 1.0)
|
||||||
|
|
||||||
|
class Key(val time: Double, val value: Double, val easing: EasingFunction, val envelope: DoubleArray = defaultEnvelope)
|
||||||
|
|
||||||
enum class Hold {
|
|
||||||
HoldNone,
|
|
||||||
HoldSet,
|
|
||||||
HoldAll
|
|
||||||
}
|
|
||||||
|
|
||||||
class KeyframerChannel {
|
class KeyframerChannel {
|
||||||
val keys = mutableListOf<Key>()
|
val keys = mutableListOf<Key>()
|
||||||
@@ -18,14 +16,17 @@ class KeyframerChannel {
|
|||||||
return 0.0
|
return 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(time: Double, value: Double?, easing: EasingFunction = Easing.Linear.function, jump: Hold = Hold.HoldNone) {
|
fun add(
|
||||||
if (jump == Hold.HoldAll || (jump == Hold.HoldSet && value != null)) {
|
time: Double,
|
||||||
lastValue()?.let {
|
value: Double?,
|
||||||
keys.add(Key(time, it, Easing.Linear.function))
|
easing: EasingFunction = Easing.Linear.function,
|
||||||
}
|
envelope: DoubleArray = defaultEnvelope
|
||||||
|
) {
|
||||||
|
require(envelope.size >= 2) {
|
||||||
|
"envelope should contain at least 2 entries"
|
||||||
}
|
}
|
||||||
value?.let {
|
value?.let {
|
||||||
keys.add(Key(time, it, easing))
|
keys.add(Key(time, it, easing, envelope))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +66,8 @@ class KeyframerChannel {
|
|||||||
val rightKey = keys[rightIndex]
|
val rightKey = keys[rightIndex]
|
||||||
val leftKey = keys[leftIndex]
|
val leftKey = keys[leftIndex]
|
||||||
val t0 = (time - leftKey.time) / (rightKey.time - leftKey.time)
|
val t0 = (time - leftKey.time) / (rightKey.time - leftKey.time)
|
||||||
val e0 = rightKey.easing(t0, 0.0, 1.0, 1.0)
|
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)
|
leftKey.value * (1.0 - e0) + rightKey.value * (e0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,8 @@ class KeyframerChannelQuaternion {
|
|||||||
return 0.0
|
return 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(time: Double, value: Quaternion?, easing: EasingFunction = Easing.Linear.function, jump: Hold = Hold.HoldNone) {
|
fun add(time: Double, value: Quaternion?, easing: EasingFunction = Easing.Linear.function) {
|
||||||
if (jump == Hold.HoldAll || (jump == Hold.HoldSet && value != null)) {
|
|
||||||
lastValue()?.let {
|
|
||||||
keys.add(KeyQuaternion(time, it, Easing.Linear.function))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value?.let {
|
value?.let {
|
||||||
keys.add(KeyQuaternion(time, it, easing))
|
keys.add(KeyQuaternion(time, it, easing))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ class KeyframerChannelVector3 {
|
|||||||
return 0.0
|
return 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(time: Double, value: Vector3?, easing: EasingFunction = Easing.Linear.function, jump: Hold = Hold.HoldNone) {
|
fun add(time: Double, value: Vector3?, easing: EasingFunction = Easing.Linear.function) {
|
||||||
if (jump == Hold.HoldAll || (jump == Hold.HoldSet && value != null)) {
|
|
||||||
lastValue()?.let {
|
|
||||||
keys.add(KeyVector3(time, it, Easing.Linear.function))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value?.let {
|
value?.let {
|
||||||
keys.add(KeyVector3(time, it, easing))
|
keys.add(KeyVector3(time, it, easing))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import java.lang.IllegalStateException
|
|||||||
import java.lang.NullPointerException
|
import java.lang.NullPointerException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
import kotlin.reflect.KProperty1
|
import kotlin.reflect.KProperty1
|
||||||
import kotlin.reflect.full.memberProperties
|
import kotlin.reflect.full.memberProperties
|
||||||
@@ -258,7 +257,6 @@ open class Keyframer {
|
|||||||
expressionContext.putAll(parameters)
|
expressionContext.putAll(parameters)
|
||||||
expressionContext["t"] = 0.0
|
expressionContext["t"] = 0.0
|
||||||
|
|
||||||
|
|
||||||
fun easingFunctionFromName(easingCandidate: String): EasingFunction {
|
fun easingFunctionFromName(easingCandidate: String): EasingFunction {
|
||||||
return when (easingCandidate) {
|
return when (easingCandidate) {
|
||||||
"linear" -> Easing.Linear.function
|
"linear" -> Easing.Linear.function
|
||||||
@@ -346,9 +344,20 @@ open class Keyframer {
|
|||||||
throw ExpressionException("error in $path.'easing': ${e.message ?: ""}")
|
throw ExpressionException("error in $path.'easing': ${e.message ?: ""}")
|
||||||
}
|
}
|
||||||
|
|
||||||
val hold = Hold.HoldNone
|
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", "hold")
|
|
||||||
|
val reservedKeys = setOf("time", "easing", "envelope")
|
||||||
|
|
||||||
for (channelCandidate in computed.filter { it.key !in reservedKeys }) {
|
for (channelCandidate in computed.filter { it.key !in reservedKeys }) {
|
||||||
if (channelCandidate.key in channelKeys) {
|
if (channelCandidate.key in channelKeys) {
|
||||||
@@ -385,6 +394,14 @@ open class Keyframer {
|
|||||||
else -> error("unknown easing for '$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 {
|
val dictDuration = try {
|
||||||
when (val candidate = valueMap["duration"]) {
|
when (val candidate = valueMap["duration"]) {
|
||||||
null -> null
|
null -> null
|
||||||
@@ -400,12 +417,14 @@ open class Keyframer {
|
|||||||
|
|
||||||
if (dictDuration != null) {
|
if (dictDuration != null) {
|
||||||
if (dictDuration <= 0.0) {
|
if (dictDuration <= 0.0) {
|
||||||
channel.add(max(lastTime, time + dictDuration), lastValue, Easing.Linear.function, hold)
|
channel.add(max(lastTime, time + dictDuration), lastValue, Easing.Linear.function, defaultEnvelope)
|
||||||
channel.add(time, value, dictEasing, hold)
|
channel.add(time, value, dictEasing, dictEnvelope)
|
||||||
} else {
|
} else {
|
||||||
channel.add(time, lastValue, Easing.Linear.function, hold)
|
channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope)
|
||||||
channel.add(time + dictDuration, value, dictEasing, hold)
|
channel.add(time + dictDuration, value, dictEasing, dictEnvelope)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
channel.add(time, value, dictEasing, dictEnvelope)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -420,39 +439,12 @@ open class Keyframer {
|
|||||||
} catch (e: ExpressionException) {
|
} catch (e: ExpressionException) {
|
||||||
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
|
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
|
||||||
}
|
}
|
||||||
channel.add(time, value, easing, hold)
|
channel.add(time, value, easing, envelope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastTime = time + duration
|
lastTime = time + duration
|
||||||
expressionContext["t"] = lastTime
|
expressionContext["t"] = lastTime
|
||||||
|
|
||||||
if (computed.containsKey("repeat")) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val repeatObject = computed["repeat"] as? Map<String, Any> ?: error("'repeat' should be a map")
|
|
||||||
val count = try {
|
|
||||||
when (val candidate = repeatObject["count"]) {
|
|
||||||
null -> 1
|
|
||||||
is Int -> candidate
|
|
||||||
is Double -> candidate.toInt()
|
|
||||||
is String -> evaluateExpression(candidate, expressionContext, functions)?.roundToInt()
|
|
||||||
?: error("cannot evaluate expression for count: '$candidate'")
|
|
||||||
else -> error("unknown value type for count: '$candidate")
|
|
||||||
}
|
|
||||||
} catch (e: ExpressionException) {
|
|
||||||
throw ExpressionException("error in $path.repeat.'count': ${e.message ?: ""}")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val repeatKeys = repeatObject["keys"] as? List<Map<String, Any>> ?: error("no repeat keys")
|
|
||||||
|
|
||||||
for (i in 0 until count) {
|
|
||||||
expressionContext["rep"] = i.toDouble()
|
|
||||||
for (repeatKey in repeatKeys) {
|
|
||||||
handleKey(repeatKey, "$path.repeat")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for ((index, key) in keys.withIndex()) {
|
for ((index, key) in keys.withIndex()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user