@solidrt/core 0.0.55 → 0.0.57

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/input.ts ADDED
@@ -0,0 +1,12 @@
1
+ // The runtime-free half of the input surface, importable as
2
+ // `@solidrt/core/input` by modules that must stay headless (the camera
3
+ // controls and their checks run on the bare flux binary, which has no
4
+ // event bus). The map, the control-side axes contract and the keyboard
5
+ // device are pure; the gamepad device and the pointer feed need the
6
+ // runtime and live on the main entry.
7
+ export { createInputMap, invert, scale } from "./input-map"
8
+ export type { ActionKind, ActionValue, ActionsDecl, Binding, DeltaSink, GestureListener, InputMap, InputSource } from "./input-map"
9
+ export { createAxes, combineRates } from "./input-axes"
10
+ export type { Axes, AxesDecl, AxesHooks, AxisKind, AxisValue, Vec2 } from "./input-axes"
11
+ export { keyboard } from "./input-keyboard"
12
+ export type { KeyboardDevice, KeyboardVec2Keys } from "./input-keyboard"
package/src/pan.ts CHANGED
@@ -18,7 +18,13 @@ export interface PanOptions {
18
18
  */
19
19
  axis?: PanAxis
20
20
  onPanStart?: () => void
21
- /** Pointer movement since the previous event; positive dx is rightward, positive dy downward. */
21
+ /**
22
+ * Pointer movement since the previous event, in the handler node's PARENT
23
+ * frame (the frame its own x/y live in: window pixels under no scaling
24
+ * ancestor, design pixels under a designSize fit), so it applies 1:1
25
+ * whether the consumer moves the node itself or content inside it.
26
+ * Positive dx is rightward, positive dy downward.
27
+ */
22
28
  onPanMove?: (dx: number, dy: number) => void
23
29
  onPanEnd?: () => void
24
30
  }
@@ -35,8 +41,17 @@ export interface PanOptions {
35
41
  // cancel() is the external-cancel hook; it ends an active pan without
36
42
  // onPanEnd. Options are read at event time. Single-pointer by design; for
37
43
  // multi-pointer pinch/rotate compose createTransform instead.
44
+ //
45
+ // Frames: the slop is finger travel, so it is measured in window pixels
46
+ // (clientX/clientY); the deltas are measured in the node's parent frame
47
+ // (parentX/parentY), the frame the node's own coordinates live in, so a
48
+ // scaled ancestor (a designSize fit) is accounted for and both the drag
49
+ // idiom (move the node) and the content idiom (scroll what is inside it)
50
+ // apply the delta unchanged. The event carries both frames exactly, on
51
+ // and off the node.
38
52
  export function createPan(options: PanOptions) {
39
- // Down position while armed; last delivered position while active.
53
+ // Down position (window pixels) while armed; last delivered position
54
+ // (parent frame) while active.
40
55
  let origin: { x: number; y: number } | null = null
41
56
  let active: number | null = null
42
57
  let armed: number | null = null
@@ -78,7 +93,7 @@ export function createPan(options: PanOptions) {
78
93
  if (arena.steal(e.pointerId, owner)) {
79
94
  active = e.pointerId
80
95
  armed = null
81
- origin = { x: e.clientX, y: e.clientY }
96
+ origin = { x: e.parentX, y: e.parentY }
82
97
  options.onPanStart?.()
83
98
  } else {
84
99
  // The arena is resolved against us; the drag belongs elsewhere.
@@ -87,8 +102,8 @@ export function createPan(options: PanOptions) {
87
102
  return
88
103
  }
89
104
  if (active === e.pointerId && origin) {
90
- options.onPanMove?.(e.clientX - origin.x, e.clientY - origin.y)
91
- origin = { x: e.clientX, y: e.clientY }
105
+ options.onPanMove?.(e.parentX - origin.x, e.parentY - origin.y)
106
+ origin = { x: e.parentX, y: e.parentY }
92
107
  }
93
108
  },
94
109
  onPointerUp: (e: PointerEvent) => {
@@ -66,8 +66,8 @@ declare module "srt:events" {
66
66
  // The running application's own surface (lattice), present in every build.
67
67
  declare module "srt:app" {
68
68
  /**
69
- * Leave the current app, unconditionally: back to the launcher in a dev
70
- * client, quit when standalone or at the launcher root (on Android the
69
+ * Leave the current app, unconditionally: back to the player in a dev
70
+ * client, quit when standalone or at the player root (on Android the
71
71
  * client backgrounds instead of dying). The default action of an
72
72
  * unprevented `back` event; prefer the @solidrt/core re-export.
73
73
  */
@@ -81,7 +81,7 @@ declare module "srt:dev" {
81
81
  export const canDiscover: boolean
82
82
  export const recents: string[]
83
83
  /**
84
- * The dev-server address the client was launched with (so the launcher can
84
+ * The dev-server address the client was launched with (so the player can
85
85
  * auto-connect without on-device interaction), or null when launched without
86
86
  * one.
87
87
  */
@@ -101,7 +101,7 @@ declare module "srt:dev" {
101
101
  export function registerDebug(name: string, fn: (args?: any) => unknown): void
102
102
  }
103
103
 
104
- // Installed-app management (lattice), the launcher's surface over the client's
104
+ // Installed-app management (lattice), the player's surface over the client's
105
105
  // version store. Present only in go/dev client builds; elsewhere `available`
106
106
  // is false, `list` returns [], and launch/remove are no-ops.
107
107
  declare module "srt:apps" {
@@ -166,7 +166,7 @@ declare module "srt:apps" {
166
166
  export function info(id: string): AppInfo
167
167
  /**
168
168
  * Boot the app's installed current version, replacing the running app (the
169
- * launcher). Throws when the app is not installed. Custom fonts of the
169
+ * player). Throws when the app is not installed. Custom fonts of the
170
170
  * launched app register at client startup only, not mid-session.
171
171
  */
172
172
  export function launch(id: string): void
@@ -182,8 +182,8 @@ declare module "srt:apps" {
182
182
  */
183
183
  export function clearCache(id: string): void
184
184
  /**
185
- * Build identity of this runtime, for the launcher's settings screen. Not
186
- * app-specific, but surfaced here since the launcher already imports this
185
+ * Build identity of this runtime, for the player's settings screen. Not
186
+ * app-specific, but surfaced here since the player already imports this
187
187
  * module. `version` is the release version (git describe; "0.0.0-dev" in a
188
188
  * plain build), `profile` is "debug" or "release", `platform` is the OS
189
189
  * (std::env::consts::OS, e.g. "linux", "android", "windows", "macos").
package/src/text-input.ts CHANGED
@@ -214,6 +214,9 @@ export type EditorLine = {
214
214
  /** The caret's box in content coordinates (before scroll). */
215
215
  export type CaretRect = { x: number; y: number; height: number }
216
216
 
217
+ /** One line's slice of a selection highlight, in content coordinates (before scroll). */
218
+ export type SelectionRect = { x: number; y: number; width: number; height: number }
219
+
217
220
  export type TextEditorLayout = {
218
221
  lines(): EditorLine[]
219
222
  caret(): CaretRect
@@ -221,6 +224,8 @@ export type TextEditorLayout = {
221
224
  caretLine(): number
222
225
  /** The caret position (grapheme boundary) on line `line` nearest to content x. */
223
226
  offsetAtX(line: number, x: number): number
227
+ /** Highlight boxes for the anchor..focus range, one per touched line; empty when collapsed. */
228
+ selectionRects(anchor: number, focus: number): SelectionRect[]
224
229
  /** Index of the line at content y (clamped to the first/last line). */
225
230
  lineAtY(y: number): number
226
231
  /** The caret position one grapheme left/right of `offset`; a break sequence is one step. For createTextBuffer's `step`. */
@@ -322,18 +327,22 @@ export function createTextEditorLayout(
322
327
  }
323
328
  let caretLine = createMemo(() => lineOf(input().caret))
324
329
 
325
- // The caret sits at the last stop at or before its offset (an offset inside
326
- // a grapheme, e.g. from a controlled value, snaps back).
327
- let caret = createMemo((): CaretRect => {
328
- let offset = input().caret
329
- let index = caretLine()
330
- let line = lines()[index]!
330
+ // The x of the last stop at or before `offset` on line `index` (an offset
331
+ // inside a grapheme, e.g. from a controlled value, snaps back).
332
+ let xAt = (index: number, offset: number): number => {
331
333
  let x = 0
332
334
  for (let stop of lineStops(index)) {
333
335
  if (stop.offset > offset) break
334
336
  x = stop.x
335
337
  }
336
- return { x, y: line.y, height: line.height }
338
+ return x
339
+ }
340
+
341
+ let caret = createMemo((): CaretRect => {
342
+ let offset = input().caret
343
+ let index = caretLine()
344
+ let line = lines()[index]!
345
+ return { x: xAt(index, offset), y: line.y, height: line.height }
337
346
  })
338
347
 
339
348
  // Only positions that show on this line are candidates: a boundary offset
@@ -352,6 +361,32 @@ export function createTextEditorLayout(
352
361
  return best
353
362
  }
354
363
 
364
+ // One box per line the range touches, offsets snapped to stops like the
365
+ // caret. A line the selection continues past (every one but the last)
366
+ // highlights to its ink end plus a space width, so the selected break - a
367
+ // newline, or the wrap the range crosses - is visible, and a selected empty
368
+ // line shows as that sliver alone. The last line ends at the focus stop; a
369
+ // range ending on a wrap boundary owns a zero-width box there (the boundary
370
+ // offset displays on the next line) and drops it.
371
+ let selectionRects = (anchor: number, focus: number): SelectionRect[] => {
372
+ let start = Math.min(anchor, focus)
373
+ let end = Math.max(anchor, focus)
374
+ if (start >= end) return []
375
+ let ls = lines()
376
+ let first = lineOf(start)
377
+ let last = lineOf(end)
378
+ let breakWidth = measureText(" ", input().font).width
379
+ let out: SelectionRect[] = []
380
+ for (let i = first; i <= last; i++) {
381
+ let line = ls[i]!
382
+ let x = i === first ? xAt(i, start) : 0
383
+ let width = i === last ? xAt(i, end) - x : Math.max(line.width - x, 0) + breakWidth
384
+ if (width <= 0) continue
385
+ out.push({ x, y: line.y, width, height: line.height })
386
+ }
387
+ return out
388
+ }
389
+
355
390
  let lineAtY = (y: number): number => {
356
391
  let ls = lines()
357
392
  let index = 0
@@ -399,7 +434,7 @@ export function createTextEditorLayout(
399
434
  setScrollY(follow(scrollY(), c.y, c.height, vh, contentHeight))
400
435
  })
401
436
 
402
- return { lines, caret, caretLine, offsetAtX, lineAtY, step, scrollX, scrollY }
437
+ return { lines, caret, caretLine, offsetAtX, selectionRects, lineAtY, step, scrollX, scrollY }
403
438
  }
404
439
 
405
440
  // Wrap units wider than `width` (through their glued pieces) split into one
package/src/transform.ts CHANGED
@@ -26,14 +26,18 @@ const ENGAGE = 0.6
26
26
  const QUIET_SMOOTH = 0.19
27
27
 
28
28
  export type TransformDelta = {
29
- /** Focal-point movement since the previous frame, logical px. */
29
+ /** Focal-point movement since the previous frame, in the handler node's
30
+ * PARENT frame (the frame its own x/y live in: window pixels under no
31
+ * scaling ancestor, design pixels under a designSize fit), so it applies
32
+ * 1:1 to the node or to content inside it. */
30
33
  dx: number
31
34
  dy: number
32
35
  /** Multiplicative span change since the previous frame (1 = unchanged, >1 = fingers spreading). */
33
36
  scale: number
34
37
  /** Rotation of the pointer pair since the previous frame, radians (0 with a single pointer). */
35
38
  rotation: number
36
- /** Current focal point in window coordinates (the clientX/clientY frame) - the zoom-about anchor. */
39
+ /** Current focal point in the handler node's LOCAL frame (its own
40
+ * pixels) - the zoom-about anchor for content inside it. */
37
41
  x: number
38
42
  y: number
39
43
  /** How many pointers are down right now. Consumers that give one- and
@@ -106,12 +110,25 @@ export interface TransformOptions {
106
110
  // path, so an active transform survives leaving the node or the window.
107
111
  // cancel() is the external-cancel hook; it ends an active gesture without
108
112
  // onTransformEnd. Options are read at event time.
113
+ //
114
+ // Frames: the slop, the span filter and its rate gates are finger-travel
115
+ // thresholds, so they are measured in window pixels (clientX/clientY);
116
+ // the focal movement (dx/dy) and the pair angle are measured in the node's
117
+ // parent frame (parentX/parentY), the frame the node's own coordinates
118
+ // live in, so a scaled ancestor (a designSize fit) is accounted for and
119
+ // the delta applies unchanged to the node or to content inside it; the
120
+ // focal point (x/y) is reported in the node's local frame (localX/localY),
121
+ // the anchor a zoom-about-the-fingers needs. Scale and rotation are ratios
122
+ // and angles, the same in every uniformly scaled frame. The event carries
123
+ // all three frames exactly, on and off the node.
109
124
  export function createTransform(options: TransformOptions) {
125
+ type Tracked = { cx: number; cy: number; px: number; py: number; lx: number; ly: number }
126
+ let at = (e: PointerEvent): Tracked => ({ cx: e.clientX, cy: e.clientY, px: e.parentX, py: e.parentY, lx: e.localX, ly: e.localY })
110
127
  // All tracked pointers, in down order (the first two define the rotation pair).
111
- let pointers = new Map<number, { x: number; y: number }>()
128
+ let pointers = new Map<number, Tracked>()
112
129
  let active = false
113
130
  // Slop baseline while armed; last delivered configuration while active.
114
- let ref: { x: number; y: number; span: number; angle: number } | null = null
131
+ let ref: { cx: number; cy: number; px: number; py: number; span: number; angle: number } | null = null
115
132
  // The zoom gate and filter (see the zoom-noise note above): scale stays 1
116
133
  // until the SMOOTHED span leaves spanBase by SLOP, then streams smoothed
117
134
  // ratios for the rest of the gesture.
@@ -128,27 +145,41 @@ export function createTransform(options: TransformOptions) {
128
145
  // emits nothing.
129
146
  let rebase = false
130
147
 
148
+ // Centroids in all three frames, the span in window pixels (it feeds the
149
+ // finger-travel gates), the pair angle in the parent frame.
131
150
  let measure = () => {
132
151
  let n = pointers.size
133
- let x = 0
134
- let y = 0
152
+ let cx = 0
153
+ let cy = 0
154
+ let px = 0
155
+ let py = 0
156
+ let lx = 0
157
+ let ly = 0
135
158
  for (let p of pointers.values()) {
136
- x += p.x
137
- y += p.y
159
+ cx += p.cx
160
+ cy += p.cy
161
+ px += p.px
162
+ py += p.py
163
+ lx += p.lx
164
+ ly += p.ly
138
165
  }
139
- x /= n
140
- y /= n
166
+ cx /= n
167
+ cy /= n
168
+ px /= n
169
+ py /= n
170
+ lx /= n
171
+ ly /= n
141
172
  let span = 0
142
- for (let p of pointers.values()) span += Math.hypot(p.x - x, p.y - y)
173
+ for (let p of pointers.values()) span += Math.hypot(p.cx - cx, p.cy - cy)
143
174
  span /= n
144
175
  let angle = 0
145
176
  if (n >= 2) {
146
177
  let pair = pointers.values()
147
178
  let a = pair.next().value!
148
179
  let b = pair.next().value!
149
- angle = Math.atan2(b.y - a.y, b.x - a.x)
180
+ angle = Math.atan2(b.py - a.py, b.px - a.px)
150
181
  }
151
- return { x, y, span, angle }
182
+ return { cx, cy, px, py, lx, ly, span, angle }
152
183
  }
153
184
 
154
185
  let reset = () => {
@@ -202,10 +233,10 @@ export function createTransform(options: TransformOptions) {
202
233
  if (rotation > Math.PI) rotation -= 2 * Math.PI
203
234
  else if (rotation < -Math.PI) rotation += 2 * Math.PI
204
235
  }
205
- let dx = m.x - ref.x
206
- let dy = m.y - ref.y
236
+ let dx = m.px - ref.px
237
+ let dy = m.py - ref.py
207
238
  ref = m
208
- options.onTransformMove?.({ dx, dy, scale, rotation, x: m.x, y: m.y, pointers: pointers.size })
239
+ options.onTransformMove?.({ dx, dy, scale, rotation, x: m.lx, y: m.ly, pointers: pointers.size })
209
240
  }
210
241
 
211
242
  // An unmount mid-gesture must not leave resolved claims behind (or a live
@@ -226,24 +257,22 @@ export function createTransform(options: TransformOptions) {
226
257
  // A finger joining an established gesture belongs to it outright; if
227
258
  // the arena refuses (resolved elsewhere) the finger stays out.
228
259
  if (!arena.steal(e.pointerId, owner)) return
229
- pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
260
+ pointers.set(e.pointerId, at(e))
230
261
  rebase = true
231
262
  return
232
263
  }
233
- pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
264
+ pointers.set(e.pointerId, at(e))
234
265
  ref = measure()
235
266
  },
236
267
  onPointerMove: (e: PointerEvent) => {
237
- let p = pointers.get(e.pointerId)
238
- if (!p || !ref) return
239
- p.x = e.clientX
240
- p.y = e.clientY
268
+ if (!pointers.has(e.pointerId) || !ref) return
269
+ pointers.set(e.pointerId, at(e))
241
270
  if (!active) {
242
271
  // Arming runs per event: the slop test tolerates a mixed-age
243
272
  // measure (8px against <=1 frame of staleness), and the arena
244
273
  // steal must happen synchronously inside the dispatch walk.
245
274
  let m = measure()
246
- let travel = Math.hypot(m.x - ref.x, m.y - ref.y)
275
+ let travel = Math.hypot(m.cx - ref.cx, m.cy - ref.cy)
247
276
  if (travel < SLOP && Math.abs(m.span - ref.span) < SLOP) return
248
277
  let taken: number[] = []
249
278
  let refused = false
package/src/types.d.ts CHANGED
@@ -175,6 +175,48 @@ export interface PaintProps {
175
175
  strokeWidth?: number
176
176
  }
177
177
 
178
+ /**
179
+ * A drop shadow behind a shape (CSS box-shadow field semantics, one shadow):
180
+ * the shape's outer geometry, offset by x/y, grown by `spread`, softened by
181
+ * `blur` (a CSS-style radius in logical px), painted in `color` under the
182
+ * shape. It casts from the shape's outer box whatever the draw style (strokes
183
+ * paint inside, so the box is the outer edge) - except on `path`, where the
184
+ * shadow mirrors the element's own fill/stroke silhouette and `spread` is
185
+ * rejected (an arbitrary path cannot be inflated exactly). `color` is
186
+ * required; there is no currentColor to inherit.
187
+ */
188
+ export interface ShadowProps {
189
+ x?: number
190
+ y?: number
191
+ blur?: number
192
+ spread?: number
193
+ color: Color
194
+ }
195
+
196
+ /**
197
+ * A subtree filter (the CSS filter functions, object form): the view's
198
+ * children are composited together, run through the set color operations -
199
+ * fused into one color matrix, applied in the fixed order grayscale, sepia,
200
+ * saturate, hueRotate, brightness, contrast, invert - and then blurred.
201
+ * `blur` is a CSS-style radius in logical px; `hueRotate` is radians (this
202
+ * API's angle convention); the rest are the CSS amounts (1 = unchanged for
203
+ * saturate/brightness/contrast, 0 = unchanged for the others; grayscale,
204
+ * sepia and invert saturate at 1). There is no `opacity` key - the view's
205
+ * own `opacity` prop is the same compositing layer. Like opacity, a filter
206
+ * on a non-boundary view costs a save_layer; on a repaintBoundary view it
207
+ * rides the composite for free. Hit testing is unaffected.
208
+ */
209
+ export interface FilterProps {
210
+ blur?: number
211
+ grayscale?: number
212
+ sepia?: number
213
+ saturate?: number
214
+ hueRotate?: number
215
+ brightness?: number
216
+ contrast?: number
217
+ invert?: number
218
+ }
219
+
178
220
  /** A percentage value, from `pct(50)`. Resolves against the element box. */
179
221
  export type Pct = { readonly __unit: "pct"; v: number }
180
222
 
@@ -548,9 +590,10 @@ export interface TransitionProps {
548
590
  * `all` covers every animatable property the element has, and a bare
549
591
  * string is shorthand for it: `transition="300ms ease-out"`. Only
550
592
  * properties the element carries animate (a d-rect has x, a view's x is
551
- * its transform); the initial value never animates unless the entry sets
552
- * `from` (an enter animation), and a non-numeric write (e.g. null)
553
- * cancels the running animation and snaps. `null` clears the
593
+ * its transform); the initial values (every write before the element's
594
+ * first paint) never animate unless the entry sets `from` (an enter
595
+ * animation), and a non-numeric write (e.g. null) cancels the running
596
+ * animation and snaps. `null` clears the
554
597
  * declaration; already-running animations finish. A spec built in a
555
598
  * conditional widens `curve` to `string` for TypeScript; write
556
599
  * `satisfies Transition` on the branch.
@@ -607,11 +650,13 @@ export interface WindowShaderProps {
607
650
  program: ProgramId
608
651
  /**
609
652
  * Uniforms filled by name, paced to the next real repaint. A number drives
610
- * a scalar (`float`/`int`); a flat number array drives the declared GLSL
611
- * type: 2/3/4 for `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
653
+ * a scalar (`float`/`int`); a flat number array (or a Float32Array /
654
+ * Float64Array) drives the declared GLSL type: 2/3/4 for
655
+ * `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`. An undefined or null
656
+ * entry is skipped.
612
657
  */
613
- params?: Record<string, number | number[]>
614
- /** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. */
658
+ params?: Record<string, number | number[] | Float32Array | Float64Array>
659
+ /** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. An undefined or null entry is skipped. */
615
660
  textures?: TextureBindings
616
661
  /** Vertices drawn (attributeless triangles). Default 3, the covering triangle. */
617
662
  vertexCount?: number
@@ -671,7 +716,9 @@ export interface ViewOwnProps extends TransformProps, PointerProps {
671
716
  * skipping rasterization entirely. Costs texture memory and re-rasterizes
672
717
  * on layout-size or display-scale changes. Content painted outside the
673
718
  * element's layout box is cropped, and ancestor scale animations smear the
674
- * bitmap; best for screen-aligned, static, raster-expensive content.
719
+ * bitmap; best for screen-aligned, static, raster-expensive content. On a
720
+ * d-view the box is the inherited frame (the nearest laid-out ancestor's
721
+ * box, or the design size under a designSize view), same as a capture.
675
722
  *
676
723
  * "snapshot-no-aa" is "snapshot" rasterized without anti-aliasing: cheaper
677
724
  * (no multisampled scratch, one render pass), but vector content - svg
@@ -701,6 +748,19 @@ export interface ViewOwnProps extends TransformProps, PointerProps {
701
748
  * unrotated, opaque content.
702
749
  */
703
750
  shader?: ViewShaderProps | null
751
+ filter?: FilterProps
752
+ /**
753
+ * Filters the pixels already painted BENEATH the view's box, in place,
754
+ * before the view's own content draws - frosted glass over live content
755
+ * (CSS `backdrop-filter`). Same keys and semantics as `filter`. The
756
+ * region is the layout box; give the view an overflow clip with
757
+ * `clipRadius` for rounded glass. Costs an offscreen capture of the
758
+ * pixels beneath at that point in the frame, so treat it as a deliberate
759
+ * panel, not a casual style. Inside a `repaintBoundary="snapshot"`
760
+ * subtree it sees only that boundary's own offscreen content, never what
761
+ * is behind the boundary (the same containment `blendMode` has there).
762
+ */
763
+ backdropFilter?: FilterProps
704
764
  }
705
765
 
706
766
  /**
@@ -716,11 +776,13 @@ export interface ViewShaderProps {
716
776
  program: ProgramId
717
777
  /**
718
778
  * Uniforms filled by name, paced to the next real repaint. A number drives
719
- * a scalar (`float`/`int`); a flat number array drives the declared GLSL
720
- * type: 2/3/4 for `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
779
+ * a scalar (`float`/`int`); a flat number array (or a Float32Array /
780
+ * Float64Array) drives the declared GLSL type: 2/3/4 for
781
+ * `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`. An undefined or null
782
+ * entry is skipped.
721
783
  */
722
- params?: Record<string, number | number[]>
723
- /** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. */
784
+ params?: Record<string, number | number[] | Float32Array | Float64Array>
785
+ /** Extra sampler2D inputs: uniform name to texture id, or `{ id, filter?, wrap? }` for a per-binding sampling override. An undefined or null entry is skipped. */
724
786
  textures?: TextureBindings
725
787
  /**
726
788
  * Transparent margin in logical px on every side of the layout box, for
@@ -761,10 +823,13 @@ export interface RectProps extends PaintProps, PointerProps, DashProps {
761
823
  // number applies to all four corners; an array is [top-left, top-right,
762
824
  // bottom-right, bottom-left] (CSS border-radius order).
763
825
  radius?: number | [number, number, number, number]
826
+ shadow?: ShadowProps
764
827
  }
765
828
 
766
829
  // Strokes paint inside the box, same as `RectProps`.
767
- export interface OvalProps extends PaintProps, PointerProps, DashProps {}
830
+ export interface OvalProps extends PaintProps, PointerProps, DashProps {
831
+ shadow?: ShadowProps
832
+ }
768
833
 
769
834
  /**
770
835
  * A stroke's dash pattern, on every stroked primitive (`rect`, `oval`,
@@ -847,6 +912,8 @@ export interface LineProps extends PaintProps, PointerProps, DashProps {
847
912
  export interface PathProps extends PaintProps, PointerProps, DashProps {
848
913
  d?: string
849
914
  fillRule?: "nonzero" | "evenodd"
915
+ /** Shadows the drawn silhouette (fill and/or stroke); `spread` is rejected here. */
916
+ shadow?: ShadowProps
850
917
  }
851
918
 
852
919
  export type FontFamily = "sans" | "serif" | "mono" | (string & {})
@@ -957,7 +1024,8 @@ export interface TextureProps extends PaintProps, PointerProps {
957
1024
  // throws, and set src before params: a write with no src to route to
958
1025
  // throws). However often a signal writes, the target renders once per
959
1026
  // frame at the raster flush. A number drives a scalar (`float`/`int`); a
960
- // flat number array drives the declared GLSL type: 2/3/4 for
961
- // `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
962
- params?: Record<string, number | number[]>
1027
+ // flat number array (or a Float32Array / Float64Array) drives the declared
1028
+ // GLSL type: 2/3/4 for `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
1029
+ // An undefined or null entry is skipped.
1030
+ params?: Record<string, number | number[] | Float32Array | Float64Array>
963
1031
  }