darkly 0.5.0

A GPU-native paint engine on wgpu: brushes, layers, blend modes, masks, selections, and undo.
Documentation
// Procedural `r(θ)` shape prelude — shared by compute-path brushes that
// stamp the same family of polar-radius silhouettes the `shape` fragment
// node produces.
//
// Functions here are parameterised on a `ShapeParams` struct so the same
// math can be reused by multiple terminals without a tight coupling to a
// shader-global `u`. The terminal that includes this prelude is
// responsible for building a `ShapeParams` for each dab (typically from
// its own dab record).
//
// **Bit-exact parity with `crates/darkly/src/brush/nodes/shape.rs`** is
// load-bearing — the CPU-side `integrate_centroid` walks the same `r(θ)`
// at a finer resolution, and per-pixel render tests catch drift as a
// mismatch. Keep the `hash1d` / `fbm_1d` / `r_sine` / `r_perlin` /
// `r_superformula` formulas here byte-equivalent to `shape.rs`.
//
// Include via `concat!()` at the consumer site:
//
//   concat!(
//       include_str!("../../../../../shaders/source_over.wgsl"),
//       "\n",
//       include_str!("../../../../../shaders/brush/_shape.wgsl"),
//       "\n",
//       include_str!("../../../../../shaders/brush/<terminal>.wgsl"),
//   )
//
// Scope: shape-radius only. The soft-disc coverage function (`r_solid` +
// linear falloff) is small enough that each terminal that needs it
// inlines its own copy. Coverage *math* is per-terminal; *radius* math is
// shared.
//
// Credits:
//   - Gielis superformula: Johan Gielis, AJB 90(3), 2003.
//   - 1D value-noise / fBm fundamentals: Ken Perlin; Inigo Quilez
//     (https://iquilezles.org/articles/morenoise/).

struct ShapeParams {
    /// 0 = sine harmonic, 1 = periodic 1D Perlin fBm, 2 = Gielis
    /// superformula. Matches `ALGO_*` constants in `nodes/shape.rs`.
    algorithm: u32,
    /// Modulation strength on top of the unit-radius reference disc.
    amplitude: f32,
    /// Sine / Perlin period; superformula `m` divided by 4.
    frequency: f32,
    /// Rotation (radians) applied to the shape around its centre.
    /// Subtracted from `θ` so positive values rotate clockwise in
    /// screen-y-down space, matching `pen.drawing_angle`'s
    /// `atan2(dy, dx)` convention — wiring drawing_angle into the
    /// shape node's rotation port orients the silhouette along the stroke.
    rotation: f32,
    /// Perlin fBm amplitude falloff per octave.
    persistence: f32,
    /// Per-dab Perlin seed (typically a random scalar).
    seed: f32,
    /// Perlin fBm octave count.
    octaves: u32,
    /// Superformula exponents.
    n1: f32,
    n2: f32,
    n3: f32,
}

const SHAPE_TAU: f32 = 6.28318530717958647692;

/// Integer bit-mix hash — bit-identical to `hash1d` in `shape.rs`.
/// We avoid `fract(sin(x*K)*M)` because `sin` precision
/// differs between CPU and GPU and the `*43758` amplification turns
/// sub-ULP drift into a totally different noise array — which the
/// centroid alignment test would flag.
fn shape_hash1d(x: f32, seed: f32) -> f32 {
    let xi = u32(x);
    let si = u32(seed);
    var h = xi + si * 2654435761u;
    h = h ^ (h >> 16u);
    h = h * 0x85ebca6bu;
    h = h ^ (h >> 13u);
    h = h * 0xc2b2ae35u;
    h = h ^ (h >> 16u);
    return f32(h) / 4294967295.0;
}

/// Periodic 1D value-noise fBm — mirrors `fbm_1d` in `shape.rs`.
fn shape_fbm_1d(t: f32, p: ShapeParams) -> f32 {
    var sum: f32 = 0.0;
    var norm: f32 = 0.0;
    var amp: f32 = 1.0;
    let base_freq = max(i32(p.frequency), 1);
    for (var o: u32 = 0u; o < p.octaves; o = o + 1u) {
        let freq = base_freq << o;
        let freq_f = f32(freq);
        let x = t * freq_f;
        let i = floor(x);
        let f = x - i;
        let s = f * f * (3.0 - 2.0 * f);
        // rem_euclid for non-negative t: i is in [0, freq) already.
        let i_wrapped = i - floor(i / freq_f) * freq_f;
        let i_next = i_wrapped + 1.0 - select(0.0, freq_f, (i_wrapped + 1.0) >= freq_f);
        let a = shape_hash1d(i_wrapped, p.seed);
        let b = shape_hash1d(i_next, p.seed);
        sum = sum + amp * (a * (1.0 - s) + b * s);
        norm = norm + amp;
        amp = amp * p.persistence;
    }
    if (norm > 0.0) {
        return sum / norm;
    }
    return 0.5;
}

fn shape_r_sine(p: ShapeParams, theta: f32) -> f32 {
    return 1.0 + p.amplitude * sin(p.frequency * theta);
}

fn shape_r_perlin(p: ShapeParams, theta: f32) -> f32 {
    var t = theta / SHAPE_TAU;
    t = t - floor(t);
    // fbm in [0, 1] → remap to [-1, 1] so amplitude scales 1:1 with sine.
    return 1.0 + p.amplitude * (2.0 * shape_fbm_1d(t, p) - 1.0);
}

fn shape_r_superformula(p: ShapeParams, theta: f32) -> f32 {
    let m_quarter = p.frequency * theta * 0.25;
    let term_a = pow(abs(cos(m_quarter)), p.n2);
    let term_b = pow(abs(sin(m_quarter)), p.n3);
    let s = term_a + term_b;
    if (s <= 0.0) {
        return 0.0;
    }
    return pow(s, -1.0 / p.n1);
}

/// Polar radius `r(θ)` in the shape's natural units (unmodulated disc has
/// `r = 1`). Branches on `p.algorithm`. Same dispatch table as
/// `shape.rs::r_theta`.
fn shape_r_theta(p: ShapeParams, theta: f32) -> f32 {
    let phased = theta - p.rotation;
    switch p.algorithm {
        case 1u: { return shape_r_perlin(p, phased); }
        case 2u: { return shape_r_superformula(p, phased); }
        default: { return shape_r_sine(p, phased); }
    }
}