From def299f5b31d3e316b5f18c46b5dc9c22dffaae2 Mon Sep 17 00:00:00 2001 From: Zhirkevich Alexander Y Date: Wed, 24 Jul 2024 17:14:54 +0300 Subject: [PATCH] expressions improvements --- .../internal/animation/ExpressionProperty.kt | 8 +- .../expressions/EvaluationContext.kt | 122 ++++++++++++- .../animation/expressions/Expression.kt | 2 +- .../expressions/ExpressionContext.kt | 2 +- .../expressions/ExpressionEvaluator.kt | 2 +- .../expressions/MainExpressionInterpreter.kt | 22 ++- .../SingleExpressionInterpreter.kt | 142 +++++++++++++-- .../expressions/operations/OpGlobalContext.kt | 111 ++++++++---- .../composition/OpCompositionContext.kt | 2 +- .../operations/composition/OpEffectContext.kt | 2 +- .../operations/composition/OpLayerContext.kt | 5 +- .../composition/OpPropertyContext.kt | 154 ++++++++++------- .../composition/OpTransformContext.kt | 19 +- .../operations/condition/OpBlock.kt | 26 ++- .../operations/condition/OpFunction.kt | 56 ++++++ .../operations/condition/OpReturn.kt | 7 + .../expressions/operations/js/JsContext.kt | 34 ++-- .../operations/js/number/JsNumberContext.kt | 5 +- .../operations/js/string/JsStringContext.kt | 162 +++++++++++------- .../expressions/operations/math/OpMath.kt | 24 ++- .../expressions/operations/value/OpAssign.kt | 14 +- .../operations/value/OpAssignByIndex.kt | 9 +- .../operations/value/OpGetVariable.kt | 11 +- .../expressions/operations/value/OpVar.kt | 9 +- .../internal/helpers/LottieBlendMode.kt | 4 +- .../expressions/global/CustomFunctionsTest.kt | 113 ++++++++++++ 26 files changed, 838 insertions(+), 229 deletions(-) create mode 100644 compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpFunction.kt create mode 100644 compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpReturn.kt create mode 100644 compottie/src/commonTest/kotlin/expressions/global/CustomFunctionsTest.kt diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/ExpressionProperty.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/ExpressionProperty.kt index 4fd5c94a..67c5f3af 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/ExpressionProperty.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/ExpressionProperty.kt @@ -22,6 +22,12 @@ internal abstract class ExpressionProperty : AnimatedProperty { override fun interpolated(state: AnimationState): T { val evaluator = expressionEvaluator - return mapEvaluated(evaluator.run { evaluate(state) }) + val evaluated = evaluator.run { evaluate(state) } + + return if (evaluated is AnimatedProperty<*>){ + evaluated.interpolated(state) as T + } else { + mapEvaluated(evaluated) + } } } \ No newline at end of file diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/EvaluationContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/EvaluationContext.kt index 792f2d8a..0d3b75f2 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/EvaluationContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/EvaluationContext.kt @@ -1,20 +1,132 @@ package io.github.alexzhirkevich.compottie.internal.animation.expressions -internal interface EvaluationContext { +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.condition.OpFunction + + +internal enum class VariableScope { + Global, Block +} - val variables : MutableMap +internal interface EvaluationContext { val randomSource : RandomSource + + fun registerFunction(function: OpFunction) + + fun getFunction(name: String) : OpFunction? + + fun getVariable(name : String) : Any? + + fun setVariable(name: String, value : Any, scope: VariableScope) + + fun withScope(extraVariables : Map, block : (EvaluationContext) -> Any) : Any + } internal class DefaultEvaluatorContext( - override val variables: MutableMap = mutableMapOf(), override val randomSource: RandomSource = RandomSource(), ) : EvaluationContext { - val result: Any? get() = variables["\$bm_rt"] + private val globalVariables: MutableMap = mutableMapOf() + + private val blockVariables: MutableMap = mutableMapOf() + + private val functions: MutableMap = mutableMapOf() + + private val child by lazy { + BlockEvaluatorContext(this) + } + + val result: Any? get() = getVariable("\$bm_rt") + + fun reset() { + globalVariables.clear() + blockVariables.clear() + } + + override fun setVariable(name: String, value: Any, scope: VariableScope) { + val map = when (scope) { + VariableScope.Global -> globalVariables + VariableScope.Block -> blockVariables + } + map[name] = value + } + + override fun registerFunction(function: OpFunction) { + functions[function.name] = function + } + + override fun getFunction(name: String): OpFunction? { + return functions[name] + } + + override fun getVariable(name: String): Any? { + return when { + blockVariables.containsKey(name) -> blockVariables[name] + globalVariables.containsKey(name) -> globalVariables[name] + else -> null + } + } + + override fun withScope( + extraVariables: Map, + block: (EvaluationContext) -> Any + ) : Any { + child.reset() + extraVariables.forEach { (n, v) -> + child.setVariable(n, v, VariableScope.Block) + } + return block(child) + } +} + +private class BlockEvaluatorContext( + private val parent : EvaluationContext +) : EvaluationContext { + + private val child by lazy { + BlockEvaluatorContext(this) + } + + private val scopeVariables: MutableMap = mutableMapOf() + private val scopeFunctions: MutableMap = mutableMapOf() + + override val randomSource: RandomSource + get() = parent.randomSource + + override fun registerFunction(function: OpFunction) { + scopeFunctions[function.name] = function + } + + override fun getFunction(name: String): OpFunction? { + return scopeFunctions[name] ?: parent.getFunction(name) + } + + override fun getVariable(name: String): Any? { + return if (scopeVariables.containsKey(name)) { + scopeVariables[name] + } else { + parent.getVariable(name) + } + } + + override fun setVariable(name: String, value: Any, scope: VariableScope) { + when (scope) { + VariableScope.Global -> parent.setVariable(name, value, scope) + VariableScope.Block -> scopeVariables[name] = value + } + } + + override fun withScope(extraVariables: Map, block: (EvaluationContext) -> Any) : Any { + child.reset() + extraVariables.forEach { (n, v) -> + child.setVariable(n, v, VariableScope.Block) + } + return block(child) + } fun reset() { - variables.clear() + scopeFunctions.clear() + scopeVariables.clear() } } \ No newline at end of file diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/Expression.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/Expression.kt index d08c5d09..3841fc83 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/Expression.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/Expression.kt @@ -3,7 +3,7 @@ package io.github.alexzhirkevich.compottie.internal.animation.expressions import io.github.alexzhirkevich.compottie.internal.AnimationState import io.github.alexzhirkevich.compottie.internal.animation.RawProperty -internal val EXPR_DEBUG_PRINT_ENABLED = false +internal val EXPR_DEBUG_PRINT_ENABLED = true internal fun interface Expression { diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/ExpressionContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/ExpressionContext.kt index 2066be5e..4d8615e6 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/ExpressionContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/ExpressionContext.kt @@ -6,7 +6,7 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.operati internal interface ExpressionContext : Expression { - fun interpret(op: String?, args: List): Expression? + fun interpret(op: String?, args: List?): Expression? fun withContext( block: T.( diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/ExpressionEvaluator.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/ExpressionEvaluator.kt index c8c05356..1f92d3c5 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/ExpressionEvaluator.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/ExpressionEvaluator.kt @@ -24,7 +24,7 @@ private class ExpressionEvaluatorImpl(expr : String) : ExpressionEvaluator { private val context = DefaultEvaluatorContext() - private val expression: Expression = MainExpressionInterpreter(expr).interpret() + private val expression: Expression = MainExpressionInterpreter(expr, context).interpret() override fun RawProperty<*>.evaluate(state: AnimationState): Any { return if (state.enableExpressions) { diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/MainExpressionInterpreter.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/MainExpressionInterpreter.kt index 81f79336..d37a1b0e 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/MainExpressionInterpreter.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/MainExpressionInterpreter.kt @@ -3,12 +3,16 @@ package io.github.alexzhirkevich.compottie.internal.animation.expressions import androidx.compose.ui.util.fastForEach import io.github.alexzhirkevich.compottie.Compottie -internal class MainExpressionInterpreter(expr : String) : ExpressionInterpreter { +internal class MainExpressionInterpreter( + expr : String, + private val context: EvaluationContext +) : ExpressionInterpreter { private val expressions = try { val lines = expr .replace("\t", " ") .replace("\r", "") + .replace("\n{", "{") .split(";", "\n") .filter(String::isNotBlank) @@ -19,12 +23,19 @@ internal class MainExpressionInterpreter(expr : String) : ExpressionInterpreter var line = lines[i] - while (line.endsWithExpressionChar() || line.countOpenedBlocks()> 0) { + while ( + line.endsWithExpressionChar() || + line.countOpenedBlocks()> 0 + ) { check(i < lines.lastIndex) { "Unexpected end of line: $line" } - line += ";" + lines[i + 1] + line += if (line.endsWith("{")) { + lines[i + 1] + } else { + ";" + lines[i + 1] + } i++ } @@ -32,8 +43,11 @@ internal class MainExpressionInterpreter(expr : String) : ExpressionInterpreter i++ } }.map { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("Expressions: $expr") + } try { - SingleExpressionInterpreter(it).interpret() + SingleExpressionInterpreter(it, context).interpret() } catch (t: Throwable) { Compottie.logger?.warn( "Unsupported or invalid Lottie expression: $it. You can ignore it if the animation runs fine or expressions are disabled (${t.message})" diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/SingleExpressionInterpreter.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/SingleExpressionInterpreter.kt index e6af12b7..89286de1 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/SingleExpressionInterpreter.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/SingleExpressionInterpreter.kt @@ -9,21 +9,27 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.operati import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.condition.OpEquals import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpGetVariable import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.OpGlobalContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.condition.FunctionParam +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.condition.OpFunction +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.condition.OpFunctionExec import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.js.JsContext import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.condition.OpIfCondition import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpIndex import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpMakeArray import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.math.OpMul import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.condition.OpNot +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.condition.OpReturn import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.math.OpSub import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.math.OpUnaryMinus import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.math.OpUnaryPlus import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.unresolvedReference import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpVar -import kotlin.math.exp +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract internal class SingleExpressionInterpreter( private val expr : String, + private val context: EvaluationContext ) : ExpressionInterpreter { private var pos = -1 private var ch: Char = ' ' @@ -54,8 +60,10 @@ internal class SingleExpressionInterpreter( ch = if (--pos > 0 && pos < expr.length) expr[pos] else ';' } + fun Char.skip() : Boolean = this == ' ' || this == '\n' + private fun eat(charToEat: Char): Boolean { - while (ch == ' ') nextChar() + while (ch.skip()) nextChar() if (ch == charToEat) { nextChar() @@ -70,7 +78,7 @@ internal class SingleExpressionInterpreter( while (i < expr.length) { if (condition(expr[i])) return true - if (expr[i] == ' ') + if (expr[i].skip()) i++ else return false } @@ -143,6 +151,7 @@ internal class SingleExpressionInterpreter( private fun parseAssignmentValue(x : Expression, merge : ((Any, Any) -> Any)? = null) = when { x is OpIndex && x.variable is OpGetVariable -> OpAssignByIndex( variableName = x.variable.name, + scope = x.variable.assignInScope ?: VariableScope.Global, index = x.index, assignableValue = parseExpressionOp(OpGlobalContext), merge = merge @@ -155,10 +164,11 @@ internal class SingleExpressionInterpreter( x is OpGetVariable -> OpAssign( variableName = x.name, assignableValue = parseExpressionOp(OpGlobalContext), + scope = x.assignInScope ?: VariableScope.Global, merge = merge ).also { if (EXPR_DEBUG_PRINT_ENABLED) { - println("parsing assignment for ${x.name}") + println("parsing assignment for ${x.name} in ${it.scope} scope") } } @@ -346,9 +356,13 @@ internal class SingleExpressionInterpreter( val startPos = pos do { nextChar() - } while (ch.isFun()) + } while ( + ch.isFun() + && !(expr.substring(startPos, pos) == "function" && ch == ' ') + && !(expr.substring(startPos, pos) == "return" && ch == ' ') + ) - val func = expr.substring(startPos, pos) + val func = expr.substring(startPos, pos).trim() parseFunction(context, func) } @@ -376,8 +390,12 @@ internal class SingleExpressionInterpreter( } } - private fun parseFunction(context: Expression, func : String?) : Expression { - val args = buildList { + private fun parseFunctionArgs(name : String?): List? { + + if (!nextCharIs('('::equals)){ + return null + } + return buildList { when { eat('(') -> { if (eat(')')){ @@ -388,17 +406,93 @@ internal class SingleExpressionInterpreter( } while (eat(',')) require(eat(')')) { - "Bad expression:Missing ')' after argument to $func" + "Bad expression:Missing ')' after argument to $name" } } } } + } + + private fun parseFunction(context: Expression, func : String?) : Expression { + + if (func == "function") { + + val start = pos + + while (ch != '(') { + nextChar() + } + + val name = expr.substring(start, pos).trim() + + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making defined function $name") + } + + val args = parseFunctionArgs(name).let { args -> + args?.map { + when (it) { + is OpGetVariable -> FunctionParam(name = it.name, default = null) + is OpAssign -> FunctionParam( + name = it.variableName, + default = it.assignableValue + ) + + else -> error("Invalid function declaration at $start") + } + } + } + + checkNotNull(args){ + "Missing function args" + } + + + check(nextCharIs('{'::equals)) { + "Missing function body at $pos" + } + + + val block = parseBlock( + scoped = false // function scope will be used + ) + + this.context.registerFunction( + OpFunction( + name = name, + parameters = args, + body = block + ) + ) + if (EXPR_DEBUG_PRINT_ENABLED) { + println("registered function $name") + } + return OpConstant(Undefined) + } + + if (func == "return"){ + val expr = parseExpressionOp(OpGlobalContext) + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making return with $expr") + } + return OpReturn(expr) + } + + val args = parseFunctionArgs(func) + if (EXPR_DEBUG_PRINT_ENABLED) { println("making fun $func") } return when (context) { is ExpressionContext<*> -> context.interpret(func, args) + ?: (if (args != null && func != null && this.context.getFunction(func) != null) { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("parsed call for defined function $func") + } + OpFunctionExec(func, args) + } + else null) ?: unresolvedReference( ref = func ?: "null", obj = context::class.simpleName @@ -409,11 +503,17 @@ internal class SingleExpressionInterpreter( else -> { JsContext.interpret(context, func, args) ?: error("Unsupported Lottie expression function: $func") +// when { +// func != null && this.context.getFunction(func) != null -> +// OpFunctionExec(func, args) +// else -> JsContext.interpret(context, func, args) +// ?: error("Unsupported Lottie expression function: $func") +// } } } } - private fun parseBlock(): Expression { + private fun parseBlock(scoped : Boolean = true): Expression { val list = buildList { if (eat('{')) { while (!eat('}') && pos < expr.length) { @@ -424,11 +524,29 @@ internal class SingleExpressionInterpreter( add(parseAssignment(OpGlobalContext)) } } - return OpBlock(list) + return OpBlock(list, scoped) + } +} + + +@OptIn(ExperimentalContracts::class) +internal fun checkArgsNotNull(args : List<*>?, func : String) { + contract { + returns() implies (args != null) + } + checkNotNull(args){ + "$func call was missing" } } -internal fun checkArgs(args : List<*>, count : Int, func : String) { +@OptIn(ExperimentalContracts::class) +internal fun checkArgs(args : List<*>?, count : Int, func : String) { + contract { + returns() implies (args != null) + } + checkNotNull(args){ + "$func call was missing" + } require(args.size == count){ "$func takes $count arguments, but ${args.size} got" } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/OpGlobalContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/OpGlobalContext.kt index b587c139..33aca851 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/OpGlobalContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/OpGlobalContext.kt @@ -9,9 +9,11 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.Evaluat import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression import io.github.alexzhirkevich.compottie.internal.animation.expressions.ExpressionContext import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableScope import io.github.alexzhirkevich.compottie.internal.animation.expressions.argAt import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgs import io.github.alexzhirkevich.compottie.internal.animation.expressions.argForNameOrIndex +import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgsNotNull import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.color.OpHslToRgb import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.color.OpRgbToHsl import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.composition.OpGetComp @@ -48,15 +50,22 @@ internal object OpGlobalContext : ExpressionContext, Expression { private val thisLayer = OpGetLayer() private val thisComp = OpGetComp(null) - override fun interpret(op: String?, args: List): Expression? { - + override fun interpret(op: String?, args: List?): Expression? { return when (op) { - "var", "let", "const" -> OpVar - "Infinity" -> OpConstant(Float.POSITIVE_INFINITY) - "Math" -> OpMath - "time" -> OpGetTime + "var", "let", "const" -> { + OpVar(if (op == "var") VariableScope.Global else VariableScope.Block) + } + "Infinity" -> { + OpConstant(Float.POSITIVE_INFINITY) + } + "Math" -> { + OpMath + } + "time" -> { + OpGetTime + } "thisComp" -> { - if (args.isEmpty()) { + if (args.isNullOrEmpty()) { OpGetComp(null) } else { OpGetLayer(nameOrIndex = args.single()) @@ -68,8 +77,12 @@ internal object OpGlobalContext : ExpressionContext, Expression { return OpGetComp(args.argAt(0)) } - "thisLayer" -> OpGetLayer() - "thisProperty" -> thisProperty + "thisLayer" -> { + OpGetLayer() + } + "thisProperty" -> { + thisProperty + } "add", "\$bm_sum", "sum" -> { checkArgs(args, 2, op) OpAdd( @@ -127,16 +140,21 @@ internal object OpGlobalContext : ExpressionContext, Expression { ) } - "length" -> OpLength( - args.argForNameOrIndex(0, "vec", "point1")!!, - args.argForNameOrIndex(1, "point2"), - ) + "length" -> { + checkArgsNotNull(args, op) + OpLength( + args.argForNameOrIndex(0, "vec", "point1")!!, + args.argForNameOrIndex(1, "point2"), + ) + } "normalize" -> { checkArgs(args, 1, op) OpNormalize(args.argAt(0)) } - "cross" -> error("cross is not supported yet") //todo: support OpCross + "cross" -> { + error("cross is not supported yet") //todo: support OpCross + } "degreesToRadians" -> { checkArgs(args, 1, op) @@ -148,19 +166,29 @@ internal object OpGlobalContext : ExpressionContext, Expression { OpRadiansToDegree(args.argAt(0)) } - "timeToFrames" -> OpTimeToFrames( - args.argForNameOrIndex(0,"t"), - args.argForNameOrIndex(1,"fps"), - ) - "framesToTime" -> OpFramesToTime( - args.argForNameOrIndex(0,"frames"), - args.argForNameOrIndex(1,"fps"), - ) - "seedRandom" -> OpSetRandomSeed( - args.argForNameOrIndex(0,"offset")!!, - args.argForNameOrIndex(1,"timeless"), - ) + "timeToFrames" -> { + checkArgsNotNull(args, op) + OpTimeToFrames( + args.argForNameOrIndex(0,"t"), + args.argForNameOrIndex(1,"fps"), + ) + } + "framesToTime" -> { + checkArgsNotNull(args, op) + OpFramesToTime( + args.argForNameOrIndex(0,"frames"), + args.argForNameOrIndex(1,"fps"), + ) + } + "seedRandom" -> { + checkArgsNotNull(args, op) + OpSetRandomSeed( + args.argForNameOrIndex(0,"offset")!!, + args.argForNameOrIndex(1,"timeless"), + ) + } "random", "gaussRandom" -> { + checkArgsNotNull(args, op) OpRandomNumber( args.argForNameOrIndex(0,"maxValOrArray1"), args.argForNameOrIndex(1,"maxValOrArray2"), @@ -173,10 +201,22 @@ internal object OpGlobalContext : ExpressionContext, Expression { OpNoise(args.argAt(0)) } - "linear" -> OpInterpolate.interpret(LinearEasing, args) - "ease" -> OpInterpolate.interpret(easeInOut, args) - "easeIn" -> OpInterpolate.interpret(easeIn, args) - "easeOut" -> OpInterpolate.interpret(easeOut, args) + "linear" -> { + checkArgsNotNull(args, op) + OpInterpolate.interpret(LinearEasing, args) + } + "ease" -> { + checkArgsNotNull(args, op) + OpInterpolate.interpret(easeInOut, args) + } + "easeIn" -> { + checkArgsNotNull(args, op) + OpInterpolate.interpret(easeIn, args) + } + "easeOut" -> { + checkArgsNotNull(args, op) + OpInterpolate.interpret(easeOut, args) + } "hslToRgb" -> { checkArgs(args, 1, op) @@ -189,16 +229,21 @@ internal object OpGlobalContext : ExpressionContext, Expression { } "if" -> { + checkArgsNotNull(args, op) OpIfCondition(condition = args.single()) }//error("Compottie doesn't support conditions in expressions yet") - "true" -> OpConstant(true) - "false" -> OpConstant(false) + "true" -> { + OpConstant(true) + } + "false" -> { + OpConstant(false) + } else -> { thisProperty.interpret(op, args) ?: thisLayer.interpret(op, args) ?: thisComp.interpret(op, args) ?: run { - if (args.isEmpty() && op != null) { + if (args == null && op != null) { if (EXPR_DEBUG_PRINT_ENABLED) { println("making GetVariable $op...") } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpCompositionContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpCompositionContext.kt index fbf037f2..d049ff6f 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpCompositionContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpCompositionContext.kt @@ -10,7 +10,7 @@ internal sealed class OpCompositionContext : ExpressionContext + args: List? ): Expression? { return when (op) { "numLayers" -> withContext { _, _, _ -> layersCount } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpEffectContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpEffectContext.kt index 4de8519b..beec5492 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpEffectContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpEffectContext.kt @@ -8,7 +8,7 @@ import io.github.alexzhirkevich.compottie.internal.effects.LayerEffect internal sealed class OpEffectContext : ExpressionContext { - final override fun interpret(op: String?, args: List): Expression? { + final override fun interpret(op: String?, args: List?): Expression? { return when (op) { null -> { diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpLayerContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpLayerContext.kt index fe830ada..a11d1b56 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpLayerContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpLayerContext.kt @@ -6,13 +6,14 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefin import io.github.alexzhirkevich.compottie.internal.animation.expressions.argAt import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgs import io.github.alexzhirkevich.compottie.internal.animation.expressions.argForNameOrIndex +import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgsNotNull import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpConstant import io.github.alexzhirkevich.compottie.internal.layers.Layer import io.github.alexzhirkevich.compottie.internal.layers.PrecompositionLayer internal sealed class OpLayerContext : Expression, ExpressionContext { - override fun interpret(op: String?, args: List): Expression? { + override fun interpret(op: String?, args: List?): Expression? { return when (op) { "index" -> withContext { _, _, _ -> index ?: Undefined } "name" -> withContext { _, _, _ -> name ?: Undefined } @@ -75,6 +76,7 @@ internal sealed class OpLayerContext : Expression, ExpressionContext { } "fromComp" -> { + checkArgsNotNull(args, op) OpLayerToComp( layer = this, point = args.argForNameOrIndex(0,"point")!!, @@ -84,6 +86,7 @@ internal sealed class OpLayerContext : Expression, ExpressionContext { } "toWorld" -> { + checkArgsNotNull(args, op) OpLayerToWorld( layer = this, point = args.argForNameOrIndex(0,"point")!!, diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpPropertyContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpPropertyContext.kt index 19adf341..4ee463db 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpPropertyContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpPropertyContext.kt @@ -11,6 +11,7 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefin import io.github.alexzhirkevich.compottie.internal.animation.expressions.argAt import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgs import io.github.alexzhirkevich.compottie.internal.animation.expressions.argForNameOrIndex +import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgsNotNull import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random.OpSmooth import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random.OpTemporalWiggle import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random.OpWiggle @@ -27,86 +28,121 @@ internal abstract class OpPropertyContext : Expression, ExpressionContext = TODO() - final override fun interpret(op: String?, args: List): Expression? { + final override fun interpret(op: String?, args: List?): Expression? { return when (op) { - "value" -> OpPropertyValue(this) - "numKeys" -> withContext { _, _, _ -> - (this as? RawKeyframeProperty<*, *>)?.keyframes?.size ?: 0 + "value" -> { + OpPropertyValue(this) + } + "numKeys" -> { + withContext { _, _, _ -> + (this as? RawKeyframeProperty<*, *>)?.keyframes?.size ?: 0 + } } - "points" -> withTimeRemapping(args.getOrNull(0)) { _, _, state -> - (this as? AnimatedShape)?.rawBezier(state)?.vertices ?: Undefined + "points" -> { + withTimeRemapping(args?.getOrNull(0)) { _, _, state -> + (this as? AnimatedShape)?.rawBezier(state)?.vertices ?: Undefined + } } - "inTangents" -> withTimeRemapping(args.getOrNull(0)) { _, _, state -> - (this as? AnimatedShape)?.rawBezier(state)?.inTangents ?: Undefined + "inTangents" -> { + withTimeRemapping(args?.getOrNull(0)) { _, _, state -> + (this as? AnimatedShape)?.rawBezier(state)?.inTangents ?: Undefined + } } - "outTangents" -> withTimeRemapping(args.getOrNull(0)) { _, _, state -> - (this as? AnimatedShape)?.rawBezier(state)?.outTangents ?: Undefined + "outTangents" -> { + withTimeRemapping(args?.getOrNull(0)) { _, _, state -> + (this as? AnimatedShape)?.rawBezier(state)?.outTangents ?: Undefined + } } - "isClosed" -> withTimeRemapping(args.getOrNull(0)) { _, _, state -> - (this as? AnimatedShape)?.rawBezier(state)?.isClosed ?: Undefined + "isClosed" -> { + withTimeRemapping(args?.getOrNull(0)) { _, _, state -> + (this as? AnimatedShape)?.rawBezier(state)?.isClosed ?: Undefined + } } - "createPath" -> OpCreatePath( - points = args.argForNameOrIndex(0, "points"), - inTangents = args.argForNameOrIndex(1, "inTangents"), - outTangents = args.argForNameOrIndex(2, "outTangents"), - isClosed = args.argForNameOrIndex(3, "is_closed"), - ) + "createPath" -> { + checkArgsNotNull(args, op) + OpCreatePath( + points = args.argForNameOrIndex(0, "points"), + inTangents = args.argForNameOrIndex(1, "inTangents"), + outTangents = args.argForNameOrIndex(2, "outTangents"), + isClosed = args.argForNameOrIndex(3, "is_closed"), + ) + } - "propertyIndex" -> withContext { _, _, _ -> index ?: Undefined } + "propertyIndex" -> { + withContext { _, _, _ -> index ?: Undefined } + } "valueAtTime" -> { checkArgs(args, 1, op) OpPropertyValue(this, timeRemapping = args.argAt(0)) } - "wiggle" -> OpWiggle( - property = this, - freq = args.argForNameOrIndex(0, "freq")!!, - amp = args.argForNameOrIndex(1, "amp")!!, - octaves = args.argForNameOrIndex(2, "octaves"), - ampMult = args.argForNameOrIndex(3, "amp_mult"), - time = args.argForNameOrIndex(4, "t"), - ) - - "temporalWiggle" -> OpTemporalWiggle( - freq = args.argForNameOrIndex(0, "freq")!!, - amp = args.argForNameOrIndex(1, "amp")!!, - octaves = args.argForNameOrIndex(2, "octaves"), - ampMult = args.argForNameOrIndex(3, "amp_mult"), - time = args.argForNameOrIndex(4, "t"), - ) - - "smooth" -> OpSmooth( - prop = this, - width = args.argForNameOrIndex(0, "width"), - samples = args.argForNameOrIndex(1, "samples"), - time = args.argForNameOrIndex(2, "t") - ) - - "loopIn", "loopInDuration" -> OpLoopIn( - property = this, - name = args.getOrNull(0), - numKf = args.getOrNull(1), - isDuration = op == "loopInDuration" - ) - - "loopOut", "loopOutDuration" -> OpLoopOut( - property = this, - name = args.argForNameOrIndex(0, "type"), - numKf = args.argForNameOrIndex(1, "numKeyframes"), - isDuration = op == "loopOutDuration" - ) + "wiggle" -> { + checkArgsNotNull(args, op) + OpWiggle( + property = this, + freq = args.argForNameOrIndex(0, "freq")!!, + amp = args.argForNameOrIndex(1, "amp")!!, + octaves = args.argForNameOrIndex(2, "octaves"), + ampMult = args.argForNameOrIndex(3, "amp_mult"), + time = args.argForNameOrIndex(4, "t"), + ) + } + + "temporalWiggle" -> { + checkArgsNotNull(args, op) + OpTemporalWiggle( + freq = args.argForNameOrIndex(0, "freq")!!, + amp = args.argForNameOrIndex(1, "amp")!!, + octaves = args.argForNameOrIndex(2, "octaves"), + ampMult = args.argForNameOrIndex(3, "amp_mult"), + time = args.argForNameOrIndex(4, "t"), + ) + } + + "smooth" -> { + checkArgsNotNull(args, op) + OpSmooth( + prop = this, + width = args.argForNameOrIndex(0, "width"), + samples = args.argForNameOrIndex(1, "samples"), + time = args.argForNameOrIndex(2, "t") + ) + } + + "loopIn", "loopInDuration" -> { + checkArgsNotNull(args, op) + OpLoopIn( + property = this, + name = args.getOrNull(0), + numKf = args.getOrNull(1), + isDuration = op == "loopInDuration" + ) + } + + "loopOut", "loopOutDuration" -> { + checkArgsNotNull(args, op) + OpLoopOut( + property = this, + name = args.argForNameOrIndex(0, "type"), + numKf = args.argForNameOrIndex(1, "numKeyframes"), + isDuration = op == "loopOutDuration" + ) + } "getVelocityAtTime", - "getSpeedAtTime" - -> error("$op is not yet supported") + "getSpeedAtTime" -> { + error("$op is not yet supported") + } - else -> null + else -> { + null + } } } } \ No newline at end of file diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpTransformContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpTransformContext.kt index b5526bbf..62cb68a9 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpTransformContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/composition/OpTransformContext.kt @@ -1,20 +1,25 @@ package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.composition +import io.github.alexzhirkevich.compottie.internal.animation.AnimatedProperty import io.github.alexzhirkevich.compottie.internal.animation.AnimatedTransform import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression import io.github.alexzhirkevich.compottie.internal.animation.expressions.ExpressionContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.toExpressionType internal sealed class OpTransformContext : Expression, ExpressionContext { - override fun interpret(op: String?, args: List): Expression? { + override fun interpret(op: String?, args: List?): Expression? { return when(op) { - "rotation" -> withContext { _, _, s -> rotation.interpolated(s) } - "scale" -> withContext { _, _, s -> scale.interpolated(s) } - "opacity" -> withContext { _, _, s -> opacity.interpolated(s) } - "skew" -> withContext { _, _, s -> skew.interpolated(s) } - "skewAxis" -> withContext { _, _, s -> skewAxis.interpolated(s) } - "position" -> withContext { _, _, s -> position.interpolated(s) } + "rotation" -> interpolate(AnimatedTransform::rotation) + "scale" -> interpolate(AnimatedTransform::scale) + "opacity" -> interpolate(AnimatedTransform::opacity) + "skew" -> interpolate(AnimatedTransform::skew) + "skewAxis" -> interpolate(AnimatedTransform::skewAxis) + "position" -> interpolate(AnimatedTransform::position) else -> null } } + + private fun interpolate(value : (AnimatedTransform) -> AnimatedProperty<*>) : Expression = + withContext { _, _, s -> value(this).interpolated(s).toExpressionType() } } \ No newline at end of file diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpBlock.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpBlock.kt index 32586540..86c68d1d 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpBlock.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpBlock.kt @@ -8,15 +8,35 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.Express import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined internal class OpBlock( - private val expressions: List + val expressions: List, + var scoped : Boolean ) : Expression { + override fun invoke( property: RawProperty, context: EvaluationContext, state: AnimationState - ): Undefined { + ): Any { + return if (scoped){ + context.withScope(emptyMap()){ + invokeInternal(property, it, state) + } + } else { + invokeInternal(property, context, state) + } + } + + private fun invokeInternal( + property: RawProperty, + context: EvaluationContext, + state: AnimationState + ) : Any { expressions.fastForEach { - it(property, context, state) + if (it is OpReturn) { + return it(property, context, state) + } else { + it(property, context, state) + } } return Undefined } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpFunction.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpFunction.kt new file mode 100644 index 00000000..5fc8f9cc --- /dev/null +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpFunction.kt @@ -0,0 +1,56 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.condition + +import androidx.compose.ui.util.fastForEachIndexed +import io.github.alexzhirkevich.compottie.internal.AnimationState +import io.github.alexzhirkevich.compottie.internal.animation.RawProperty +import io.github.alexzhirkevich.compottie.internal.animation.expressions.EvaluationContext +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.argForNameOrIndex +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.unresolvedReference + +internal class FunctionParam( + val name : String, + val default : Expression? +) + +internal class OpFunction( + val name : String, + private val parameters : List, + private val body : Expression +) { + private val arguments = mutableMapOf() + + fun invoke( + args: List, + property: RawProperty, + context: EvaluationContext, + state: AnimationState + ): Any { + arguments.clear() + parameters.fastForEachIndexed { i, p -> + arguments[p.name] = requireNotNull(args.argForNameOrIndex(i, p.name) ?: p.default) { + "'${p.name}' argument of '$name' function is missing" + }.invoke(property, context, state) + } + + return context.withScope( + extraVariables = arguments + ){ + body.invoke(property, it, state) + } + } +} + +internal fun OpFunctionExec( + name : String, + parameters : List, +) = Expression { property, context, state -> + val function = context.getFunction(name) ?: unresolvedReference(name) + + function.invoke( + args = parameters, + property = property, + context = context, + state = state + ) +} diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpReturn.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpReturn.kt new file mode 100644 index 00000000..3c60e4ef --- /dev/null +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/condition/OpReturn.kt @@ -0,0 +1,7 @@ +package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.condition + +import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression + +internal class OpReturn( + val value : Expression +) : Expression by value \ No newline at end of file diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/JsContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/JsContext.kt index 8583b2d1..8ad5f6c8 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/JsContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/JsContext.kt @@ -8,25 +8,29 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.operati internal interface JsContext { - fun interpret(parent : Expression, op: String?, args: List): Expression? + fun interpret(parent: Expression, op: String?, args: List?): Expression? companion object : JsContext { - override fun interpret(parent : Expression, op: String?, args: List): Expression? { - return when (op) { - - "toString" -> Expression { p, v, s -> parent(p, v, s).toString() } - "indexOf", "lastIndexOf" -> { - checkArgs(args, 1, op) - JsIndexOf( - value = parent, - search = args.argAt(0), - last = op == "lastIndexOf" - ) + override fun interpret(parent: Expression, op: String?, args: List?): Expression? { + return if (args != null){ + when (op) { + "toString" -> Expression { p, v, s -> parent(p, v, s).toString() } + "indexOf", "lastIndexOf" -> { + checkArgs(args, 1, op) + JsIndexOf( + value = parent, + search = args.argAt(0), + last = op == "lastIndexOf" + ) + } + + else -> JsNumberContext.interpret(parent, op, args) + ?: JsStringContext.interpret(parent, op, args) } - - else -> JsNumberContext.interpret(parent, op, args) - ?: JsStringContext.interpret(parent, op, args) + } else { + JsNumberContext.interpret(parent, op, args) + ?: JsStringContext.interpret(parent, op, args) } } } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/number/JsNumberContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/number/JsNumberContext.kt index af237248..deb26217 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/number/JsNumberContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/number/JsNumberContext.kt @@ -1,16 +1,19 @@ package io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.js.number import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression +import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgsNotNull import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.js.JsContext internal object JsNumberContext : JsContext { - override fun interpret(parent : Expression, op: String?, args: List): Expression? { + override fun interpret(parent: Expression, op: String?, args: List?): Expression? { return when(op){ "toFixed" -> { + checkArgsNotNull(args, op) JsToFixed(parent, args.singleOrNull()) } "toPrecision" -> { + checkArgsNotNull(args, op) JsToPrecision(parent, args.singleOrNull()) } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/string/JsStringContext.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/string/JsStringContext.kt index 1e6b1513..176ea3b3 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/string/JsStringContext.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/js/string/JsStringContext.kt @@ -4,6 +4,7 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.Express import io.github.alexzhirkevich.compottie.internal.animation.expressions.argAt import io.github.alexzhirkevich.compottie.internal.animation.expressions.argForNameOrIndex import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgs +import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgsNotNull import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.cast import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.js.JsContext import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.value.OpIndex @@ -11,7 +12,7 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.operati internal object JsStringContext : JsContext { - override fun interpret(parent: Expression, op: String?, args: List): Expression? { + override fun interpret(parent: Expression, op: String?, args: List?): Expression? { return when (op) { "charAt", "at" -> { checkArgs(args, 1, op) @@ -28,68 +29,103 @@ internal object JsStringContext : JsContext { } } - "endsWith" -> JsEndsWith( - string = parent, - searchString = args.argForNameOrIndex(0, "searchString")!!, - position = args.argForNameOrIndex(1, "endPosition") - ) - - "startsWith" -> JsStartsWith( - string = parent, - searchString = args.argForNameOrIndex(0, "searchString")!!, - position = args.argForNameOrIndex(1, "position") - ) - - "includes" -> JsIncludes( - string = parent, - searchString = args.argForNameOrIndex(0, "searchString")!!, - position = args.argForNameOrIndex(1, "position") - ) - - "padStart" -> JsPadStart( - string = parent, - targetLength = args.argForNameOrIndex(0, "targetLength")!!, - padString = args.argForNameOrIndex(1, "padString") - ) - - "padEnd" -> JsPadEnd( - string = parent, - targetLength = args.argForNameOrIndex(0, "targetLength")!!, - padString = args.argForNameOrIndex(1, "padString") - ) - - "match" -> JsMatch( - string = parent, - regexp = args.argAt(0) - ) - - "replace", "replaceAll" -> JsReplace( - string = parent, - pattern = args.argForNameOrIndex(0, "pattern")!!, - replacement = args.argForNameOrIndex(1, "replacement")!!, - all = op.endsWith("All") - ) - - "repeat" -> JsRepeat( - string = parent, - count = args.argAt(0) - ) - - "trim", "trimStart", "trimEnd" -> JsTrim( - string = parent, - start = op != "trimEnd", - end = op != "trimStart" - ) - - "substring", "substr" -> JsSubstring( - string = parent, - start = args.argForNameOrIndex(0, "start", "indexStart")!!, - end = args.argForNameOrIndex(1, "end", "indexEnd") - ) - - "toUppercase", "toLocaleLowerCase" -> parent.cast(String::uppercase) - "toLowerCase", "toLocaleUppercase" -> parent.cast(String::lowercase) - else -> null + "endsWith" -> { + checkArgsNotNull(args, op) + JsEndsWith( + string = parent, + searchString = args.argForNameOrIndex(0, "searchString")!!, + position = args.argForNameOrIndex(1, "endPosition") + ) + } + + "startsWith" -> { + checkArgsNotNull(args, op) + JsStartsWith( + string = parent, + searchString = args.argForNameOrIndex(0, "searchString")!!, + position = args.argForNameOrIndex(1, "position") + ) + } + + "includes" -> { + checkArgsNotNull(args, op) + JsIncludes( + string = parent, + searchString = args.argForNameOrIndex(0, "searchString")!!, + position = args.argForNameOrIndex(1, "position") + ) + } + + "padStart" -> { + checkArgsNotNull(args, op) + JsPadStart( + string = parent, + targetLength = args.argForNameOrIndex(0, "targetLength")!!, + padString = args.argForNameOrIndex(1, "padString") + ) + } + + "padEnd" -> { + checkArgsNotNull(args, op) + JsPadEnd( + string = parent, + targetLength = args.argForNameOrIndex(0, "targetLength")!!, + padString = args.argForNameOrIndex(1, "padString") + ) + } + + "match" -> { + checkArgsNotNull(args, op) + JsMatch( + string = parent, + regexp = args.argAt(0) + ) + } + + "replace", "replaceAll" -> { + checkArgsNotNull(args, op) + JsReplace( + string = parent, + pattern = args.argForNameOrIndex(0, "pattern")!!, + replacement = args.argForNameOrIndex(1, "replacement")!!, + all = op.endsWith("All") + ) + } + + "repeat" -> { + checkArgsNotNull(args, op) + JsRepeat( + string = parent, + count = args.argAt(0) + ) + } + + "trim", "trimStart", "trimEnd" -> { + JsTrim( + string = parent, + start = op != "trimEnd", + end = op != "trimStart" + ) + } + + "substring", "substr" -> { + checkArgsNotNull(args, op) + JsSubstring( + string = parent, + start = args.argForNameOrIndex(0, "start", "indexStart")!!, + end = args.argForNameOrIndex(1, "end", "indexEnd") + ) + } + + "toUppercase", "toLocaleLowerCase" -> { + parent.cast(String::uppercase) + } + "toLowerCase", "toLocaleUppercase" -> { + parent.cast(String::lowercase) + } + else -> { + null + } } } } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/math/OpMath.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/math/OpMath.kt index 5ea2b607..10ccf606 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/math/OpMath.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/math/OpMath.kt @@ -10,7 +10,7 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.Express import io.github.alexzhirkevich.compottie.internal.animation.expressions.argAt import io.github.alexzhirkevich.compottie.internal.animation.expressions.checkArgs import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.random.OpRandomNumber -import kotlin.js.JsName +import io.github.alexzhirkevich.compottie.internal.animation.expressions.operations.unresolvedReference import kotlin.math.abs import kotlin.math.acos import kotlin.math.acosh @@ -52,7 +52,17 @@ internal object OpMath : Expression, ExpressionContext { override fun interpret( op: String?, - args: List + args: List? + ): Expression { + return if (args == null){ + interpretVar(op) + } else { + interpretFun(op, args) + } + } + + private fun interpretVar( + op: String?, ): Expression { return when (op) { "PI" -> PI @@ -63,6 +73,14 @@ internal object OpMath : Expression, ExpressionContext { "LOG2E" -> LOG2E "SQRT1_2" -> SQRT1_2 "SQRT2" -> SQRT2 + else -> unresolvedReference("$op", "Math") + } + } + private fun interpretFun( + op: String?, + args: List + ): Expression { + return when (op) { "abs" -> op1(args, ::abs, op) "asoc" -> op1(args, ::acos, op) "asoch" -> op1(args, ::acosh, op) @@ -97,7 +115,7 @@ internal object OpMath : Expression, ExpressionContext { "tanh" -> op1(args, ::tanh, op) "trunc" -> op1(args, ::truncate, op) - else -> error("Unsupported Math operation: $op") + else -> unresolvedReference("$op", "Math") } } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpAssign.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpAssign.kt index b1051d25..f723f75c 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpAssign.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpAssign.kt @@ -5,8 +5,10 @@ import io.github.alexzhirkevich.compottie.internal.animation.RawProperty import io.github.alexzhirkevich.compottie.internal.animation.expressions.EvaluationContext import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableScope internal class OpAssign( + val scope : VariableScope, val variableName : String, val assignableValue : Expression, private val merge : ((Any, Any) -> Any)? @@ -19,15 +21,19 @@ internal class OpAssign( ): Undefined { val v = assignableValue.invoke(property, context, state) - val current = context.variables[variableName] + val current = context.getVariable(variableName) check(merge == null || current != null) { "Cant modify $variableName as it is undefined" } - context.variables[variableName] = if (current != null && merge != null) { - merge.invoke(current, v) - } else v + context.setVariable( + name = variableName, + value = if (current != null && merge != null) { + merge.invoke(current, v) + } else v, + scope = scope + ) return Undefined } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpAssignByIndex.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpAssignByIndex.kt index e339170f..de9bdd9b 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpAssignByIndex.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpAssignByIndex.kt @@ -6,22 +6,23 @@ import io.github.alexzhirkevich.compottie.internal.animation.Vec2 import io.github.alexzhirkevich.compottie.internal.animation.expressions.EvaluationContext import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableScope internal class OpAssignByIndex( private val variableName : String, + private val scope : VariableScope, private val index : Expression, private val assignableValue : Expression, private val merge : ((Any, Any) -> Any)? ) : Expression { - override tailrec fun invoke( property: RawProperty, context: EvaluationContext, state: AnimationState, ): Undefined { val v = assignableValue.invoke(property, context, state) - val current = context.variables[variableName] + val current = context.getVariable(variableName) check(merge == null || current != null) { @@ -29,7 +30,7 @@ internal class OpAssignByIndex( } if (current == null) { - context.variables[variableName] = mutableListOf() + context.setVariable(variableName, mutableListOf(), scope) return invoke(property, context, state) } else { val i = index.invoke(property, context, state) @@ -57,7 +58,7 @@ internal class OpAssignByIndex( } is List<*> -> { - context.variables[variableName] = current.toMutableList() + context.setVariable(variableName, current.toMutableList(), scope) return invoke(property, context, state) } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpGetVariable.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpGetVariable.kt index 93ac9479..45c0aecb 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpGetVariable.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpGetVariable.kt @@ -4,20 +4,21 @@ import io.github.alexzhirkevich.compottie.internal.AnimationState import io.github.alexzhirkevich.compottie.internal.animation.RawProperty import io.github.alexzhirkevich.compottie.internal.animation.expressions.EvaluationContext import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression -import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableScope internal class OpGetVariable( val name : String, - val declare : Boolean = false + val assignInScope : VariableScope? = null ) : Expression { + override fun invoke( property: RawProperty, context: EvaluationContext, state: AnimationState ): Any { - return if (declare) { - context.variables[name] = 0f // array assigns are processed with OpAssign - } else checkNotNull(context.variables[name]) { + return if (assignInScope != null) { + context.setVariable(name, 0f, assignInScope) + } else checkNotNull(context.getVariable(name)) { "Undefined variable: $name" } } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpVar.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpVar.kt index 14f0a414..181d0b05 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpVar.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/animation/expressions/operations/value/OpVar.kt @@ -6,13 +6,16 @@ import io.github.alexzhirkevich.compottie.internal.animation.expressions.Evaluat import io.github.alexzhirkevich.compottie.internal.animation.expressions.Expression import io.github.alexzhirkevich.compottie.internal.animation.expressions.ExpressionContext import io.github.alexzhirkevich.compottie.internal.animation.expressions.Undefined +import io.github.alexzhirkevich.compottie.internal.animation.expressions.VariableScope -internal object OpVar : Expression, ExpressionContext { +internal class OpVar( + val scope : VariableScope +) : Expression, ExpressionContext { - override fun interpret(op: String?, args: List): Expression { + override fun interpret(op: String?, args: List?): Expression { return if (op == null) Expression.UndefinedExpression - else OpGetVariable(op, declare = true) + else OpGetVariable(op, assignInScope = scope) } override fun invoke( diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/helpers/LottieBlendMode.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/helpers/LottieBlendMode.kt index ec4d7596..6e763831 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/helpers/LottieBlendMode.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/helpers/LottieBlendMode.kt @@ -24,11 +24,13 @@ internal value class LottieBlendMode(val type : Byte){ val Saturation = LottieBlendMode(13) val Color = LottieBlendMode(14) val Luminosity = LottieBlendMode(15) + val Add = LottieBlendMode(16) + val Mix = LottieBlendMode(17) } } internal fun LottieBlendMode.asComposeBlendMode() : BlendMode { - return BlendModeMapping[this] ?: error("Unknown lottie blend mode: $this") + return BlendModeMapping[this] ?: BlendMode.SrcOver } private val BlendModeMapping = mapOf( diff --git a/compottie/src/commonTest/kotlin/expressions/global/CustomFunctionsTest.kt b/compottie/src/commonTest/kotlin/expressions/global/CustomFunctionsTest.kt new file mode 100644 index 00000000..7bedf697 --- /dev/null +++ b/compottie/src/commonTest/kotlin/expressions/global/CustomFunctionsTest.kt @@ -0,0 +1,113 @@ +package expressions.global + +import expressions.assertExprReturns +import expressions.ret +import kotlin.test.Test + +internal class CustomFunctionsTest { + + @Test + fun creation() { + """ + var $ret; + function test(a,b) { return sum(a,b) } + $ret = test(1,2) + """.trimIndent().assertExprReturns(3f) + + """ + var $ret; + function test(a,b) { + return sum(a,b) + } + $ret = test(1,2) + """.trimIndent().assertExprReturns(3f) + + """ + var $ret; + function test(a,b) + { + let x = b + 1 + return sum(a,x) + } + $ret = test(1,2) + """.trimIndent().assertExprReturns(4f) + } + + @Test + fun scopes() { + """ + var $ret; + function test(a,b) + { + var x = b + 1 + return sum(a,x) + } + test(1,2) + $ret = x + """.trimIndent().assertExprReturns(3f) + + """ + var $ret; + function test(a,b) + { + let x = b + 1 + return sum(a,x) + } + test(1,2) + $ret = x + """.trimIndent().assertExprReturns(0f) + + """ + var $ret; + function test(a,b) + { + const x = b + 1 + return sum(a,x) + } + test(1,2) + $ret = x + """.trimIndent().assertExprReturns(0f) + } + + @Test + fun defaultArgs(){ + """ + var $ret; + function test(a, b = 2) + { + return sum(a,b) + } + $ret = test(1) + """.trimIndent().assertExprReturns(3f) + + """ + var $ret; + function test(a, b = 2) + { + return sum(a,b) + } + $ret = test(2,3) + """.trimIndent().assertExprReturns(5f) + + """ + var $ret; + function test(a = 1, b = 2) + { + return sum(a,b) + } + $ret = test() + """.trimIndent().assertExprReturns(3f) + } + + @Test + fun namedArgs(){ + """ + var $ret; + function test(a, b) + { + return sum(a,b) + } + $ret = test(b = 2, a = 1) + """.trimIndent().assertExprReturns(3f) + } +} \ No newline at end of file