[orx-fcurve] Add mfcurve, FCurve.min, FCurve.max, DemoFCurveSheet01.kt
This commit is contained in:
200
orx-fcurve/src/commonMain/kotlin/FCurveModifier.kt
Normal file
200
orx-fcurve/src/commonMain/kotlin/FCurveModifier.kt
Normal file
@@ -0,0 +1,200 @@
|
||||
package org.openrndr.extra.fcurve
|
||||
|
||||
import org.openrndr.extra.expressions.FunctionExtensions
|
||||
import org.openrndr.extra.expressions.compileFunction1
|
||||
import org.openrndr.math.Vector2
|
||||
|
||||
/**
|
||||
* Modify an [fcurve] string using a [modifiers] string
|
||||
*/
|
||||
fun modifyFCurve(
|
||||
fcurve: String,
|
||||
modifiers: String,
|
||||
constants: Map<String, Double> = emptyMap(),
|
||||
functions: FunctionExtensions = FunctionExtensions.EMPTY
|
||||
): String {
|
||||
val parts = fCurveCommands(fcurve)
|
||||
val mparts = parts.reversed().toMutableList()
|
||||
|
||||
@Suppress("RegExpRedundantEscape")
|
||||
val modifier = Regex("([xy])=\\{([^{}]+)\\}")
|
||||
|
||||
val modifierExpressions = modifier.findAll(modifiers).map { it.groupValues[1] to it.groupValues[2] }.toMap()
|
||||
|
||||
val xModifierExpression = modifierExpressions["x"]
|
||||
val xModifier =
|
||||
if (xModifierExpression != null) compileFunction1(xModifierExpression, "x", constants, functions) else {
|
||||
{ x: Double -> x }
|
||||
}
|
||||
|
||||
val yModifierExpression = modifierExpressions["y"]
|
||||
val yModifier =
|
||||
if (yModifierExpression != null) compileFunction1(yModifierExpression, "y", constants, functions) else {
|
||||
{ y: Double -> y }
|
||||
}
|
||||
|
||||
fun popToken(): String = mparts.removeLast()
|
||||
fun popNumber(): Double = mparts.removeLast().toDoubleOrNull() ?: error("not a number")
|
||||
|
||||
fun String.numberOrFactorOf(percentageOf: (Double) -> Double): Double {
|
||||
return if (endsWith("%")) {
|
||||
val f = (dropLast(1).toDoubleOrNull() ?: error("'$this' is not a percentage")) / 100.0
|
||||
percentageOf(f)
|
||||
} else {
|
||||
toDoubleOrNull() ?: error("'$this' is not a number")
|
||||
}
|
||||
}
|
||||
|
||||
fun String.numberOrPercentageOf(percentageOf: () -> Double): Double {
|
||||
return numberOrFactorOf { f -> f * percentageOf() }
|
||||
}
|
||||
|
||||
var cursor = Vector2.ZERO
|
||||
var modified = ""
|
||||
fun emit(command: String, vararg ops: Double, relative: Boolean, x: Double, y: Double) {
|
||||
modified = modified + " " + command + " " + ops.joinToString(" ")
|
||||
cursor = if (relative) {
|
||||
Vector2(x + cursor.x, y + cursor.y)
|
||||
} else {
|
||||
Vector2(x + cursor.x, y)
|
||||
}
|
||||
}
|
||||
|
||||
while (mparts.isNotEmpty()) {
|
||||
val command = mparts.removeLast()
|
||||
|
||||
when (command) {
|
||||
|
||||
/**
|
||||
* Handle move cursor command
|
||||
*/
|
||||
"m", "M" -> {
|
||||
val relative = command.first().isLowerCase()
|
||||
val rf = if (relative) 1.0 else 0.0
|
||||
val y = popNumber() + rf * cursor.y
|
||||
emit("M", yModifier(y), relative = false, x = 0.0, y = y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle line command
|
||||
*/
|
||||
"l", "L" -> {
|
||||
val relative = command.first().isLowerCase()
|
||||
val rf = if (relative) 1.0 else 0.0
|
||||
val x = popNumber()
|
||||
val y = popNumber() + rf * cursor.y
|
||||
|
||||
emit("L", xModifier(x), yModifier(y), relative = false, x = x, y = y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cubic bezier command
|
||||
*/
|
||||
"c", "C" -> {
|
||||
val relative = command.first().isLowerCase()
|
||||
val rf = if (relative) 1.0 else 0.0
|
||||
|
||||
val tcx0 = popToken()
|
||||
val tcy0 = popToken()
|
||||
val tcx1 = popToken()
|
||||
val tcy1 = popToken()
|
||||
val x = popNumber()
|
||||
val y = popNumber()
|
||||
val ay = y + cursor.y * rf
|
||||
val x0 = tcx0.numberOrPercentageOf { x }
|
||||
val y0 = tcy0.numberOrFactorOf { factor ->
|
||||
if (relative) y * factor else cursor.y * (1.0 - factor).coerceAtLeast(0.0) + y * factor
|
||||
} + cursor.y * rf
|
||||
val x1 = tcx1.numberOrPercentageOf { x }
|
||||
val y1 = tcy1.numberOrFactorOf { factor ->
|
||||
if (relative) y * factor else cursor.y * (1.0 - factor).coerceAtLeast(0.0) + y * factor
|
||||
} + cursor.y * rf
|
||||
emit(
|
||||
"C",
|
||||
xModifier(x0),
|
||||
yModifier(y0),
|
||||
xModifier(x1),
|
||||
yModifier(y1),
|
||||
xModifier(x),
|
||||
xModifier(ay),
|
||||
relative = false,
|
||||
x = x,
|
||||
y = ay
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle quadratic bezier command
|
||||
*/
|
||||
"q", "Q" -> {
|
||||
val relative = command.first().isLowerCase()
|
||||
val rf = if (relative) 1.0 else 0.0
|
||||
val tcx0 = popToken()
|
||||
val tcy0 = popToken()
|
||||
val x = popNumber()
|
||||
val y = popNumber()
|
||||
val ay = y + cursor.y * rf
|
||||
val x0 = tcx0.numberOrPercentageOf { x }
|
||||
val y0 = tcy0.numberOrFactorOf { factor ->
|
||||
if (relative) y * factor else cursor.y * (1.0 - factor).coerceAtLeast(0.0) + y * factor
|
||||
} + rf * cursor.y
|
||||
emit("Q", xModifier(x0), yModifier(y0), xModifier(x), yModifier(ay), relative = false, x = x, y = ay)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle horizontal line (or hold) command
|
||||
*/
|
||||
"h", "H" -> {
|
||||
if (command == "H") {
|
||||
val x = popNumber()
|
||||
emit(command, xModifier(x), relative = false, x = x, y = cursor.y)
|
||||
cursor = Vector2(x, cursor.y)
|
||||
} else {
|
||||
val x = popNumber()
|
||||
emit(command, xModifier(x), relative = false, x = x, y = cursor.y)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cubic smooth to command
|
||||
*/
|
||||
"s", "S" -> {
|
||||
val relative = command.first().isLowerCase()
|
||||
val rf = if (relative) 1.0 else 0.0
|
||||
val tcx0 = popToken()
|
||||
val tcy0 = popToken()
|
||||
val x = popNumber()
|
||||
val y = popNumber()
|
||||
val ay = y + cursor.y * rf
|
||||
val x1 = tcx0.numberOrPercentageOf { x }
|
||||
val y1 = tcy0.numberOrFactorOf { factor ->
|
||||
if (relative) y * factor else cursor.y * (1.0 - factor).coerceAtLeast(0.0) + y * factor
|
||||
} + rf * cursor.y
|
||||
emit("S", xModifier(x1), yModifier(y1), xModifier(x), yModifier(ay), relative = false, x = x, y = ay)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle quadratic smooth to command
|
||||
*/
|
||||
"t", "T" -> {
|
||||
val relative = command.first().isLowerCase()
|
||||
val rf = if (relative) 1.0 else 0.0
|
||||
val x = popNumber()
|
||||
val y = popNumber() + cursor.y * rf
|
||||
emit("T", xModifier(x), yModifier(y), relative = false, x = x, y = y)
|
||||
}
|
||||
|
||||
else -> error("unknown command: $command in ${parts}")
|
||||
}
|
||||
}
|
||||
return modified
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val f = "l 10 10 h 4 t 20.0 20.0 s 5% 50% 30.0 30.0"
|
||||
println(modifyFCurve(f, "x={sqrt(x)} y={y * 2.0}"))
|
||||
|
||||
val mf = "l 10 10 h 4 t 20.0 20.0 s 5% 50% 30.0 30.0 | x={2.0 * x} y={-3.0 * y}"
|
||||
val f2 = mfcurve(mf)
|
||||
println(f2)
|
||||
}
|
||||
Reference in New Issue
Block a user