MarfVectors.kt
src/jvmMain/kotlin/afield/audio/synth/MarfVectors.kt · 92 lines
@file:afield.runtime.completion.CompletionCategory("Patterns")
package afield.audio.synth
import afield.math.linalg.Vector2f
import afield.math.linalg.Vector3f
import afield.math.linalg.Vector4f
MarfSpace instances for Field's generated linalg vectors, so a reader can emit a position, a colour or a set of material parameters instead of one number.
Everything in Marf.kt is generic over the value type already; this file supplies the arithmetic
for the three types most stage material actually is. The reason it is a file of its own is the
dependency direction: the core layer needs nothing but PatPattern, and keeping afield.math.linalg
out of it means a reader over some other type costs one object rather than an edit to the layer.
val trajectory = marfBankIn(
MarfVector3f,
stageTo(Vector3f(0f, 0f, 0f), 1.0),
stageTo(Vector3f(1f, 0.5f, 0f), 0.5, MarfCurve.Smooth),
stageTo(Vector3f(0f, 1f, 1f), 2.0, MarfCurve.Exp(3.0))
)
val path = trajectory.read(pat("<0 1 2>").nums(), fields = MarfFieldClock.STAGES)
path.at(3.5).value // a Vector3f
path.signal { it.x.toDouble() } // and any component as an ordinary signal
Every instance copies before it writes
This matters more here than the arithmetic does. In afield.math.linalg, add(other),
mul(scalar), sub(other) and normalize() are in-place members that return the receiver,
and / resolves to the mutating member operator rather than to the allocating extension. A stage
target lives in a bank and is read once per stage for as long as the reader runs, so a single
in-place write would corrupt the material rather than produce a wrong frame:
a.add(b) // WRONG: mutates a, which is the bank's stored target
a.add(b, Vector3f()) // right: fresh destination, a and b untouched
Every method below uses the out-parameter form. MarfVectorSpaceTest reads a bank twice and
requires the second pass to agree with the first, which is the property that a mutating
implementation breaks and a type check does not.
object MarfVector2f : MarfSpace<Vector2f>
2-component vector stages. Screen positions, UV coordinates, a pan/tilt pair.
override val zero: Vector2f get() = Vector2f()
override fun add(a: Vector2f, b: Vector2f): Vector2f = a.add(b, Vector2f())
override fun sub(a: Vector2f, b: Vector2f): Vector2f = a.sub(b, Vector2f())
override fun scale(a: Vector2f, k: Double): Vector2f = a.mul(k.toFloat(), Vector2f())
override fun lerp(from: Vector2f, to: Vector2f, fraction: Double): Vector2f
override fun lerp(from: Vector2f, to: Vector2f, fraction: Double): Vector2f =
from.lerp(to, fraction.toFloat(), Vector2f())
override fun label(value: Vector2f): String
override fun label(value: Vector2f): String = "${fmtComponent(value.x)},${fmtComponent(value.y)}"
}
object MarfVector3f : MarfSpace<Vector3f>
3-component vector stages. Positions, directions, RGB.
override val zero: Vector3f get() = Vector3f()
override fun add(a: Vector3f, b: Vector3f): Vector3f = a.add(b, Vector3f())
override fun sub(a: Vector3f, b: Vector3f): Vector3f = a.sub(b, Vector3f())
override fun scale(a: Vector3f, k: Double): Vector3f = a.mul(k.toFloat(), Vector3f())
override fun lerp(from: Vector3f, to: Vector3f, fraction: Double): Vector3f
override fun lerp(from: Vector3f, to: Vector3f, fraction: Double): Vector3f =
from.lerp(to, fraction.toFloat(), Vector3f())
override fun label(value: Vector3f): String
override fun label(value: Vector3f): String =
"${fmtComponent(value.x)},${fmtComponent(value.y)},${fmtComponent(value.z)}"
}
object MarfVector4f : MarfSpace<Vector4f>
4-component vector stages. RGBA, a quaternion's storage, four packed material parameters.
override val zero: Vector4f get() = Vector4f()
override fun add(a: Vector4f, b: Vector4f): Vector4f = a.add(b, Vector4f())
override fun sub(a: Vector4f, b: Vector4f): Vector4f = a.sub(b, Vector4f())
override fun scale(a: Vector4f, k: Double): Vector4f = a.mul(k.toFloat(), Vector4f())
override fun lerp(from: Vector4f, to: Vector4f, fraction: Double): Vector4f
override fun lerp(from: Vector4f, to: Vector4f, fraction: Double): Vector4f =
from.lerp(to, fraction.toFloat(), Vector4f())
override fun label(value: Vector4f): String
override fun label(value: Vector4f): String =
"${fmtComponent(value.x)},${fmtComponent(value.y)},${fmtComponent(value.z)},${fmtComponent(value.w)}"
const val QUATERNIONS_ARE_NOT_A_VECTOR_SPACE: String =
Note on quaternions: a Vector4f reader over four quaternion components interpolates
componentwise and then does not renormalise, which is nlerp without the n and is not a
rotation path. Read the four numbers and normalise at the point of use, or write a reader
whose blend is slerp — see the note on vector spaces in MarfSpace.
"componentwise interpolation of a quaternion needs renormalising before it is a rotation"
}
Short, locale-free component text, so a stage label stays readable in a figure.
private fun fmtComponent(v: Float): String = patFormatNumber(Math.round(v * 1000.0) / 1000.0)