Skip to content

Commit

Permalink
[orx-fcurve] Add mfcurve, FCurve.min, FCurve.max, DemoFCurveSheet01.kt
Browse files Browse the repository at this point in the history
  • Loading branch information
edwinRNDR committed May 13, 2024
1 parent 968d544 commit cc92dc2
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 50 deletions.
12 changes: 3 additions & 9 deletions orx-fcurve/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ This is an example of a flat horizontal FCurve:

```kotlin
// set the initial value to 0.5, hold that value for 1 seconds
val sizeCurve = fcurve("M0.5 H1")
val sizeCurve = fcurve("M0.5 h1")
```

Two horizontal segments at different heights:
Expand All @@ -36,12 +36,6 @@ val sizeCurve = fcurve("M0.4 h0.5 M0.6 h0.5")
Note that `x` values are relative, except for `H` where `x` is absolute.
For `y` values, lower case commands are relative and upper case commands are absolute.

The last example can be written with absolute times as:

```kotlin
// hold value 0.4 until time 0.5, then hold value 0.6 until time 1.0
val sizeCurve = fcurve("M0.4 H0.5 M0.6 H1.0")
```

### Line

Expand Down Expand Up @@ -103,11 +97,11 @@ commands require the presence of a previous segment, otherwise the program will
```kotlin
// Hold the value 0.5 during 0.2 seconds
// then draw a smooth curve down to 0.5, up to 0.7 down to 0.3 and up to 0.7
val smoothCurveT = fcurve("M0.5 H0.2 T0.2,0.3 T0.2,0.7 T0.2,0.3 T0.2,0.7")
val smoothCurveT = fcurve("M0.5 h0.2 T0.2,0.3 T0.2,0.7 T0.2,0.3 T0.2,0.7")

// Hold the value 0.5 during 0.2 seconds
// then draw a smooth with 4 repetitions where we move up slowly and down quickly
val smoothCurveS = fcurve("M0.5 H0.2 S0.2,0.0,0.2,0.5 S0.2,0.0,0.2,0.5 S0.2,0.0,0.2,0.5 S0.2,0.0,0.2,0.5")
val smoothCurveS = fcurve("M0.5 h0.2 S0.2,0.0,0.2,0.5 S0.2,0.0,0.2,0.5 S0.2,0.0,0.2,0.5 S0.2,0.0,0.2,0.5")
```

## Useful FCurve methods
Expand Down
29 changes: 28 additions & 1 deletion orx-fcurve/src/commonMain/kotlin/EFCurve.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,34 @@ package org.openrndr.extra.fcurve
import org.openrndr.extra.expressions.FunctionExtensions
import org.openrndr.extra.expressions.evaluateExpression

/**
* expand mfcurve to fcurve
*/
fun mfcurve(
mf: String,
constants: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
): String {
/**
* perform comment substitution
*/
val stripped = Regex("(#.*)$", RegexOption.MULTILINE).replace(mf, "")

/**
* detect modifier
*/
val parts = stripped.split("|")

val efcurve = parts.getOrElse(0) { "" }
val modifier = parts.getOrNull(1)

var fcurve = efcurve(efcurve, constants, functions)
if (modifier != null) {
fcurve = modifyFCurve(fcurve, modifier, constants, functions)
}
return fcurve
}

/**
* expand efcurve to fcurve
* @param ef an efcurve string
Expand All @@ -12,7 +40,6 @@ fun efcurve(
ef: String,
constants: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY

): String {
// IntelliJ falsely reports a redundant escape character. the escape character is required when running the regular
// expression on a javascript target. Removing the escape character will result in a `Lone quantifier brackets`
Expand Down
97 changes: 61 additions & 36 deletions orx-fcurve/src/commonMain/kotlin/FCurve.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.openrndr.math.Vector2
import org.openrndr.math.transforms.buildTransform
import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour
import org.openrndr.shape.bounds
import kotlin.math.abs

/**
Expand Down Expand Up @@ -91,6 +92,19 @@ data class FCurve(val segments: List<Segment2D>) {
return FCurve(segments.map { it.reverse.transform(t) })
}

val bounds by lazy {
segments.map { it.bounds }.bounds
}
val min: Double
get() {
if (segments.isEmpty()) return 0.0 else return bounds.position(0.0, 0.0).y
}

val max: Double
get() {
if (segments.isEmpty()) return 0.0 else return bounds.position(1.0, 1.0).y
}

/**
* Change the duration of the Fcurve
*/
Expand Down Expand Up @@ -146,9 +160,8 @@ data class FCurve(val segments: List<Segment2D>) {
0.0
} else {
segments.first().start.x

}
}
}

/**
* The unitless end position of the Fcurve
Expand Down Expand Up @@ -208,24 +221,28 @@ data class FCurve(val segments: List<Segment2D>) {
/**
* Return a list of contours that can be used to visualize the Fcurve
*/
fun contours(scale: Vector2 = Vector2.ONE): List<ShapeContour> {
fun contours(scale: Vector2 = Vector2(1.0, -1.0), offset: Vector2 = Vector2.ZERO): List<ShapeContour> {
var active = mutableListOf<Segment2D>()
val result = mutableListOf<ShapeContour>()

for (segment in segments) {
if (active.isEmpty()) {
active.add(segment.transform(buildTransform {

val tsegment = segment.transform(
buildTransform {
translate(offset)
scale(scale.x, scale.y)
}))
}
)

if (active.isEmpty()) {
active.add(tsegment)
} else {
val dy = abs(active.last().end.y - segment.start.y)
val dy = abs(active.last().end.y - tsegment.start.y)
if (dy > 1E-3) {
result.add(ShapeContour.fromSegments(active, false))
active = mutableListOf()
}
active.add(segment.transform(buildTransform {
scale(scale.x, scale.y)
}))
active.add(tsegment)
}
}
if (active.isNotEmpty()) {
Expand Down Expand Up @@ -293,21 +310,28 @@ class FCurveBuilder {

fun continueTo(x: Double, y: Double, relative: Boolean = false) {
val r = if (relative) 1.0 else 0.0
val lastSegment = segments.last()
val lastDuration = lastSegment.end.x - lastSegment.start.x
val outTangent = segments.last().cubic.control.last()
val outPos = lastSegment.end
val dx = outPos.x - outTangent.x
val dy = outPos.y - outTangent.y
val ts = x / lastDuration
segments.add(
Segment2D(
cursor,
Vector2(cursor.x + dx * ts, cursor.y + dy),
Vector2(cursor.x + x * 0.66, cursor.y * r + y),
Vector2(cursor.x + x, cursor.y * r + y)
).scaleTangents()
)

if (segments.isNotEmpty()) {
val lastSegment = segments.last()
val lastDuration = lastSegment.end.x - lastSegment.start.x
val outTangent = if (segments.last().linear) lastSegment.end else segments.last().control.last()
val outPos = lastSegment.end
val d = outPos - outTangent
//val dn = d.normalized
val ts = 1.0// x / lastDuration
segments.add(
Segment2D(
cursor,
cursor + d * ts,
Vector2(cursor.x + x, cursor.y * r + y)
).scaleTangents()
)
} else {
segments.add(
Segment2D(cursor,
Vector2(cursor.x + x, cursor.y * r + y)).quadratic
)
}
cursor = Vector2(cursor.x + x, cursor.y * r + y)
path += "${if (relative) "t" else "T"}$x,$y"
}
Expand All @@ -334,7 +358,7 @@ class FCurveBuilder {
if (relative) {
lineTo(x, cursor.y)
} else {
require(segments.isEmpty()) { "absolute hold (H $x) is only allowed when used as first command"}
require(segments.isEmpty()) { "absolute hold (H $x) is only allowed when used as first command" }
cursor = cursor.copy(x = x)
}
path += "h$x"
Expand All @@ -361,11 +385,12 @@ fun fcurve(builder: FCurveBuilder.() -> Unit): FCurve {
/**
* Split an Fcurve string in to command parts
*/
private fun fCurveCommands(d: String): List<String> {
fun fCurveCommands(d: String): List<String> {
val svgCommands = "mMlLqQsStTcChH"
val number = "0-9.\\-E%"

return d.split(Regex("(?:[\t ,]|\r?\n)+|(?<=[$svgCommands])(?=[$number])|(?<=[$number])(?=[$svgCommands])")).filter { it.isNotBlank() }
return d.split(Regex("(?:[\t ,]|\r?\n)+|(?<=[$svgCommands])(?=[$number])|(?<=[$number])(?=[$svgCommands])"))
.filter { it.isNotBlank() }
}

private fun evaluateFCurveCommands(parts: List<String>): FCurve {
Expand Down Expand Up @@ -418,8 +443,8 @@ private fun evaluateFCurveCommands(parts: List<String>): FCurve {
*/
"l", "L" -> {
val isRelative = command.first().isLowerCase()
val x = popNumberOrPercentageOf { dx() }
val y = popNumberOrPercentageOf { cursor.y }
val x = popNumber()
val y = popNumber()
lineTo(x, y, isRelative)
}

Expand All @@ -432,8 +457,8 @@ private fun evaluateFCurveCommands(parts: List<String>): FCurve {
val tcy0 = popToken()
val tcx1 = popToken()
val tcy1 = popToken()
val x = popNumberOrPercentageOf { dx() }
val y = popNumberOrPercentageOf { cursor.y }
val x = popNumber()
val y = popNumber()
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
Expand Down Expand Up @@ -476,8 +501,8 @@ private fun evaluateFCurveCommands(parts: List<String>): FCurve {
val relative = command.first().isLowerCase()
val tcx0 = popToken()
val tcy0 = popToken()
val x = popNumberOrPercentageOf { dx() }
val y = popNumberOrPercentageOf { cursor.y }
val x = popNumber()
val y = popNumber()
val x1 = tcx0.numberOrPercentageOf { x }
val y1 = tcy0.numberOrPercentageOf { y }
continueTo(x1, y1, x, y, relative)
Expand All @@ -488,8 +513,8 @@ private fun evaluateFCurveCommands(parts: List<String>): FCurve {
*/
"t", "T" -> {
val isRelative = command.first().isLowerCase()
val x = popNumberOrPercentageOf { dx() }
val y = popNumberOrPercentageOf { cursor.y }
val x = popNumber()
val y = popNumber()
continueTo(x, y, isRelative)
}

Expand Down
Loading

0 comments on commit cc92dc2

Please sign in to comment.