Skip to content

RectSLImg.kt

src/jvmMain/kotlin/afield/lang/rectsl/RectSLImg.kt · 97 lines

package afield.lang.rectsl

import afield.lang.rectsl.study.CpuRectSLRasterizer
import afield.lang.rectsl.study.RectSLDrawable
import afield.lang.rectsl.study.RectSLStudy
import afield.media.image.Img
import kotlin.math.pow

A RectSL scene as a sequence of Imgs: the bridge from the rectangle language to everything in Field that reads an image — afield.media.image filters, showImg, and Patch.mapImage, which puts a scene on a light rig.

Frames come from the CPU reference rasterizer the RectSL study tools use (CpuRectSLRasterizer), so no window or graphics context is needed: the scene's background, its rectangles blended as the GPU blends them into an 8-bit target, and its StackSL materials run through the StackSL GLSL interpreter. One sample per pixel, at the pixel's centre; the wall fills the frame whatever its ->fit says, row 0 at the top of the wall.

The source is compiled once, here, including any {{ … }} Kotlin escapes; a scene that does not compile throws, naming its errors.

val scene = RectSLImager(source)
val img = scene.frame(1.5, 256, 128)          // the colours as typed, 0..1
patch.mapImage(img.resize(16, 8))             // one fixture per cell, area-averaged

sRGB or light. By default a frame holds the colours as typed: 0x808080 is 0.502, which is what the RectSL pictures show and what an 8-bit target stores. Pass linear = true for the light those values stand for (0.216 for that grey). Averaging, blurring or down-sizing a frame is only physically right on linear values, and an LED's PWM is linear in light; so a rig driven from a linear frame at gamma 1 and a rig driven from an as-typed frame at withGamma(2.2) both show roughly the right brightness, but only the first averages a thin line to the right energy.

class RectSLImager(val source: String)

A compiled RectSL scene that renders Img frames at any time and size.

@property source the RectSL program, with or without the #rectsl first line.

class RectSLImager(val source: String) {
    private val drawable: RectSLDrawable = RectSLStudy.drawable(source)

val scene: SceneProgram get() = drawable.scene

The compiled scene: its wall, duration, fps and rectangles.

val duration: Double get() = scene.duration

The scene's ->duration, in seconds.

val fps: Double get() = scene.fps

The scene's ->fps.

fun heightFor(width: Int): Int

Rows for a frame width pixels across that keeps the wall's aspect (at least 1).

    fun heightFor(width: Int): Int = maxOf(1, Math.round(width * scene.wall.height / scene.wall.width).toInt())

fun frame(time: Double, width: Int, height: Int = heightFor(width), linear: Boolean = false): Img

The scene at time seconds as a 3-channel Img, width × height pixels, row 0 at the top. Colours are as typed unless linear (see the class notes). time is not wrapped: past the scene's duration its tracks hold their last values, as SceneProgram.sample does.

    fun frame(time: Double, width: Int, height: Int = heightFor(width), linear: Boolean = false): Img {
        require(width >= 1 && height >= 1) { "a frame is at least 1 x 1, got $width x $height" }
        var raster: afield.lang.stacksl.cpu.CpuRaster? = null
        CpuRectSLRasterizer.render(drawable, width, height, listOf(time)) { _, r -> raster = r }
        val rgba = requireNotNull(raster).rgba
        val out = FloatArray(width * height * 3)
        for (p in 0 until width * height) for (c in 0 until 3) {
            val v = rgba[p * 4 + c].toDouble()
            out[p * 3 + c] = (if (linear) v else encodeSrgb(v)).toFloat()
        }
        return Img(width, height, 3, out)
    }

fun frames(count: Int, width: Int, height: Int = heightFor(width), linear: Boolean = false): List<Img>

count frames evenly spaced over the scene's duration, the last one at duration.

    fun frames(count: Int, width: Int, height: Int = heightFor(width), linear: Boolean = false): List<Img> {
        require(count >= 1)
        return (0 until count).map { i -> frame(if (count == 1) 0.0 else duration * i / (count - 1.0), width, height, linear) }
    }

override fun toString()

    override fun toString() = "RectSLImager(${scene.wall.width} x ${scene.wall.height} wall, ${scene.duration}s, ${scene.nodes.size} nodes)"

companion object

    companion object {

fun encodeSrgb(v: Double): Double

The sRGB transfer function: linear light 0..1 to the value a colour picker shows.

        fun encodeSrgb(v: Double): Double {
            val c = v.coerceIn(0.0, 1.0)
            return if (c <= 0.0031308) c * 12.92 else 1.055 * c.pow(1 / 2.4) - 0.055
        }
    }
}

fun rectSLImage(source: String, time: Double, width: Int, height: Int? = null, linear: Boolean = false): Img

source at time as an Img — RectSLImager for one frame. Compiles the scene every call.

fun rectSLImage(source: String, time: Double, width: Int, height: Int? = null, linear: Boolean = false): Img {
    val imager = RectSLImager(source)
    return imager.frame(time, width, height ?: imager.heightFor(width), linear)
}