WaterMedia.kt
src/jvmMain/kotlin/afield/graphics/paint/WaterMedia.kt · 215 lines
@file:afield.runtime.completion.CompletionCategory("Graphics")
package afield.graphics.paint
import afield.math.linalg.Vec4
import java.lang.foreign.MemorySegment
Watercolour and ink on paper: the Paper kinds, WaterPigments and their Watercolors palette, the WaterPaint mixed from them, and the reports and ledgers a wash returns.
A sheet is made with PaintCanvas.paper and worked with PaintCanvas.waterBrush. WaterMap
names the fields of the sheet's state that can be read back. Test: WaterMediaTest.
enum class Paper(internal val id: String)
A sheet of paper for watercolour and ink (PaintCanvas.paper). Each is a physical make-up — how
much water it holds, how fast it drinks and wicks, its grain, tooth and sizing — not a texture:
the same drop behaves differently on each because the water does. The numbers are estimates in
the ranges reported for each kind of paper.
enum class Paper(internal val id: String) {
300 g/m² cotton, cold pressed: a moderate tooth, sized, so a wash stands for a while before it sinks in.
ColdPress("cold press"),
The same sheet hot pressed: smooth, a little harder sized. Crisp edges.
HotPress("hot press"),
A deep tooth that a nearly dry brush skips across.
Rough("rough"),
Thin unsized xuan (rice) paper of long fibres, for ink: it drinks at once and feathers along its grain.
Xuan("xuan"),
Thick unsized blotting paper: takes up a lot of water fast, and holds the ink where it lands.
Blotting("blotting"),
}
data class WaterPigment
A pigment as water carries it: its optics (Kubelka–Munk absorption k per channel and scattering
s, per µg/mm² of pigment) and what decides how it moves — particle density (g/cm³) and
diameterUm (dense, coarse pigment settles out onto the paper within a minute; fine pigment
stays suspended), stain (per second it adsorbs onto fibres from water in the sheet, after which
it can't be lifted) and lift (per second a deposit goes back into still water standing on it; a
moving brush lifts much faster).
Watercolors has named ones; WaterPigment.of makes one from a colour.
data class WaterPigment(
/** What to call it. [Watercolors] uses the common name, and [WaterPigment.of] takes any. */
val name: String,
/** Kubelka–Munk absorption per µg/mm²: three entries, one per channel, in `r, g, b` order. */
val k: List<Double>,
/** Kubelka–Munk scattering per µg/mm². Low is a dye, high a body colour. */
val s: Double,
/** Particle density, g/cm³. With [diameterUm], this is what decides how fast it settles. */
val density: Double,
/**
* Particle diameter, µm. Dense, coarse pigment settles onto the paper within a minute; fine
* pigment stays suspended and travels with the water.
*/
val diameterUm: Double,
/** Per second, how much adsorbs onto fibres from water in the sheet; after that it will not lift. */
val stain: Double,
/**
* Per second, how much of a deposit goes back into still water standing on it. A moving brush
* lifts much faster than this.
*/
val lift: Double,
) {
internal fun params(): FloatArray
internal fun params(): FloatArray = floatArrayOf(k[0].toFloat(), k[1].toFloat(), k[2].toFloat(), s.toFloat(),
density.toFloat(), diameterUm.toFloat(), stain.toFloat(), lift.toFloat())
operator fun times(concentration: Double): WaterPaint = WaterPaint(mapOf(this to concentration))
In paint concentration µg per mm³ of water (a strong wash is a few).
companion object
companion object {
fun of
A pigment whose masstone (laid thick) is masstone (sRGB), tinting with absorption
strength per µg/mm², opacity its scattering relative to that (0 a dye, 1 a body colour,
more for more).
fun of(
name: String, masstone: Vec4, strength: Double = 1.0, opacity: Double = 0.05, density: Double = 2.5,
diameterUm: Double = 0.5, stain: Double = 0.02, lift: Double = 0.004,
): WaterPigment {
val v = PaintNative.aqPigmentMake(linear(masstone.x), linear(masstone.y), linear(masstone.z), strength.toFloat(),
opacity.toFloat(), density.toFloat(), diameterUm.toFloat(), stain.toFloat(), lift.toFloat())
return fromParams(name, v)
}
internal fun fromParams(name: String, v: FloatArray)
internal fun fromParams(name: String, v: FloatArray) =
WaterPigment(name, listOf(v[0].toDouble(), v[1].toDouble(), v[2].toDouble()), v[3].toDouble(),
v[4].toDouble(), v[5].toDouble(), v[6].toDouble(), v[7].toDouble())
}
}
object Watercolors
Watercolour pigments, by their common names; estimates of each one's physics.
object Watercolors {
val names: List<String> by lazy { PaintNative.aqPigmentNames() }
The names get knows.
operator fun get(name: String): WaterPigment
The pigment called name.
operator fun get(name: String): WaterPigment = PaintNative.aqPigment(name)?.let { WaterPigment.fromParams(name, it) }
?: throw NoSuchElementException("no watercolour pigment \"$name\"; there are: ${names.joinToString()}")
val ultramarine get() = get("ultramarine")
PB29: coarse and dense, it settles fast, and lifts readily.
val phthaloBlue get() = get("phthalo blue")
PB15: very fine and staining: smooth, intense, hard to lift.
val burntSienna get() = get("burnt sienna")
PBr7: an iron oxide, fine but dense.
val quinacridoneRose get() = get("quinacridone rose")
val cadmiumYellow get() = get("cadmium yellow")
PY35: dense and semi-opaque.
val viridian get() = get("viridian")
PG18: coarse, settles fast.
val sumi get() = get("sumi")
Carbon in animal glue: very fine, fixed by its glue once dry.
data class WaterPaint(val pigments: Map<WaterPigment, Double>)
Pigments dissolved in water, each at a concentration in µg per mm³ of water. Add two with +.
data class WaterPaint(val pigments: Map<WaterPigment, Double>) {
operator fun plus(o: WaterPaint)
Both paints at once: concentrations add pigment by pigment, so two washes make a stronger one rather than an average.
operator fun plus(o: WaterPaint) = WaterPaint((pigments.keys + o.pigments.keys).associateWith { (pigments[it] ?: 0.0) + (o.pigments[it] ?: 0.0) })
companion object
companion object {
val Clear = WaterPaint(emptyMap())
Clean water.
data class WashReport(val waterLaid: Double, val waterTaken: Double, val pigmentLaid: Double, val pigmentTaken: Double, val seconds: Double)
What a watercolour stroke did, mm³ of water and µg of pigment.
data class WashReport(val waterLaid: Double, val waterTaken: Double, val pigmentLaid: Double, val pigmentTaken: Double, val seconds: Double)
data class PigmentLedger
One pigment's books on a sheet, µg.
data class PigmentLedger(
/** The pigment these books are for, by [WaterPigment.name]. */
val name: String,
/** In the surface water, moving with it. */
val suspended: Double,
/** In the water in the sheet's pores, moving with it. */
val carried: Double,
/** Settled out of water still on or in the sheet: water flowing through carries it on. */
val deposited: Double,
/** Settled and dried in place, its gum set: rewetting lifts it slowly, a brush faster. */
val glued: Double,
/** Stained into the fibres: it no longer lifts. */
val bound: Double,
/** All of it that has been laid on the sheet, however it has since moved. */
val added: Double,
/** All of it a brush has lifted back off. */
val taken: Double,
/** All of it carried off the edge of the sheet by water that ran away. */
val runoff: Double,
/** Sealed into the picture under paint laid over it. */
val sealed: Double,
) {
val imbalance: Double get()
On and in the sheet, less what the ledger says should be: rounding only.
val imbalance: Double get() = suspended + carried + deposited + glued + bound - (added - taken - runoff - sealed)
}
data class WaterLedger
The water on a sheet and every volume that came or went, mm³ (µl), at clockSeconds.
data class WaterLedger(
/**
* The canvas's own clock when these numbers were read. It advances as strokes are painted and
* as [PaintCanvas.waitSeconds] and [PaintCanvas.wait] let time pass, not with wall time.
*/
val clockSeconds: Double,
/** Standing on the sheet right now. */
val surfaceMm3: Double,
/** In the sheet's pores right now. */
val absorbedMm3: Double,
/** All the water a brush has laid down. */
val addedMm3: Double,
/** All the water a brush has lifted back off. */
val takenMm3: Double,
/** All the water that has evaporated. */
val evaporatedMm3: Double,
/** All the water that has run off the edge of the sheet. */
val runoffMm3: Double,
/** All the surface water that has gone into the sheet so far. */
val absorbedInAllMm3: Double,
/** The same books kept per pigment, one entry for each the sheet has seen. */
val pigments: List<PigmentLedger>,
) {
val imbalanceMm3: Double get()
Water on and in the sheet, less what the ledger says should be: rounding only.
val imbalanceMm3: Double get() = surfaceMm3 + absorbedMm3 - (addedMm3 - takenMm3 - evaporatedMm3 - runoffMm3)
val dry: Boolean get() = surfaceMm3 <= 0.0 && absorbedMm3 <= 0.0
True once no water stands on or in the sheet.
enum class WaterMap(internal val code: Int, val unit: String)
A map of the water's state (PaintCanvas.waterMap): one value per pixel.
enum class WaterMap(internal val code: Int, val unit: String) {
Water standing on the sheet.
SurfaceWater(0, "µm"),
Water in the sheet's pores.
AbsorbedWater(1, "µm"),
The pores' saturation, 0..1.
Saturation(2, ""),
All the pigment, in every state.
Pigment(3, "µg/mm²"),
Suspended(4, "µg/mm²"),
Loose deposit: sediment, and pigment dried in place.
Deposited(5, "µg/mm²"),
Bound(6, "µg/mm²"),
The paper's surface relief.
Relief(7, "µm"),
Where paint has sealed the sheet against water, 0..1.
Sealed(8, "share"),
Pigment in the water in the pores.
Carried(9, "µg/mm²"),
Pigment settled and dried in place, its gum set.
Glued(10, "µg/mm²"),
}