@solidrt/core 0.0.42 → 0.0.43

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/AGENTS.md CHANGED
@@ -24,6 +24,15 @@ hardcode desktop pixels.
24
24
  (<600), `medium` (600-840), `expanded` (>=840). Drive column counts / layout
25
25
  switches off it (see the responsive-grid example).
26
26
 
27
+ Exception - fixed-aspect content. For content with fixed internal geometry
28
+ (diagrams, slides, dashboards, games, emulators), do not branch on window size
29
+ at all: author everything in one design space and let `viewBox` fit it.
30
+ `<view flex={1} viewBox={[1280, 800]}>` uniformly scales and centers the
31
+ children (letterboxed), pointer events on them arrive in design coordinates,
32
+ and the same code runs unchanged from a desktop window to a phone. Reach for
33
+ `windowSizeClass` branching only when the layout genuinely reflows across form
34
+ factors.
35
+
27
36
  `env` and `capabilities` (both exported from `@solidrt/core`) are the two
28
37
  objects that expose this. They are plain objects with REACTIVE GETTERS, not
29
38
  functions - read them as `capabilities.windowSizeClass`, `env.displayScale`
@@ -104,10 +113,11 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
104
113
  Outlines: `drawStyle="stroke"` (or "stroke-and-fill") plus `strokeWidth`.
105
114
  Corner radius on draw primitives: `radius` (number or [tl, tr, br, bl]).
106
115
 
107
- - Registered JSX intrinsics: `window`, `view`, `text`, `rect`, `oval`, `path`,
108
- `texture`, `audio`, plus the `d-` variants `d-view`, `d-rect`, `d-oval`,
109
- `d-path`, `d-texture`, `d-text`. NOTE: `<line>` has a LineProps type but is
110
- NOT a registered intrinsic - it will not typecheck.
116
+ - Registered JSX intrinsics: `window`, `view`, `text`, `rect`, `oval`, `line`,
117
+ `path`, `texture`, `audio`, plus the `d-` variants `d-view`, `d-rect`,
118
+ `d-oval`, `d-line`, `d-path`, `d-texture`, `d-text`. Line endpoints
119
+ (`x1`/`y1`/`x2`/`y2`) exist only on `d-line`; a laid-out `<line>` has no
120
+ endpoint props and spans its layout box corner to corner.
111
121
 
112
122
  - Plain vs `d-` variant (the `d-` prefix means "detached" - detached from the
113
123
  layout engine, Taffy): a plain element (e.g. `rect`) is `RectProps &
@@ -119,6 +129,13 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
119
129
  directly-positioned, often-animating elements (e.g. hundreds of balls), `d-`
120
130
  skips the per-element layout that plain elements would incur.
121
131
 
132
+ - Transform origin on a `d-view`: with `originX`/`originY` unset, scale/rotate
133
+ pivot at the view's local (0,0) - the origin its children's coordinates are
134
+ authored against - not at a box center (a laid-out view pivots at its own
135
+ box center; a d-view has no box). To scale a detached group around its
136
+ content's center, set the origin explicitly in pixels, e.g.
137
+ `originX={100} originY={50}` for content drawn in a 200x100 local space.
138
+
122
139
  - Layout-affecting vs not (this matters for per-frame work). Props fall in three
123
140
  buckets, split by where they take effect:
124
141
  - `LayoutProps` - width/height, min/max sizes, margin, padding, `position` and
@@ -137,6 +154,11 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
137
154
  `position:absolute` at `left:0,top:0`, or just let normal flow place it) and
138
155
  then translate it with `x`/`y`.
139
156
 
157
+ - JSX text children collapse whitespace (ordinary JSX semantics): runs of
158
+ spaces become one, so space-padding a mono label collapses silently. An
159
+ expression container preserves it - `<d-text>{"one two"}</d-text>` - and
160
+ `\n` inside one produces a hard line break.
161
+
140
162
  - Events: there is NO `onClick`/`onPress`. A "button" is a `<view>`/`<rect>`
141
163
  with `onPointerDown`. Handlers: onPointerDown/Up/Move/Enter/Leave, onWheel,
142
164
  onKeyDown/Up, onTextInput, onFocus/onBlur. Text entry: focus a node with an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "colord": "^2.9.3"
28
28
  },
29
29
  "devDependencies": {
30
- "@solidrt/flux-types": "0.0.42"
30
+ "@solidrt/flux-types": "0.0.43"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@solidjs/signals": "2.0.0-beta.26",
package/src/gpu.ts CHANGED
@@ -60,6 +60,13 @@ export type CreateOptions = { manual?: boolean; label?: string }
60
60
  export type SamplerOptions = { filter?: gpu.FilterMode; wrap?: gpu.WrapMode }
61
61
  export type { FilterMode, WrapMode } from "flux:gpu"
62
62
 
63
+ // Pixel format option for the pixel-upload creates (createTexture,
64
+ // createMutableTexture), fixed for the id's lifetime like the sampler state.
65
+ // "rgba8" (default) or "r8" - see TextureFormat in flux:gpu for the r8
66
+ // contract (1 byte/pixel, sampled as `(v, 0, 0, 1)`, any width).
67
+ export type TextureFormatOptions = { format?: gpu.TextureFormat }
68
+ export type { TextureFormat } from "flux:gpu"
69
+
63
70
  // The branded id types, one per id space (see flux:gpu): plain numbers at
64
71
  // runtime, distinct types to the checker, so a cross-space slip like
65
72
  // destroyBuffer(textureId) fails to compile. Exported so apps can annotate
@@ -166,21 +173,22 @@ export let glsl = String.raw
166
173
  export { captureSnapshot, readTexture } from "flux:gpu"
167
174
 
168
175
  /**
169
- * Uploads raw RGBA8 pixels to an immutable GPU texture and returns its id (use
170
- * it as `<texture src={id} />`). `data` must be exactly `width * height * 4`
171
- * bytes; a mismatch throws. For pixels you intend to mutate and re-upload, use
172
- * `createMutableTexture` instead. When called inside a reactive scope the
173
- * texture is freed automatically once that owner is disposed; when called
174
- * outside one (e.g. after an `await`, where the owner is no longer current)
175
- * nothing is registered and you must call `destroyTexture` (from flux:gpu)
176
- * yourself. Pass `{ manual: true }` to skip the auto-free and own the
177
- * disposal yourself even inside a reactive scope.
176
+ * Uploads raw pixels to an immutable GPU texture and returns its id (use it
177
+ * as `<texture src={id} />`). `data` must be exactly `width * height` pixels
178
+ * at the declared format's size (`* 4` bytes for the default "rgba8", `* 1`
179
+ * for "r8"); a mismatch throws. For pixels you intend to mutate and
180
+ * re-upload, use `createMutableTexture` instead. When called inside a
181
+ * reactive scope the texture is freed automatically once that owner is
182
+ * disposed; when called outside one (e.g. after an `await`, where the owner
183
+ * is no longer current) nothing is registered and you must call
184
+ * `destroyTexture` (from flux:gpu) yourself. Pass `{ manual: true }` to skip
185
+ * the auto-free and own the disposal yourself even inside a reactive scope.
178
186
  */
179
187
  export function createTexture(
180
188
  data: Uint8Array,
181
189
  width: number,
182
190
  height: number,
183
- opts?: CreateOptions & SamplerOptions,
191
+ opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
184
192
  ): gpu.TextureId {
185
193
  let id = gpu.createTexture(data, width, height, opts)
186
194
  if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
@@ -189,18 +197,19 @@ export function createTexture(
189
197
 
190
198
  /**
191
199
  * Creates a GPU texture you intend to update over time: seed it with `data`,
192
- * then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels. `data`
193
- * is RGBA8 and must hold at least `width * height * 4` bytes (it may hold several
194
- * frames). Like `createTexture`, the texture is freed automatically when the
195
- * reactive owner is disposed (opt out with `{ manual: true }`); created
196
- * outside a reactive scope you must call `destroyTexture` (from flux:gpu)
197
- * yourself.
200
+ * then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels.
201
+ * `data` must hold at least `width * height` pixels at the declared format's
202
+ * size (`* 4` bytes for the default "rgba8", `* 1` for "r8"; it may hold
203
+ * several frames). Like `createTexture`, the texture is freed automatically
204
+ * when the reactive owner is disposed (opt out with `{ manual: true }`);
205
+ * created outside a reactive scope you must call `destroyTexture` (from
206
+ * flux:gpu) yourself.
198
207
  */
199
208
  export function createMutableTexture(
200
209
  data: Uint8Array,
201
210
  width: number,
202
211
  height: number,
203
- opts?: CreateOptions & SamplerOptions,
212
+ opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
204
213
  ): gpu.TextureId {
205
214
  let id = gpu.createMutableTexture(data, width, height, opts)
206
215
  if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
@@ -237,7 +246,9 @@ export function createMutableTexture(
237
246
  * as written, so a shader carrying its own uniform names - one ported from
238
247
  * elsewhere - runs unchanged here without dropping to compileShader /
239
248
  * linkProgram. The built-in vertex stage still supplies `vUV`; declare
240
- * `in vec2 vUV;` yourself to read it.
249
+ * `in vec2 vUV;` yourself to read it. One naming trap: GLSL ES reserves
250
+ * `packed` as a keyword, so `vec4 packed = texture(...)` fails with a syntax
251
+ * error that does not name the identifier - pick another name.
241
252
  */
242
253
  export function createShaderTexture(
243
254
  fragmentSrc: string,
package/src/sound.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // a large track from a file path on demand instead of decoding it into memory.
6
6
  //
7
7
  // The imperative primitive lives in the `flux:audio` module; import
8
- // { play, load, stream } from "flux:audio" for non-reactive use.
8
+ // { play, load, loadPcm, stream } from "flux:audio" for non-reactive use.
9
9
 
10
10
  import { createSignal, onCleanup } from "@solidjs/signals"
11
11
  import { load, stream } from "flux:audio"
@@ -13,13 +13,19 @@ import { file } from "flux:fs"
13
13
 
14
14
  type FluxFile = ReturnType<typeof file>
15
15
 
16
- type LoadedSound = ReturnType<typeof load>
16
+ type Clip = ReturnType<typeof load>
17
+ type Playback = ReturnType<Clip["play"]>
17
18
 
18
19
  export type SoundOptions = {
19
20
  /** Repeat the clip until stopped. Defaults to false. */
20
21
  loop?: boolean
21
22
  /** Volume scale, 1.0 leaves the clip unchanged. Defaults to 1.0. */
22
23
  gain?: number
24
+ /**
25
+ * Stereo position in [-1, 1], -1 = left, 0 = center, 1 = right (equal-power).
26
+ * Omitted means unspatialized.
27
+ */
28
+ pan?: number
23
29
  /**
24
30
  * Let play() stack overlapping voices instead of restarting. Defaults to
25
31
  * true: rapid triggers overlap. Set false for a single-voice sound where each
@@ -34,6 +40,8 @@ export type SoundStreamOptions = {
34
40
  loop?: boolean
35
41
  /** Volume scale, 1.0 leaves the track unchanged. Defaults to 1.0. */
36
42
  gain?: number
43
+ /** Stereo position in [-1, 1] (see {@link SoundOptions.pan}). */
44
+ pan?: number
37
45
  }
38
46
 
39
47
  /** A decoded sound with reactive lifecycle. */
@@ -42,30 +50,45 @@ export type Sound = {
42
50
  play(): void
43
51
  /** Stop every voice started from this sound. */
44
52
  stop(): void
53
+ /** Set the volume of every live voice, and of voices started later. */
54
+ setGain(gain: number): void
55
+ /** Set the stereo position of every live voice, and of voices started later. */
56
+ setPan(pan: number): void
45
57
  /** True after play() until stop() (does not track natural completion). */
46
58
  playing(): boolean
47
59
  /** Set if loading failed. */
48
60
  error(): Error | undefined
49
61
  }
50
62
 
51
- // Shared reactive wrapper: owns the loaded handle, tracks live voices, and
63
+ // Shared reactive wrapper: owns the loaded clip, tracks live voices, and
52
64
  // disposes both on cleanup. `loader` runs once (may throw -> error signal).
65
+ // Gain and pan are remembered so later voices start where setGain/setPan left
66
+ // the sound, not back at the initial options.
53
67
  function reactiveSound(
54
- loader: () => LoadedSound,
68
+ loader: () => Clip,
55
69
  overlap: boolean,
56
- playOptions: { loop?: boolean; gain?: number },
70
+ initial: { loop?: boolean; gain?: number; pan?: number },
57
71
  ): Sound {
58
72
  let [error, setError] = createSignal<Error | undefined>(undefined, { ownedWrite: true })
59
73
  let [playing, setPlaying] = createSignal(false, { ownedWrite: true })
60
74
 
61
- let handle: LoadedSound | undefined
62
- let voices: { stop(): void }[] = []
75
+ let clip: Clip | undefined
76
+ let voices: Playback[] = []
77
+ let loop = initial.loop
78
+ let gain = initial.gain
79
+ let pan = initial.pan
63
80
  try {
64
- handle = loader()
81
+ clip = loader()
65
82
  } catch (e) {
66
83
  setError(e instanceof Error ? e : new Error(String(e)))
67
84
  }
68
85
 
86
+ // Voices that finished on their own keep a dead handle in `voices` until the
87
+ // next call here; ended() lets each touch point clear them out.
88
+ let prune = () => {
89
+ voices = voices.filter((v) => !v.ended())
90
+ }
91
+
69
92
  let stopAll = () => {
70
93
  for (let v of voices) v.stop()
71
94
  voices = []
@@ -74,20 +97,31 @@ function reactiveSound(
74
97
 
75
98
  onCleanup(() => {
76
99
  stopAll()
77
- if (handle) {
78
- handle.unload()
79
- handle = undefined
100
+ if (clip) {
101
+ clip.unload()
102
+ clip = undefined
80
103
  }
81
104
  })
82
105
 
83
106
  return {
84
107
  play() {
85
- if (!handle) return
86
- if (!overlap) stopAll()
87
- voices.push(handle.play(playOptions))
108
+ if (!clip) return
109
+ if (overlap) prune()
110
+ else stopAll()
111
+ voices.push(clip.play({ loop, gain, pan }))
88
112
  setPlaying(true)
89
113
  },
90
114
  stop: stopAll,
115
+ setGain(value) {
116
+ gain = value
117
+ prune()
118
+ for (let v of voices) v.setGain(value)
119
+ },
120
+ setPan(value) {
121
+ pan = value
122
+ prune()
123
+ for (let v of voices) v.setPan(value)
124
+ },
91
125
  playing,
92
126
  error,
93
127
  }
@@ -102,6 +136,7 @@ export function createSound(source: Uint8Array, options: SoundOptions = {}): Sou
102
136
  return reactiveSound(() => load(source), options.overlap ?? true, {
103
137
  loop: options.loop,
104
138
  gain: options.gain,
139
+ pan: options.pan,
105
140
  })
106
141
  }
107
142
 
@@ -115,5 +150,5 @@ export function createSound(source: Uint8Array, options: SoundOptions = {}): Sou
115
150
  */
116
151
  export function createSoundStream(source: string | FluxFile, options: SoundStreamOptions = {}): Sound {
117
152
  let src = typeof source === "string" ? file(source) : source
118
- return reactiveSound(() => stream(src), false, { loop: options.loop, gain: options.gain })
153
+ return reactiveSound(() => stream(src), false, { loop: options.loop, gain: options.gain, pan: options.pan })
119
154
  }
package/src/types.d.ts CHANGED
@@ -157,7 +157,12 @@ export type Pct = { readonly __unit: "pct"; v: number }
157
157
  // One axis of the transform origin (the point rotate/scale/3D pivot around),
158
158
  // split per axis to match the engine's x/y prop convention. A bare number is
159
159
  // pixels; `pct(50)` is a fraction of the box, so a percentage origin tracks the
160
- // layout size with no reactive wiring. Unset defaults to the axis center.
160
+ // layout size with no reactive wiring. Unset defaults to the axis center on a
161
+ // laid-out view; on a d-view (no box of its own) it defaults to the view's
162
+ // local (0,0) - the origin its children's coordinates are authored against, so
163
+ // the pivot never depends on the inherited box. To pivot a d-view around its
164
+ // content's center, set the origin explicitly in pixels; pct()/keywords on a
165
+ // d-view resolve against the inherited box, which is rarely what you want.
161
166
  type OriginX = number | Pct | "left" | "center" | "right"
162
167
  type OriginY = number | Pct | "top" | "center" | "bottom"
163
168
 
@@ -185,7 +190,11 @@ export interface TransformProps {
185
190
  originX?: OriginX
186
191
  originY?: OriginY
187
192
  // Group opacity in 0..1: children are composited together, then faded as a
188
- // whole (CSS `opacity`). Does not affect hit testing.
193
+ // whole (CSS `opacity`). Does not affect hit testing. Costs a compositing
194
+ // layer (save_layer around the subtree) while below 1, except on a
195
+ // repaintBoundary view, where it is hoisted to composite time for free. To
196
+ // fade a single primitive, put the alpha in its `color` (rgba) instead -
197
+ // paint alpha costs nothing.
189
198
  opacity?: number
190
199
  scrollX?: number
191
200
  scrollY?: number
@@ -458,6 +467,13 @@ export interface RectProps extends PaintProps, PointerProps {
458
467
  // Strokes paint inside the box, same as `RectProps`.
459
468
  export interface OvalProps extends PaintProps, PointerProps {}
460
469
 
470
+ // A line's geometry is numbers, not a path string: the segment primitive to
471
+ // reach for when endpoints move (each endpoint is one property write; a path
472
+ // animates by rebuilding its `d` string). Endpoints (x1/y1/x2/y2) exist on
473
+ // the detached `d-line` only. A laid-out `<line>` is practically a rule -
474
+ // give it a thin box (length x strokeWidth); in general it draws its layout
475
+ // box's top-left-to-bottom-right diagonal. For arbitrary angles and
476
+ // connectors use `d-line`; for polylines and curves, a path.
461
477
  export interface LineProps extends PaintProps, PointerProps {
462
478
  /** Dash pattern in local units: the drawn segment length. Both onLength and offLength must be set to dash; with either unset the line is solid. */
463
479
  onLength?: number