200 lines
7.1 KiB
Kotlin
200 lines
7.1 KiB
Kotlin
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)
|
|
} |