TimedGesture.kt
src/jvmMain/kotlin/afield/graphics/paint/TimedGesture.kt · 86 lines
@file:afield.runtime.completion.CompletionCategory("Graphics")
package afield.graphics.paint
A hand movement sampled in time, with pressure and pen orientation, which drives a brush: TimedGesture and its TimedGesture.Sample.
data class TimedGesture(val samples: List<Sample>)
A movement of the hand in time: where the tool is, when, how hard it presses, and how the handle
is held. An afield.geometry.path.FLine gives only a path; flicking, dragging slowly, pausing, pressing
and lifting need the time and the orientation too. PaintCanvas.gesture makes one from an FLine at a
speed; a pen or an agent can give the samples themselves.
Coordinates are those of the canvas's FLines (canvas units, or its view), Sample.t seconds
from the gesture's start. Pressure 1 is a firm press, and more is harder still. For a
WaterBrush, 0 is lifted. For a DynamicBrush, 0 is the hairs' tips just touching the surface,
and below 0 lifts them clear of it, so a flick ends below 0.
init
init {
require(samples.isNotEmpty()) { "a gesture needs at least one sample" }
require(samples.zipWithNext().all { (a, b) -> b.t >= a.t }) { "a gesture's times must not go backwards" }
}
data class Sample
One moment of a gesture.
@param tilt the handle's lean from upright, degrees (0 straight down onto the paper). @param azimuth which way the handle leans, degrees in the canvas (0 toward +x, 90 toward +y). @param roll the handle's turn about its own axis, degrees. @param s how far along its movement the tool is here, mm, measured in the world the gesture was made in (PaintCanvas.gesture3d): what is laid out along a stroke (the hand's drift, the bristles' wander, the pressure profile, how fast the load runs down, a knife's tears, a rag's creases, a pencil's tremor) follows it, so two eyes of a stereo pair that see one movement at different lengths still paint it alike. NaN: the length on the canvas. @param piece which movement of the hand this sample belongs to: a new value lifts the tool.
data class Sample(
val x: Double, val y: Double, val t: Double, val pressure: Double,
val tilt: Double = 0.0, val azimuth: Double = 0.0, val roll: Double = 0.0,
val s: Double = Double.NaN, val piece: Int = 0,
)
val duration: Double get() = samples.last().t - samples.first().t
How long it lasts, seconds.
fun faster(factor: Double): TimedGesture
The same gesture factor times as fast.
fun faster(factor: Double): TimedGesture = TimedGesture(samples.map { it.copy(t = it.t / factor) })
fun pause(seconds: Double): TimedGesture
The same gesture held still for seconds at its end, the tool still down.
fun pause(seconds: Double): TimedGesture = TimedGesture(samples + samples.last().copy(t = samples.last().t + seconds))
fun then(next: TimedGesture, gap: Double = 0.0): TimedGesture
This gesture followed by next, gap seconds later: as further movements of the hand (an
ordinary brush lifts between them; a brush of moving hairs is carried on through).
fun then(next: TimedGesture, gap: Double = 0.0): TimedGesture {
val t0 = samples.last().t + gap - next.samples.first().t
val p0 = samples.last().piece + 1 - next.samples.first().piece
val s0 = samples.last().s.takeIf { it.isFinite() }?.let { it - (next.samples.first().s.takeIf { v -> v.isFinite() } ?: 0.0) } ?: 0.0
return TimedGesture(samples + next.samples.map { it.copy(t = it.t + t0, piece = it.piece + p0, s = it.s + s0) })
}
fun reversed(): TimedGesture
The same path backwards, at the same pace.
fun reversed(): TimedGesture {
val end = samples.last().t
val sEnd = samples.last().s
val pEnd = samples.last().piece + samples.first().piece
return TimedGesture(samples.reversed().map { it.copy(t = end - it.t, s = sEnd - it.s, piece = pEnd - it.piece) })
}
val pieces: List<List<Sample>> get()
The movements of the hand, each its run of samples (Sample.piece).
val pieces: List<List<Sample>> get() {
val out = mutableListOf<MutableList<Sample>>()
for (s in samples) if (out.isEmpty() || out.last().last().piece != s.piece) out += mutableListOf(s) else out.last() += s
return out
}
internal fun xytp(): FloatArray
internal fun xytp(): FloatArray = FloatArray(samples.size * 4).also { a ->
samples.forEachIndexed { i, s ->
a[4 * i] = s.x.toFloat(); a[4 * i + 1] = s.y.toFloat(); a[4 * i + 2] = s.t.toFloat(); a[4 * i + 3] = s.pressure.toFloat()
}
}
}