onejs-sl 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sl.ts CHANGED
@@ -24,10 +24,10 @@
24
24
 
25
25
  import { parseColor as parseHex } from "./color"
26
26
  import {
27
- Builder, INPUTS, SLError, SL_IR_VERSION, TYPE, hashProgram, widthName,
28
- type InputName, type NodeRef, type Program, type SLNode, type SLType, type UniformControl,
27
+ Builder, INPUTS, RAMP_STOP_COMPUTED, SLError, TYPE, hashProgram, programVersion, truncateHeld, widthName, writtenColour,
28
+ type InputName, type NodeRef, type Program, type SLKind, type SLNode, type SLType, type UniformControl,
29
29
  } from "./ir"
30
- import { SLOP, type SLOpCode } from "./ops"
30
+ import { SLOP, SL_HLSL, SL_NAME, type SLOpCode } from "./ops"
31
31
  import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
32
32
 
33
33
  // MARK: values
@@ -55,7 +55,8 @@ export type Num = Val | number
55
55
  // point rather than a hazard; see the comment above that loop.
56
56
  // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
57
57
  export class Val {
58
- constructor(readonly owner: Builder, readonly ref: NodeRef, readonly width: SLType) {}
58
+ /** `kind` is what the value holds beside its width: an int, a uint or a bool. None means floats. */
59
+ constructor(readonly owner: Builder, readonly ref: NodeRef, readonly width: SLType, readonly kind?: SLKind) {}
59
60
 
60
61
  // Arithmetic. Mixing a vector with a Float broadcasts, as in HLSL.
61
62
  add(o: Num): this { return bin(SLOP.ADD, this, o) as this }
@@ -73,6 +74,11 @@ export class Val {
73
74
  min(o: Num): this { return bin(SLOP.MIN, this, o) as this }
74
75
  max(o: Num): this { return bin(SLOP.MAX, this, o) as this }
75
76
  clamp(lo: Num, hi: Num): this {
77
+ if ([this, lo, hi].some((v) => typeof v !== "number" && v.kind !== undefined)) {
78
+ const u = unify([this, lo, hi])
79
+ if (u.kind !== undefined) return mk(u.owner, u.owner.call(SLOP.CLAMP, u.width, u.refs, undefined, u.kind), u.width, u.kind) as this
80
+ return (mk(u.owner, u.refs[0]!, u.width) as Val).clamp(mk(u.owner, u.refs[1]!, u.width) as Val, mk(u.owner, u.refs[2]!, u.width) as Val) as this
81
+ }
76
82
  const { refs, width } = alignN([this, lo, hi])
77
83
  return mk(this.owner, this.owner.call(SLOP.CLAMP, width, refs), width) as this
78
84
  }
@@ -89,6 +95,27 @@ export class Val {
89
95
  }
90
96
  normalize(): this { return un(SLOP.NORMALIZE, this) as this }
91
97
 
98
+ // Comparisons give a bool per component; an int meeting a float is compared as a float.
99
+ lt(o: Num): Bool { return compare(SLOP.LT, this, o) }
100
+ le(o: Num): Bool { return compare(SLOP.LE, this, o) }
101
+ gt(o: Num): Bool { return compare(SLOP.GT, this, o) }
102
+ ge(o: Num): Bool { return compare(SLOP.GE, this, o) }
103
+ eq(o: Num): Bool { return compare(SLOP.EQ, this, o) }
104
+ ne(o: Num): Bool { return compare(SLOP.NE, this, o) }
105
+
106
+ // Logic, on bools.
107
+ and(o: Val | boolean): Bool { return logic(SLOP.AND, this, o) }
108
+ or(o: Val | boolean): Bool { return logic(SLOP.OR, this, o) }
109
+ not(): Bool { return logic(SLOP.NOT, this) }
110
+
111
+ // Bits, on ints and uints. A shift takes its count modulo 32 on every backend.
112
+ band(o: Num): this { return bits(SLOP.BIT_AND, this, o) as this }
113
+ bor(o: Num): this { return bits(SLOP.BIT_OR, this, o) as this }
114
+ bxor(o: Num): this { return bits(SLOP.BIT_XOR, this, o) as this }
115
+ shl(o: Num): this { return bits(SLOP.SHL, this, o) as this }
116
+ shr(o: Num): this { return bits(SLOP.SHR, this, o) as this }
117
+ bnot(): this { return bits(SLOP.BIT_NOT, this) as this }
118
+
92
119
  /**
93
120
  * Arbitrary swizzle. The common single and full width ones are also getters
94
121
  * (`p.x`, `c.rgb`); this covers everything else and is typed by its argument,
@@ -106,7 +133,7 @@ export class Val {
106
133
  chans.push(c)
107
134
  }
108
135
  const t = chans.length as SLType
109
- return mk(this.owner, this.owner.add({ k: "swizzle", type: t, src: this.ref, chans }), t) as Widen<S>
136
+ return mk(this.owner, this.owner.add(withKind({ k: "swizzle", type: t, src: this.ref, chans }, this.kind)), t, this.kind) as Widen<S>
110
137
  }
111
138
 
112
139
  get x(): Float { return this.swz("x") }
@@ -160,9 +187,18 @@ export interface Float extends Val { readonly width: 1 }
160
187
  export interface Vec2 extends Val { readonly width: 2 }
161
188
  export interface Vec3 extends Val { readonly width: 3 }
162
189
  export interface Vec4 extends Val { readonly width: 4 }
190
+ /** A value holding a bool, an int or a uint rather than floats. */
191
+ export interface Bool extends Val { readonly width: 1; readonly kind: "bool" }
192
+ export interface Int extends Val { readonly width: 1; readonly kind: "int" }
193
+ export interface UInt extends Val { readonly width: 1; readonly kind: "uint" }
194
+
195
+ function mk(b: Builder, ref: NodeRef, width: SLType, kind?: SLKind): any {
196
+ return new Val(b, ref, width, kind)
197
+ }
163
198
 
164
- function mk(b: Builder, ref: NodeRef, width: SLType): any {
165
- return new Val(b, ref, width)
199
+ /** A node with its kind, which a float node leaves out so it keys and hashes as it always did. */
200
+ function withKind<T extends SLNode>(n: T, kind: SLKind | undefined): T {
201
+ return kind === undefined ? n : { ...n, kind }
166
202
  }
167
203
 
168
204
  // MARK: recording context
@@ -233,19 +269,222 @@ function alignN(vs: Num[]): { refs: NodeRef[]; width: SLType } {
233
269
 
234
270
  /** float -> vecN by repeating the component. */
235
271
  function broadcast(v: Val, width: SLType): NodeRef {
236
- return v.owner.add({ k: "swizzle", type: width, src: v.ref, chans: new Array(width).fill(0) })
272
+ return v.owner.add(withKind({ k: "swizzle", type: width, src: v.ref, chans: new Array(width).fill(0) }, v.kind))
237
273
  }
238
274
 
239
275
  function bin(op: SLOpCode, a: Val, o: Num): Val {
276
+ if (a.kind !== undefined || (typeof o !== "number" && o.kind !== undefined)) return kindedBin(op, a, o)
240
277
  const [x, y] = align2(a, o)
241
278
  const width = typeof o === "number" ? a.width : (Math.max(a.width, o.width) as SLType)
242
279
  return mk(a.owner, a.owner.call(op, width, [x, y]), width)
243
280
  }
244
281
 
245
282
  function un(op: SLOpCode, a: Val): Val {
283
+ if (a.kind !== undefined) {
284
+ if ((op === SLOP.NEG || op === SLOP.ABS) && a.kind === "int") return mk(a.owner, a.owner.call(op, a.width, [a.ref], undefined, a.kind), a.width, a.kind)
285
+ a = toFloat(a)
286
+ }
246
287
  return mk(a.owner, a.owner.call(op, a.width, [a.ref]), a.width)
247
288
  }
248
289
 
290
+ // MARK: kinds (IR 4)
291
+
292
+ /** The ops an int or a uint keeps its kind through. Anything else takes it as a float. */
293
+ const INT_OPS = new Set<number>([SLOP.ADD, SLOP.SUB, SLOP.MUL, SLOP.DIV, SLOP.MOD, SLOP.MIN, SLOP.MAX, SLOP.CLAMP])
294
+
295
+ /** A value as floats: itself when it already is, a conversion otherwise (a bool is 0 or 1). */
296
+ export function toFloat(v: Val): Val {
297
+ if (v.kind === undefined) return v
298
+ return mk(v.owner, v.owner.call(SLOP.CAST, v.width, [v.ref]), v.width)
299
+ }
300
+
301
+ /**
302
+ * The operands of an op brought to one kind and one width.
303
+ *
304
+ * Two ints stay ints, and a whole number beside one is an int too. Anything
305
+ * else meeting a float, a bool among them, becomes a float, as HLSL converts
306
+ * it. An int and a uint are refused, since which one wins is a guess.
307
+ */
308
+ function unify(vs: Num[]): { refs: NodeRef[]; width: SLType; kind: SLKind | undefined; owner: Builder } {
309
+ let owner: Builder | null = null
310
+ const kinds = new Set<SLKind | undefined>()
311
+ let width: SLType = 1
312
+ for (const v of vs) {
313
+ if (typeof v === "number") continue
314
+ if (owner === null) owner = v.owner
315
+ else if (v.owner !== owner) throw new SLError("a value from another program cannot be used in this one")
316
+ kinds.add(v.kind)
317
+ if (v.width > width) width = v.width
318
+ }
319
+ const b = owner ?? ctx()
320
+ if (kinds.has("int") && kinds.has("uint")) {
321
+ throw new SLError("cannot combine an int with a uint; convert one with int(...) or uint(...)")
322
+ }
323
+ let kind = kinds.size === 1 ? [...kinds][0] : undefined
324
+ if (kind === "bool") kind = undefined
325
+ if (kind !== undefined && vs.some((v) => typeof v === "number" && !Number.isInteger(v))) kind = undefined
326
+ const refs = vs.map((v) => {
327
+ if (typeof v === "number") return b.constant(new Array(width).fill(v), kind)
328
+ const x = kind === undefined ? toFloat(v) : v
329
+ if (x.width === width) return x.ref
330
+ if (x.width === 1) return broadcast(x, width)
331
+ throw new SLError(`cannot combine a ${widthName(x.width)} with a ${widthName(width)}`)
332
+ })
333
+ return { refs, width, kind, owner: b }
334
+ }
335
+
336
+ function kindedBin(op: SLOpCode, a: Val, o: Num): Val {
337
+ const { refs, width, kind, owner } = unify([a, o])
338
+ if (kind === undefined || !INT_OPS.has(op)) {
339
+ const x = mk(owner, refs[0]!, width) as Val
340
+ return bin(op, x, mk(owner, refs[1]!, width) as Val)
341
+ }
342
+ return mk(owner, owner.call(op, width, refs, undefined, kind), width, kind)
343
+ }
344
+
345
+ function compare(op: SLOpCode, a: Val, o: Num): Bool {
346
+ const { refs, width, kind, owner } = unify([a, o])
347
+ void kind
348
+ return mk(owner, owner.call(op, width, refs, undefined, "bool"), width, "bool")
349
+ }
350
+
351
+ /** A bool operand, a JS boolean made a constant. */
352
+ function asBool(b: Builder, v: Val | boolean, what: string): Val {
353
+ if (typeof v === "boolean") return mk(b, b.constant([v ? 1 : 0], "bool"), 1, "bool")
354
+ if (v.kind !== "bool") throw new SLError(`${what} takes bools, and this is ${v.kind === undefined ? `a ${widthName(v.width)}` : `an ${v.kind}`}; compare it, as in x > 0`)
355
+ return v
356
+ }
357
+
358
+ function logic(op: SLOpCode, a: Val, o?: Val | boolean): Bool {
359
+ const name = op === SLOP.AND ? "&&" : op === SLOP.OR ? "||" : "!"
360
+ const x = asBool(a.owner, a, name)
361
+ if (o === undefined) return mk(a.owner, a.owner.call(op, x.width, [x.ref], undefined, "bool"), x.width, "bool")
362
+ const y = asBool(a.owner, o, name)
363
+ if (x.width !== y.width) throw new SLError(`cannot combine a bool${x.width} with a bool${y.width}`)
364
+ return mk(a.owner, a.owner.call(op, x.width, [x.ref, y.ref], undefined, "bool"), x.width, "bool")
365
+ }
366
+
367
+ function bits(op: SLOpCode, a: Val, o?: Num): Val {
368
+ const word = SL_HLSL[op]?.syntax ?? SL_NAME[op]
369
+ const intOnly = (v: Num) => {
370
+ if (typeof v === "number" ? !Number.isInteger(v) : v.kind !== "int" && v.kind !== "uint") {
371
+ throw new SLError(`${word} works on the bits of an int or a uint, and this is ${typeof v === "number" ? "a fraction" : v.kind === "bool" ? "a bool; use && or ||" : `a ${widthName(v.width)}`}`)
372
+ }
373
+ }
374
+ intOnly(a)
375
+ if (o === undefined) return mk(a.owner, a.owner.call(op, a.width, [a.ref], undefined, a.kind), a.width, a.kind)
376
+ intOnly(o)
377
+ // A shift's count is its own kind; the value shifted keeps its own.
378
+ if (op === SLOP.SHL || op === SLOP.SHR) {
379
+ const count = typeof o === "number" ? a.owner.constant([o], a.kind) : o.ref
380
+ return mk(a.owner, a.owner.call(op, a.width, [a.ref, count], undefined, a.kind), a.width, a.kind)
381
+ }
382
+ const { refs, width, kind, owner } = unify([a, o])
383
+ return mk(owner, owner.call(op, width, refs, undefined, kind), width, kind)
384
+ }
385
+
386
+ /** A conversion to `kind`, or to floats when it is undefined. A float to an int truncates toward zero, held to the int's range. */
387
+ function convert(v: Num | boolean, kind: SLKind | undefined): Val {
388
+ const b = typeof v === "object" ? v.owner : ctx()
389
+ if (typeof v === "boolean") v = mk(b, b.constant([v ? 1 : 0], "bool"), 1, "bool") as Val
390
+ if (typeof v === "number") {
391
+ if (kind === undefined) return mk(b, b.constant([v]), 1)
392
+ if (kind === "bool") return mk(b, b.constant([v !== 0 ? 1 : 0], "bool"), 1, "bool")
393
+ // A whole number the kind holds is that number exactly (`747796405u`);
394
+ // anything else is a float converted, as the GPU would.
395
+ const exact = Number.isInteger(v) && (kind === "int" ? v >= -2147483648 && v <= 2147483647 : v >= 0 && v <= 4294967295)
396
+ return mk(b, b.constant([exact ? v : truncateHeld(v, kind)], kind), 1, kind)
397
+ }
398
+ if (v.kind === kind) return v
399
+ return mk(b, b.call(SLOP.CAST, v.width, [v.ref], undefined, kind), v.width, kind)
400
+ }
401
+
402
+ /** An int: a whole number, or a value converted, a float truncating toward zero. */
403
+ export function int(v: Num | boolean): Int { return convert(v, "int") as Int }
404
+ export function uint(v: Num | boolean): UInt { return convert(v, "uint") as UInt }
405
+ /** A bool: true or false, or a value converted, where anything but zero is true. */
406
+ export function bool(v: Num | boolean): Bool { return convert(v, "bool") as Bool }
407
+
408
+ function choose(b: Builder, cond: Num | boolean, whenTrue: Num, whenFalse: Num): Val {
409
+ if (typeof cond === "boolean") return convertTo(cond ? whenTrue : whenFalse, whenTrue, whenFalse)
410
+ // What SELECT reads a float condition as, so the two agree.
411
+ const c = typeof cond === "number" ? asBool(b, cond >= 0.5, "a choice")
412
+ : cond.kind === "bool" ? cond : cond.kind === undefined ? cond.ge(0.5) : cond.ne(0)
413
+ if (c.width !== 1) throw new SLError("a choice takes one bool, and this is several; combine them with && or ||")
414
+ const { refs, width, kind, owner } = unify([whenTrue, whenFalse])
415
+ return mk(owner, owner.call(SLOP.CHOOSE, width, [c.ref, ...refs], undefined, kind), width, kind)
416
+ }
417
+
418
+ /** `v` as the type the two would unify to, for a choice whose condition is known. */
419
+ function convertTo(v: Num, a: Num, c: Num): Val {
420
+ const { refs, width, kind, owner } = unify([a, c])
421
+ return mk(owner, v === a ? refs[0]! : refs[1]!, width, kind)
422
+ }
423
+
424
+ /**
425
+ * A real branch: only the side `cond` picks runs, and its results come back.
426
+ * Both sides give the same number of results, pairwise of one type; a plain
427
+ * number takes its partner's. A value only one side needs is computed only when
428
+ * that side runs.
429
+ */
430
+ export function branch(cond: Val | boolean, whenTrue: () => Num[], whenFalse: () => Num[]): Val[] {
431
+ const b = ctx()
432
+ if (typeof cond === "boolean") return (cond ? whenTrue() : whenFalse()).map((v) => (typeof v === "number" ? float(v) : v))
433
+ const c = asBool(b, cond, "a branch")
434
+ if (c.width !== 1) throw new SLError("a branch takes one bool, and this is several; combine them with && or ||")
435
+ const t = whenTrue()
436
+ const f = whenFalse()
437
+ if (t.length !== f.length) throw new SLError(`a branch's sides give ${t.length} and ${f.length} results; they must give the same`)
438
+ const types = t.map((x, i) => pairType(x, f[i]!, i))
439
+ if (types.length === 0) return []
440
+ const node = b.add({ k: "if", cond: c.ref, then: t.map((v, i) => typed(b, v, types[i]!)), else: f.map((v, i) => typed(b, v, types[i]!)) })
441
+ return types.map((ty, i) => mk(b, b.add(withKind({ k: "proj", type: ty.width, src: node, index: i }, ty.kind)), ty.width, ty.kind))
442
+ }
443
+
444
+ /**
445
+ * A real loop. The values start as `init`; while `cond` of them holds, and
446
+ * fewer than `max` turns have run, they become `body` of them. What comes back
447
+ * is the values when it stops. `max` is what guarantees it stops: a loop that
448
+ * reaches it leaves as if its condition had failed.
449
+ */
450
+ export function loop(init: Num[], cond: (v: Val[]) => Val | boolean, body: (v: Val[]) => Num[], max = 1024): Val[] {
451
+ const b = ctx()
452
+ if (!Number.isInteger(max) || max < 1) throw new SLError(`a loop's turn limit is a whole number from 1, got ${max}`)
453
+ const start = init.map((v) => (typeof v === "number" ? (float(v) as Val) : v))
454
+ const id = b.newLoop()
455
+ const params = start.map((v, i) => mk(b, b.add(withKind({ k: "param", type: v.width, loop: id, index: i }, v.kind)), v.width, v.kind) as Val)
456
+ const c = asBool(b, cond(params), "a loop's condition")
457
+ if (c.width !== 1) throw new SLError("a loop's condition is one bool, and this is several; combine them with && or ||")
458
+ const next = body(params)
459
+ if (next.length !== params.length) throw new SLError(`a loop carries ${params.length} values and its body gave ${next.length}`)
460
+ const ty = params.map((p) => ({ width: p.width, kind: p.kind }))
461
+ const node = b.add({
462
+ k: "loop", id, init: start.map((v) => v.ref), cond: c.ref,
463
+ next: next.map((v, i) => { pairType(params[i]!, v, i); return typed(b, v, ty[i]!) }), max,
464
+ })
465
+ return ty.map((t, i) => mk(b, b.add(withKind({ k: "proj", type: t.width, src: node, index: i }, t.kind)), t.width, t.kind))
466
+ }
467
+
468
+ interface ValType { width: SLType; kind: SLKind | undefined }
469
+
470
+ /** The one type two results share, or why they do not. */
471
+ function pairType(a: Num, c: Num, i: number): ValType {
472
+ if (typeof a === "number" && typeof c === "number") return { width: 1, kind: undefined }
473
+ if (typeof a === "number") return { width: (c as Val).width, kind: (c as Val).kind }
474
+ if (typeof c === "number") return { width: a.width, kind: a.kind }
475
+ if (a.width !== c.width || a.kind !== c.kind) {
476
+ const name = (v: Val) => (v.kind === undefined ? widthName(v.width) : v.width === 1 ? v.kind : `${v.kind}${v.width}`)
477
+ throw new SLError(`result ${i} is a ${name(a)} one way and a ${name(c)} the other`)
478
+ }
479
+ return { width: a.width, kind: a.kind }
480
+ }
481
+
482
+ /** A result as a node of its type, a plain number becoming a constant. */
483
+ function typed(b: Builder, v: Num, t: ValType): NodeRef {
484
+ if (typeof v === "number") return b.constant(new Array(t.width).fill(v), t.kind)
485
+ return v.ref
486
+ }
487
+
249
488
  // MARK: the public surface
250
489
 
251
490
  export interface ProgramInputs {
@@ -288,7 +527,7 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
288
527
  }
289
528
  const nodes: SLNode[] = b.nodes.slice()
290
529
  return {
291
- version: SL_IR_VERSION,
530
+ version: programVersion(nodes, out.ref),
292
531
  nodes,
293
532
  result: out.ref,
294
533
  uniforms: b.uniforms.slice(),
@@ -302,7 +541,8 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
302
541
 
303
542
  export function float(v: Num): Float {
304
543
  const b = ctx()
305
- return typeof v === "number" ? mk(b, b.constant([v]), TYPE.FLOAT) : (v as Float)
544
+ if (typeof v !== "number") return (v.kind === undefined ? v : toFloat(v)) as Float
545
+ return mk(b, b.constant([v]), TYPE.FLOAT)
306
546
  }
307
547
 
308
548
  /** Builds a wider value from narrower parts, which must add up exactly. */
@@ -478,8 +718,12 @@ export function atan2(y: Num, x: Num): Float {
478
718
  * rather than an `if`: a real branch would have to survive both backends
479
719
  * identically, and this does not.
480
720
  */
481
- export function select(cond: Num, whenTrue: Num, whenFalse: Num): Val {
721
+ export function select(cond: Num | boolean, whenTrue: Num, whenFalse: Num): Val {
482
722
  const b = ctx()
723
+ const kinded = (v: Num | boolean) => typeof v === "object" && v.kind !== undefined
724
+ // A bool condition, or results that are not floats: a choice, which carries
725
+ // any kind, where SELECT's arithmetic carries only floats (IR 4).
726
+ if (typeof cond === "boolean" || kinded(cond) || kinded(whenTrue) || kinded(whenFalse)) return choose(b, cond, whenTrue, whenFalse)
483
727
  const c = typeof cond === "number" ? float(cond) : cond
484
728
  const t = typeof whenTrue === "number" ? float(whenTrue) : whenTrue
485
729
  const f = typeof whenFalse === "number" ? float(whenFalse) : whenFalse
@@ -533,6 +777,10 @@ export function uniformDefaults(p: Program): number[] {
533
777
  * fields `fx.noise` draws: a value or simplex base, layered as fBm, or the
534
778
  * turbulence and ridged variants built on simplex. A program has no seed;
535
779
  * offset the input for a different field. Octaves are 1 to 4, as in fx.
780
+ *
781
+ * The octave count may be a value, a uniform say, rounded to the nearest whole
782
+ * number and held to 1 to 4 where it is read. It is an operand either way
783
+ * (IR 3); a constant one is checked here, and prints as the number it is.
536
784
  */
537
785
  export function noise(p: Vec2): Float {
538
786
  return mk(p.owner, p.owner.call(SLOP.NOISE, TYPE.FLOAT, [p.ref]), TYPE.FLOAT)
@@ -540,24 +788,31 @@ export function noise(p: Vec2): Float {
540
788
  export function simplex(p: Vec2): Float {
541
789
  return mk(p.owner, p.owner.call(SLOP.SIMPLEX, TYPE.FLOAT, [p.ref]), TYPE.FLOAT)
542
790
  }
543
- function octaveCount(name: string, octaves: number): number {
544
- if (!Number.isInteger(octaves) || octaves < 1 || octaves > 4) {
545
- throw new SLError(`${name} octaves must be a whole number from 1 to 4, got ${octaves}`)
791
+ /**
792
+ * An octave op: the point and the count as operands, with `rest` (fbm's kind)
793
+ * the immediates. A constant count is checked here.
794
+ */
795
+ function octaves(op: SLOpCode, name: string, p: Vec2, count: Num, rest: number[]): Float {
796
+ const c = components(p.owner, count)
797
+ if (c.length !== 1) throw new SLError(`${name}'s octave count is one number, and this is a ${widthName(c.length as SLType)}`)
798
+ const n = c[0]!
799
+ if (typeof n === "number" && (!Number.isInteger(n) || n < 1 || n > 4)) {
800
+ throw new SLError(`${name} octaves must be a whole number from 1 to 4, got ${n}`)
546
801
  }
547
- return octaves
802
+ const v = typeof n === "number" ? float(n) : n
803
+ return mk(p.owner, p.owner.call(op, TYPE.FLOAT, [p.ref, v.ref], rest), TYPE.FLOAT)
548
804
  }
549
805
  /** Layered noise. `base` picks the grid ("value", the default) or triangles ("simplex"). */
550
- export function fbm(p: Vec2, octaves = 3, base: "value" | "simplex" = "value"): Float {
551
- const kind = base === "simplex" ? 1 : 0
552
- return mk(p.owner, p.owner.call(SLOP.FBM, TYPE.FLOAT, [p.ref], [octaveCount("fbm", octaves), kind]), TYPE.FLOAT)
806
+ export function fbm(p: Vec2, octaveCount: Num = 3, base: "value" | "simplex" = "value"): Float {
807
+ return octaves(SLOP.FBM, "fbm", p, octaveCount, [base === "simplex" ? 1 : 0])
553
808
  }
554
809
  /** Sum of |simplex| octaves: creases that stack into veins and licks. Fire, smoke, marble. */
555
- export function turbulence(p: Vec2, octaves = 3): Float {
556
- return mk(p.owner, p.owner.call(SLOP.TURBULENCE, TYPE.FLOAT, [p.ref], [octaveCount("turbulence", octaves)]), TYPE.FLOAT)
810
+ export function turbulence(p: Vec2, octaveCount: Num = 3): Float {
811
+ return octaves(SLOP.TURBULENCE, "turbulence", p, octaveCount, [])
557
812
  }
558
813
  /** The turbulence crease made bright and squared: ridges, lightning, cracks. */
559
- export function ridged(p: Vec2, octaves = 3): Float {
560
- return mk(p.owner, p.owner.call(SLOP.RIDGED, TYPE.FLOAT, [p.ref], [octaveCount("ridged", octaves)]), TYPE.FLOAT)
814
+ export function ridged(p: Vec2, octaveCount: Num = 3): Float {
815
+ return octaves(SLOP.RIDGED, "ridged", p, octaveCount, [])
561
816
  }
562
817
 
563
818
  /** "#rgb", "#rrggbb" or "#rrggbbaa" to 0..1 components. Shared with fx. */
@@ -581,14 +836,23 @@ export function parseColor(hex: string): [number, number, number, number] {
581
836
  *
582
837
  * This is the argument for the EDSL in miniature. A library function that
583
838
  * composes from primitives costs one function here and nothing anywhere else.
839
+ *
840
+ * A stop is a colour as written: a hex, four numbers, or a value that is a
841
+ * colour as written, which is one `color`, `sl.uniform.colour` or a hex
842
+ * literal made (a colour uniform, say). The ramp blends the colours as
843
+ * written, so a value computed any other way, already in the working space,
844
+ * has nothing it could blend, and is refused.
584
845
  */
585
- export function ramp(t: Num, stops: Array<string | [number, number, number, number]>): Vec4 {
846
+ export function ramp(t: Num, stops: Array<string | [number, number, number, number] | Vec3 | Vec4>): Vec4 {
586
847
  if (stops.length < 2) throw new SLError(`a ramp needs at least 2 stops, got ${stops.length}`)
587
848
  const tv = (typeof t === "number" ? float(t) : t).saturate()
849
+ const written = stops.map((s) => (s instanceof Val ? asWritten(s) : null))
588
850
  // Each stop is built immediately before the mix that consumes it. The graph
589
851
  // and the hash are the same either way (the hash is over the shape, not the
590
852
  // storage order); the order only mattered to the VM's register allocator.
591
853
  const colourAt = (i: number) => {
854
+ const w = written[i]
855
+ if (w !== null && w !== undefined) return w
592
856
  const v = typeof stops[i] === "string" ? parseColor(stops[i] as string) : (stops[i] as number[])
593
857
  return vec4(v[0], v[1], v[2], v[3])
594
858
  }
@@ -606,6 +870,18 @@ export function ramp(t: Num, stops: Array<string | [number, number, number, numb
606
870
  return toLinear(out)
607
871
  }
608
872
 
873
+ /**
874
+ * A colour value as it was written, a float4: the operand of the `toLinear`
875
+ * that reading it as a colour applied, with alpha 1 when it has three
876
+ * components. Anything else was computed in the working space.
877
+ */
878
+ function asWritten(v: Vec3 | Vec4): Vec4 {
879
+ const ref = writtenColour(v.owner.nodes, v.ref)
880
+ if (ref === null) throw new SLError(RAMP_STOP_COMPUTED)
881
+ const raw = mk(v.owner, ref, v.width) as Val
882
+ return v.width === TYPE.VEC4 ? (raw as Vec4) : vec4(raw, 1)
883
+ }
884
+
609
885
  /**
610
886
  * Signed distance to a shape, positive outside and negative inside.
611
887
  *
@@ -621,27 +897,45 @@ export function ramp(t: Num, stops: Array<string | [number, number, number, numb
621
897
  *
622
898
  * A shape takes up to `SL_SDF_PARAMS[kind]` parameters, six at most. Fewer
623
899
  * fills zeros, which is all a program could pass before six shapes grew their
624
- * fifth and sixth.
900
+ * fifth and sixth. A vector parameter counts as its components, so a box's
901
+ * half extents can be one float2.
625
902
  *
626
903
  * Which parameters a shape takes is the shape's own business; `circle` wants a
627
904
  * radius, `roundedBox` wants half extents and a corner. See `lib/sdf2d.hlsl`.
905
+ *
906
+ * A parameter may be any value, a uniform or one computed per pixel. The six
907
+ * are operands either way (IR 3), a float4 and a float2, and constant ones
908
+ * print as the numbers they are.
628
909
  */
629
- export function sdf(kind: SlSdfKind, p: Vec2, params: number[] = []): Float {
910
+ export function sdf(kind: SlSdfKind, p: Vec2, params: Num[] = []): Float {
630
911
  const id = SL_SDF_SHAPES[kind]
631
912
  if (id === undefined) throw new SLError(`"${kind}" is not a shape; see SL_SDF_SHAPES for the 42 names`)
913
+ const c = params.flatMap((v) => components(p.owner, v))
632
914
  // At least four, so a program that passed a shape more than it reads, which
633
915
  // was harmless when every shape took four, still builds.
634
916
  const most = Math.max(4, SL_SDF_PARAMS[kind])
635
- if (params.length > most) {
636
- throw new SLError(`sl.sdf("${kind}") takes at most ${most} parameters and was given ${params.length}`)
917
+ if (c.length > most) {
918
+ throw new SLError(`sl.sdf("${kind}") takes at most ${most} parameters and was given ${c.length}`)
637
919
  }
638
- for (const v of params) {
639
- if (!Number.isFinite(v)) throw new SLError(`sl.sdf parameters must be finite, got ${v}`)
920
+ for (const v of c) {
921
+ if (typeof v === "number" && !Number.isFinite(v)) throw new SLError(`sl.sdf parameters must be finite, got ${v}`)
640
922
  }
641
- // Four always, as before, and a fifth and sixth only when given: a program
642
- // using four or fewer is the same node, and hashes the same, as it was.
643
- const imm = [params[0] ?? 0, params[1] ?? 0, params[2] ?? 0, params[3] ?? 0, ...params.slice(4)]
644
- return mk(p.owner, p.owner.call(SLOP.SDF, TYPE.FLOAT, [p.ref], [id, ...imm]), TYPE.FLOAT)
923
+ const q = vec4(c[0] ?? 0, c[1] ?? 0, c[2] ?? 0, c[3] ?? 0)
924
+ const r = vec2(c[4] ?? 0, c[5] ?? 0)
925
+ return mk(p.owner, p.owner.call(SLOP.SDF, TYPE.FLOAT, [p.ref, q.ref, r.ref], [id]), TYPE.FLOAT)
926
+ }
927
+
928
+ /**
929
+ * A value as its components: numbers for a constant, which is what it would
930
+ * have been written as, and a float per component otherwise.
931
+ */
932
+ function components(b: Builder, v: Num): Num[] {
933
+ if (typeof v === "number") return [v]
934
+ if (v.owner !== b) throw new SLError("a value from another program cannot be used in this one")
935
+ const n = b.nodes[v.ref]!
936
+ if (n.k === "const") return n.v.slice()
937
+ if (v.width === 1) return [v]
938
+ return Array.from({ length: v.width }, (_, i) => v.swz("xyzw"[i] as "x"))
645
939
  }
646
940
 
647
941
  /** Distance to the nearest point of a jittered lattice. Cells, cracks, scales. */
@@ -696,7 +990,14 @@ export const uniform = {
696
990
  }
697
991
 
698
992
  export interface Texture {
993
+ /** The colour at `uv`, filtered as the texture's own settings say. */
699
994
  sample(uv: Vec2): Vec4
995
+ /**
996
+ * The colour at `uv` from mip level `lod`: 0 is the full size texture, 1
997
+ * half, and a fraction blends two levels where the texture's filter does.
998
+ * A texture with no mips has only level 0.
999
+ */
1000
+ sampleLevel(uv: Vec2, lod: Num): Vec4
700
1001
  }
701
1002
 
702
1003
  export function texture(name: string): Texture {
@@ -707,6 +1008,15 @@ export function texture(name: string): Texture {
707
1008
  if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
708
1009
  return mk(b, b.call(SLOP.SAMPLE, TYPE.VEC4, [uv.ref], [slot]), TYPE.VEC4)
709
1010
  },
1011
+ sampleLevel(uv: Vec2, lod: Num): Vec4 {
1012
+ if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
1013
+ const level = typeof lod === "number" ? b.constant([lod]) : lod
1014
+ if (typeof level !== "number") {
1015
+ if (level.owner !== b) throw new SLError("a value from another program cannot be used in this one")
1016
+ if (level.width !== 1) throw new SLError(`a mip level is one number, and this is a ${widthName(level.width)}`)
1017
+ }
1018
+ return mk(b, b.call(SLOP.SAMPLE_LOD, TYPE.VEC4, [uv.ref, typeof level === "number" ? level : level.ref], [slot]), TYPE.VEC4)
1019
+ },
710
1020
  }
711
1021
  }
712
1022