Documents' helpers are compiled into the kernels they feed, so a document calling a VM-only helper (Motion's prog/pulse/spring_step, the native ease(@name, u)) could not share it with a kernel; the P(DOOM) plates carried their own copies for that reason. - std/shared.splash (documents, shaders, kernels): prog(t, a, b) (0 at a, 1 at b, held outside; b below a runs from 1 down to 0, a = b steps), remap(x, a, b, c, d), pulse(t, t0, half_life), spring_step(t, freq, damping). The kernel prelude's remap and std.anim's pulse (an exp form of the same curve) are deleted; anim.pulse_sum uses the shared one. - The kernel parser reads `@name` and lowers ease(@ease_x, u) to std.ease's x(u) (@linear: clamp; @ease_in/@ease_out/@ease_in_out: quad), Motion's formulas; an ease(…) without an @name is an ordinary call (a kernel's own `ease`). Checked: cargo test --release -p makepad-script-compute --features vm (all, the AI-style corpus with its own ease functions included); every P(DOOM) plate rendered through the commercial change on top. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
154 lines
6.5 KiB
Text
154 lines
6.5 KiB
Text
// Stdlib additions (SPLASH-TOKENS.md §5.5), in the kernel prelude's style but
|
|
// written for the subset the shader compiler also takes: explicit `return`,
|
|
// no integer ops. Every function is plain arithmetic, inlined where called.
|
|
|
|
// ---- Math -------------------------------------------------------------------
|
|
|
|
// The VJ mid-knob multiplier: 1.0 at a dial's rest (0.5), 0.2..1.8 over its travel.
|
|
fn knob(x) { return 0.2 + 1.6 * clamp(x, 0.0, 1.0) }
|
|
// Rotate a 2D point by `a` radians (counter-clockwise).
|
|
fn rot2(p, a) {
|
|
let c = cos(a)
|
|
let s = sin(a)
|
|
return vec2(p.x * c - p.y * s, p.x * s + p.y * c)
|
|
}
|
|
// Rec. 601 luma of an rgb colour.
|
|
fn luma(c) { return dot(c, vec3(0.299, 0.587, 0.114)) }
|
|
// 1 inside the unit square (0..1 on both axes), 0 outside.
|
|
fn inside(uv) { return step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0) }
|
|
// x wrapped into 0..n.
|
|
fn wrap(x, n) { return x - n * floor(x / n) }
|
|
|
|
// ---- Timing (documents, shaders and kernels alike) --------------------------
|
|
|
|
// 0 at `a`, 1 at `b`, linear between and held outside: `ease(@ease_out_expo, prog(t, t0, t0 + 0.3))`
|
|
// (b below a runs from 1 down to 0; a = b steps there).
|
|
fn prog(t, a, b) {
|
|
if b == a { return step(a, t) }
|
|
return clamp((t - a) / (b - a), 0.0, 1.0)
|
|
}
|
|
// `x` from the range a..b to the range c..d, unclamped: `remap(t, 2.0, 3.0, 0.0, 360.0)`.
|
|
fn remap(x, a, b, c, d) { return c + (x - a) * (d - c) / (b - a) }
|
|
// A decaying hit: 1 at `t0`, halving every `half_life` s, 0 before (slams, shakes, flashes).
|
|
fn pulse(t, t0, half_life) {
|
|
if t < t0 { return 0.0 }
|
|
return pow(0.5, (t - t0) / half_life)
|
|
}
|
|
// A damped spring's response to a step at 0 (0 -> 1, an overshoot and a settle): `freq` Hz,
|
|
// `damping` 0..1 (`spring_step(t - t_open, 3.4, 0.42)` opens with a 23 % overshoot at 0.16 s).
|
|
fn spring_step(t, freq, damping) {
|
|
if t <= 0.0 { return 0.0 }
|
|
let w = 6.283185307179586 * freq
|
|
return 1.0 - exp(0.0 - damping * w * t) * cos(w * sqrt(1.0 - damping * damping) * t)
|
|
}
|
|
|
|
// ---- Colour -----------------------------------------------------------------
|
|
|
|
// Cosine palette: a + b * cos(2 pi (c t + d)).
|
|
fn palette(t, a, b, c, d) { return a + b * cos(6.283185307179586 * (c * t + d)) }
|
|
fn hsv2rgb(c) {
|
|
let k = vec3(1.0, 2.0 / 3.0, 1.0 / 3.0)
|
|
let p = abs(fract(vec3(c.x, c.x, c.x) + k) * 6.0 - vec3(3.0, 3.0, 3.0))
|
|
return c.z * mix(vec3(1.0, 1.0, 1.0), clamp(p - vec3(1.0, 1.0, 1.0), vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0)), c.y)
|
|
}
|
|
|
|
// One sRGB-encoded channel in linear light, and back.
|
|
fn srgb_to_linear1(x) {
|
|
if x <= 0.04045 { return x / 12.92 }
|
|
return pow((x + 0.055) / 1.055, 2.4)
|
|
}
|
|
fn linear_to_srgb1(x) {
|
|
if x <= 0.0031308 { return x * 12.92 }
|
|
return 1.055 * pow(x, 1.0 / 2.4) - 0.055
|
|
}
|
|
// An sRGB-encoded rgb (a #hex colour's .xyz, a picture's texel) in linear light.
|
|
fn srgb_to_linear(c) { return vec3(srgb_to_linear1(c.x), srgb_to_linear1(c.y), srgb_to_linear1(c.z)) }
|
|
// A linear-light rgb sRGB-encoded (what a display or a #hex colour holds).
|
|
fn linear_to_srgb(c) { return vec3(linear_to_srgb1(c.x), linear_to_srgb1(c.y), linear_to_srgb1(c.z)) }
|
|
// A colour's (a `#hex`'s) rgb in linear light: what segments, passes and 3D lights take.
|
|
fn linear_rgb(c) { return vec3(srgb_to_linear1(c.r), srgb_to_linear1(c.g), srgb_to_linear1(c.b)) }
|
|
|
|
// ---- Hashes and noise (float-only, stable across GPUs: no sine) ---------------
|
|
|
|
// A 2D point to 0..1, and to a vec2 in 0..1 (per-cell randomness: grain, motes).
|
|
fn hash12(p) {
|
|
var h = fract(vec3(p.x, p.y, p.x) * 0.1031)
|
|
h = h + dot(h, vec3(h.y, h.z, h.x) + 33.33)
|
|
return fract((h.x + h.y) * h.z)
|
|
}
|
|
fn hash22(p) {
|
|
var h = fract(vec3(p.x, p.y, p.x) * vec3(0.1031, 0.1030, 0.0973))
|
|
h = h + dot(h, vec3(h.y, h.z, h.x) + 33.33)
|
|
return fract(vec2(h.x + h.y, h.x + h.z) * vec2(h.z, h.y))
|
|
}
|
|
fn snoise_mod289(x) { return x - floor(x * (1.0 / 289.0)) * 289.0 }
|
|
fn snoise_permute(x) { return snoise_mod289((x * 34.0 + 10.0) * x) }
|
|
// 2D simplex noise, about -1..1 (after Ashima Arts and Stefan Gustavson, MIT):
|
|
// smooth organic variation without value noise's grid.
|
|
fn snoise2(v) {
|
|
let cx = 0.211324865405187
|
|
let cy = 0.366025403784439
|
|
let cz = -0.577350269189626
|
|
let cw = 0.024390243902439
|
|
var i = floor(v + dot(v, vec2(cy, cy)))
|
|
let x0 = v - i + dot(i, vec2(cx, cx))
|
|
var i1 = vec2(0.0, 1.0)
|
|
if x0.x > x0.y { i1 = vec2(1.0, 0.0) }
|
|
let x12a = vec2(x0.x + cx, x0.y + cx) - i1
|
|
let x12b = vec2(x0.x + cz, x0.y + cz)
|
|
i = snoise_mod289(i)
|
|
let p = snoise_permute(snoise_permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0))
|
|
var m = max(vec3(0.5, 0.5, 0.5) - vec3(dot(x0, x0), dot(x12a, x12a), dot(x12b, x12b)), vec3(0.0, 0.0, 0.0))
|
|
m = m * m
|
|
m = m * m
|
|
let x = 2.0 * fract(p * cw) - 1.0
|
|
let h = abs(x) - 0.5
|
|
let a0 = x - floor(x + 0.5)
|
|
m = m * (1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h))
|
|
let g = vec3(a0.x * x0.x + h.x * x0.y, a0.y * x12a.x + h.y * x12a.y, a0.z * x12b.x + h.z * x12b.y)
|
|
return 130.0 * dot(m, g)
|
|
}
|
|
|
|
// ---- Signed distances (2D) --------------------------------------------------
|
|
|
|
fn sd_circle(p, r) { return length(p) - r }
|
|
fn sd_box(p, b) {
|
|
let d = abs(p) - b
|
|
return length(max(d, vec2(0.0, 0.0))) + min(max(d.x, d.y), 0.0)
|
|
}
|
|
fn sd_round_box(p, b, r) { return sd_box(p, b - vec2(r, r)) - r }
|
|
fn sd_segment(p, a, b) {
|
|
let pa = p - a
|
|
let ba = b - a
|
|
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0)
|
|
return length(pa - ba * h)
|
|
}
|
|
fn sd_ring(p, r, w) { return abs(length(p) - r) - w }
|
|
|
|
// ---- Antialiased lines (screen-space width, shaders only) --------------------
|
|
|
|
// A step at `edge` antialiased over one pixel of `x`.
|
|
fn aa_step(edge, x) {
|
|
let w = abs(dFdx(x)) + abs(dFdy(x))
|
|
return clamp((x - edge) / max(w, 0.000001) + 0.5, 0.0, 1.0)
|
|
}
|
|
// Filled where the distance `d` is negative, antialiased.
|
|
fn aa_fill(d) { return 1.0 - aa_step(0.0, d) }
|
|
// Lines of `v` every `spacing`, `width` pixels wide.
|
|
fn isoline(v, spacing, width) {
|
|
let h = v / spacing
|
|
let fw = abs(dFdx(h)) + abs(dFdy(h))
|
|
let d = abs(fract(h) - 0.5) / max(fw, 0.0001)
|
|
return 1.0 - clamp(d - width, 0.0, 1.0)
|
|
}
|
|
|
|
// ---- Dithering --------------------------------------------------------------
|
|
|
|
// The 4x4 Bayer threshold (0..1) at an integer pixel position.
|
|
fn bayer4(p) {
|
|
let x = wrap(floor(p.x), 4.0)
|
|
let y = wrap(floor(p.y), 4.0)
|
|
let a = wrap(x, 2.0) * 2.0 + wrap(y, 2.0) * 3.0 - wrap(x, 2.0) * wrap(y, 2.0) * 4.0
|
|
let b = floor(x / 2.0) * 2.0 + floor(y / 2.0) * 3.0 - floor(x / 2.0) * floor(y / 2.0) * 4.0
|
|
return (a * 4.0 + b + 0.5) / 16.0
|
|
}
|