@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/AGENTS.md +150 -20
- package/agents/painting.md +29 -12
- package/agents/performance.md +14 -3
- package/docs/reference/detached.md +8 -5
- package/docs/reference/drawing.md +1 -1
- package/docs/reference/elements.md +3 -1
- package/docs/reference/gpu.md +12 -1
- package/docs/reference/layout.md +28 -0
- package/examples/README.md +3 -0
- package/examples/detached-positioning.tsx +3 -1
- package/examples/gamepad.tsx +94 -0
- package/examples/view-design-size.tsx +5 -0
- package/package.json +7 -5
- package/src/core.ts +15 -2
- package/src/environment.ts +87 -69
- package/src/gamepad.ts +10 -8
- package/src/gpu.ts +122 -18
- package/src/index.ts +14 -2
- package/src/input-axes.ts +180 -0
- package/src/input-gamepad-device.ts +161 -0
- package/src/input-gamepad.ts +24 -0
- package/src/input-keyboard.ts +156 -0
- package/src/input-map.ts +467 -0
- package/src/input-pointer.ts +213 -0
- package/src/input.ts +12 -0
- package/src/pan.ts +20 -5
- package/src/runtime-modules.d.ts +7 -7
- package/src/text-input.ts +43 -8
- package/src/transform.ts +52 -23
- package/src/types.d.ts +84 -16
- package/src/window.ts +54 -38
package/AGENTS.md
CHANGED
|
@@ -111,7 +111,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
|
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
114
|
-
2.0.0-rc.
|
|
114
|
+
2.0.0-rc.6); bun resolves them from peerDependencies. On a version bump,
|
|
115
115
|
`bun install` only warns ("incorrect peer dependency") and keeps the old
|
|
116
116
|
ones, and `bun update solid-js @solidjs/signals @solidjs/universal` does
|
|
117
117
|
not read the pin either: it adds all three to package.json at registry
|
|
@@ -132,6 +132,14 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
132
132
|
window (message, stack, a Reset button that retries the failed
|
|
133
133
|
computations) and logs `Uncaught error`. `<Errored>` gives a subtree its
|
|
134
134
|
own in-place fallback.
|
|
135
|
+
An `onFrame` callback is its own boundary, event-listener style: a throw
|
|
136
|
+
is caught and logged (`Error in onFrame callback`, repeating every
|
|
137
|
+
frame), the subscription stays, and THE REST OF THAT CALLBACK IS
|
|
138
|
+
ABANDONED - what it wrote before the throw lands, what came after never
|
|
139
|
+
does, frame after frame while the app keeps presenting. A partial frame
|
|
140
|
+
is therefore a live possibility: every sprite drawn after the throwing
|
|
141
|
+
line simply stops appearing, which reads on screen as a logic bug, not a
|
|
142
|
+
crash. Check the logs before reasoning about a rendering symptom.
|
|
135
143
|
|
|
136
144
|
- Two kinds of element:
|
|
137
145
|
- Containers - `<window>`, `<view>`. Do layout + transform + pointer events.
|
|
@@ -165,10 +173,19 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
165
173
|
LayoutProps`, so it draws AND is laid out by Taffy. The detached variant
|
|
166
174
|
(`d-rect`) is `RectProps` only - it draws but is NOT in the layout pass; you
|
|
167
175
|
place it yourself with `x`/`y` (omit them and it fills the parent, which is
|
|
168
|
-
how backgrounds work).
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
176
|
+
how backgrounds work). Laid-out elements are the DEFAULT for building a
|
|
177
|
+
screen: structure, cards, panels, labels - anything flex/gap/padding
|
|
178
|
+
should place - is `<view>`/`<text>` and plain primitives. Reach for `d-`
|
|
179
|
+
only with a concrete reason: a background/underlay, free-form drawn
|
|
180
|
+
geometry (a chart's marks, a diagram, vector art), an overlay pinned at
|
|
181
|
+
coordinates, or as a performance lever for many directly-positioned,
|
|
182
|
+
often-animating elements (e.g. hundreds of balls), where `d-` skips the
|
|
183
|
+
per-element layout plain elements would incur. Hand-positioning what flex
|
|
184
|
+
would place - a card built from d-rects and d-texts at computed offsets -
|
|
185
|
+
forfeits wrapping, sizing and resize behavior for nothing: inside a chart
|
|
186
|
+
the marks and axis labels are d-*, but the card around it is a `<view>`.
|
|
187
|
+
A `designSize` view is NOT a reason to go detached - laid-out children
|
|
188
|
+
resolve in design units too.
|
|
172
189
|
|
|
173
190
|
- Transform origin on a `d-view`: with `originX`/`originY` unset, scale/rotate
|
|
174
191
|
pivot at the view's local (0,0) - the origin its children's coordinates are
|
|
@@ -213,6 +230,22 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
213
230
|
`position:absolute` at `left:0,top:0`, or just let normal flow place it) and
|
|
214
231
|
then translate it with `x`/`y`.
|
|
215
232
|
|
|
233
|
+
- Measuring layout: never re-derive an element's position with your own
|
|
234
|
+
arithmetic - the layout engine already knows, so ask it.
|
|
235
|
+
`getBoundingBox(ref)` returns the node's laid-out box
|
|
236
|
+
({x, y, width, height}, transforms composed, relative to the nearest
|
|
237
|
+
`position="relative"` ancestor, else the window);
|
|
238
|
+
`getBoundingBoxViewport(ref)` is always window-relative (the space pointer
|
|
239
|
+
`clientX`/`clientY` report in). Both are snapshot reads of the last
|
|
240
|
+
computed layout, not reactive - read them in `onLayout(fn)` (fires after
|
|
241
|
+
layout, before paint) or in an event handler. The canonical shape: let
|
|
242
|
+
flex place the boxes, measure them in onLayout, and drive DETACHED drawing
|
|
243
|
+
from the result - a d-path connector between two cards, a badge pinned to
|
|
244
|
+
a bar. Two rules keep it one pass: write the measurement only to props
|
|
245
|
+
that do not affect layout (a `d` string, a d-* position, a transform), and
|
|
246
|
+
call `flush()` after the write so it lands in this frame's display list.
|
|
247
|
+
See examples/on-layout-connect.tsx.
|
|
248
|
+
|
|
216
249
|
- Text `lineHeight` is a MULTIPLIER of fontSize (1.3-1.6 is typical), not
|
|
217
250
|
pixels. A CSS-reflex value like 22 makes each line box 22x the font size:
|
|
218
251
|
the text becomes blank space and the parent balloons.
|
|
@@ -269,7 +302,12 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
269
302
|
- Gesture recognizers, shared by every package: `createPan` (single-pointer
|
|
270
303
|
drag, axis-aware slop, per-event dx/dy) and `createTransform` (merged
|
|
271
304
|
pan + pinch + rotate over the whole pointer set, Flutter-Scale style: streams
|
|
272
|
-
`{ dx, dy, scale, rotation, x, y, pointers }` once per FRAME
|
|
305
|
+
`{ dx, dy, scale, rotation, x, y, pointers }` once per FRAME. Frames, both
|
|
306
|
+
recognizers: slop and span gates are finger travel and measure in window
|
|
307
|
+
pixels; dx/dy are measured in the handler node's PARENT frame (the frame
|
|
308
|
+
its own x/y live in) so they apply 1:1 to the node or to content inside
|
|
309
|
+
it under any scaled ancestor (a designSize fit); the transform's focal
|
|
310
|
+
x/y is in the node's LOCAL frame, the zoom-about anchor. Positions
|
|
273
311
|
update per event, but the cross-pointer measure waits for the
|
|
274
312
|
`pointerFrame` batch terminator, when every pointer is the same age; one
|
|
275
313
|
finger degrades to a plain pan, and `pointers` is how a consumer gives one-
|
|
@@ -282,6 +320,87 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
282
320
|
rather than track pointers ad hoc, or they will double-handle against
|
|
283
321
|
scrollers and pressables.
|
|
284
322
|
|
|
323
|
+
- The input map (input-map.ts) is how anything drives a control:
|
|
324
|
+
ARCHITECTURE.md's rule is that a control (a camera, a character
|
|
325
|
+
controller) consumes a device-free abstraction and never handles events
|
|
326
|
+
or reads a device itself, and the map is the named, typed layer between
|
|
327
|
+
devices and consumers - Godot's InputMap and Unity's action maps in
|
|
328
|
+
reactive form. `createInputMap({ look: "vec2", move: "vec2", zoom:
|
|
329
|
+
"axis", jump: "button" })` declares the app's actions once; devices bind
|
|
330
|
+
to them by name (`input.bind("look", pointer.drag, gamepad(0).rightStick)`,
|
|
331
|
+
`input.bind("move", keyboard.wasd, gamepad(0).leftStick)`); consumers
|
|
332
|
+
read the action, never the device. Every axis/vec2 action carries TWO
|
|
333
|
+
channels: the RATE (the sum of every bound source's current value,
|
|
334
|
+
clamped - an axis to -1..1, a vec2 to unit length - reactive, sampled per
|
|
335
|
+
frame by the consumer and integrated over dt at its own speed) and the
|
|
336
|
+
DELTAS (immediate amounts a gesture produced, in device-free units: a
|
|
337
|
+
drag in element heights, a wheel notch or a pinch in octaves, a twist in
|
|
338
|
+
turns, with begin/end brackets around a finger's gesture and the focal
|
|
339
|
+
point as a fraction of the element). Values use the screen convention
|
|
340
|
+
throughout: x right, y down, a stick pushed up reads y = -1, and a
|
|
341
|
+
consumer that means "forward" by a vec2 reads -y. A button source may
|
|
342
|
+
bind to an axis action (1 while pressed); a vec2 source on an axis
|
|
343
|
+
action throws.
|
|
344
|
+
Devices are the nouns: `keyboard.key("Space")`, `keyboard.axis("KeyQ",
|
|
345
|
+
"KeyE")`, `keyboard.vec2({ up, down, left, right })`, `keyboard.wasd`,
|
|
346
|
+
`keyboard.arrows`, a spec with modifiers ahead of the key
|
|
347
|
+
("Shift+Tab", "Ctrl+KeyS": the most specific spec in a source wins a
|
|
348
|
+
down, an up releases on the bare key) (key events reach them through
|
|
349
|
+
`input.handlers`, spread on `<window>` for app-global keys or on the
|
|
350
|
+
leaf that should hold focus for them - keys bubble to the window, so a
|
|
351
|
+
TextInput's consumed keys never arrive); `gamepad(slot)`, `gamepad()`
|
|
352
|
+
for every pad, or `gamepad.next()` for the next unclaimed pad to press
|
|
353
|
+
any button (split screen seats players in pick-up order; its `slot`
|
|
354
|
+
resolves reactively and frees when the creating scope is disposed),
|
|
355
|
+
with `leftStick`, `rightStick`, `dpad`, `triggers` (right minus left),
|
|
356
|
+
`shoulders`, `axis(name)`, `button(name)`, reactive over gamepads() so a
|
|
357
|
+
stick wakes a control's frame loop; `createPointerFeed()` for the one
|
|
358
|
+
device that is not global - a pointer event belongs to the element
|
|
359
|
+
under it, so the feed is per element: spread `feed.handlers` on the
|
|
360
|
+
leaf (`<Scene pointer>`, `<View3d pointer>`, `<SpriteLayer pointer>` and
|
|
361
|
+
`<View2d pointer>` take a feed and spread it on their built-in leaf) and
|
|
362
|
+
bind its gestures `drag` (one pointer), `pan` (two), `pinch`, `twist`,
|
|
363
|
+
`wheel` and `mouseDelta` (raw motion while pointerLocked()). One
|
|
364
|
+
merged recognizer serves them, so they arbitrate in the arena as one. A
|
|
365
|
+
detached d-* leaf has no layout box: give the feed `{ layout }` or it
|
|
366
|
+
throws at the first press. `invert(source)` and `scale(source, k)` are
|
|
367
|
+
the two processors: keys and sticks move the CAMERA where a drag moves
|
|
368
|
+
the content, so a preset binds arrows or a stick to `pan`/`rotate`
|
|
369
|
+
through invert() and the action keeps one meaning.
|
|
370
|
+
Consumers with the axes contract (`createAxes({ rotate: "vec2", zoom:
|
|
371
|
+
"axis" }, { onNudge, onBegin, onEnd })`, what every camera control
|
|
372
|
+
exposes as `.axes`) are connected by name with `input.drive(control.axes,
|
|
373
|
+
names?)`: each axis takes the action of the same name, or the one
|
|
374
|
+
`names` gives it (null skips one), both channels; the camera components
|
|
375
|
+
take the map as their `input` prop and do exactly that. Injection by
|
|
376
|
+
name is the wire for scripts, debug commands, peers and replays:
|
|
377
|
+
`input.set("move", [0, -1])` holds a rate, `press`/`release` a button,
|
|
378
|
+
`nudge`/`begin`/`end` feed the delta channel. `bindings()` lists what is
|
|
379
|
+
bound (source labels are display strings), `unbind` removes one, and a
|
|
380
|
+
preset (`orbitBindings`, `firstPersonBindings` in @solidrt/3d,
|
|
381
|
+
`camera2dBindings` in @solidrt/2d) is a plain bindings list the app
|
|
382
|
+
applies and edits - nothing binds unless the app says so (the one
|
|
383
|
+
built-in default is the UI set inside components' createFocusNav, the
|
|
384
|
+
platform's own layer). The map is an instance: two pads on two views is
|
|
385
|
+
two maps. Contexts are a switch per action: `input.disable("move",
|
|
386
|
+
"jump")` / `enable(...)` (a set is a list of names, so
|
|
387
|
+
`disable(...Object.keys(gameActions))` when a menu opens); a disabled
|
|
388
|
+
action reads neutral, drops its deltas and closes the gesture it had
|
|
389
|
+
open, while its sources keep their state so a key still held when it
|
|
390
|
+
comes back reads at once; `enabled(name)` is reactive. Create maps and
|
|
391
|
+
feeds in an owned scope (a component body): onPress/onRelease run
|
|
392
|
+
effects under it and the feed's recognizer registers its cleanup there. The vocabulary the
|
|
393
|
+
controls share, one kind and unit per word: `pan` vec2 (view heights),
|
|
394
|
+
`zoom` axis (octaves, positive in), `rotate` vec2 (orbit turns), `roll`
|
|
395
|
+
axis (turns about the view axis), `look` vec2 (turns), `move` vec2
|
|
396
|
+
([right, forward], forward = -y), `rise` axis (up). The runtime-free
|
|
397
|
+
half (`createInputMap`, `createAxes`, `keyboard`, the processors) is
|
|
398
|
+
importable as `@solidrt/core/input` for headless checks;
|
|
399
|
+
checks/input-map-check.ts runs it on the bare flux binary, and
|
|
400
|
+
checks/input-gamepad-check.ts drives the gamepad device and the join
|
|
401
|
+
order over a signal of pad snapshots (input-gamepad-device.ts is
|
|
402
|
+
runtime-free; input-gamepad.ts hands it gamepads()).
|
|
403
|
+
|
|
285
404
|
- Reactivity is SolidJS 2.0 (`@solidjs/signals`), NOT Solid 1.x. `createSignal`
|
|
286
405
|
is as you expect, but `createEffect` takes the 2.0 two-function shape: a
|
|
287
406
|
TRACKED compute that reads signals and returns a value, then an UNTRACKED
|
|
@@ -292,13 +411,14 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
292
411
|
inside JSX, a `createMemo`, or an effect's compute phase) reads it
|
|
293
412
|
untracked: it silently freezes at the initial value.
|
|
294
413
|
Writing a signal or store from inside an owned scope (a component body, a
|
|
295
|
-
`createMemo`, an effect's compute phase) throws
|
|
296
|
-
`REACTIVE_WRITE_IN_OWNED_SCOPE` in dev; a loader
|
|
297
|
-
body
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
414
|
+
`createMemo`, an effect's compute phase, a `ref` callback) throws
|
|
415
|
+
`REACTIVE_WRITE_IN_OWNED_SCOPE` in dev; a loader kicked off in the
|
|
416
|
+
component body or a ref that reports progress through a signal is the
|
|
417
|
+
classic reflex that hits this. `untrack` does NOT exempt a write (the
|
|
418
|
+
guard is owner-based, and untrack only stops tracking). Move the write
|
|
419
|
+
into an event handler, an effect's apply phase, or `onSettled`; opt in
|
|
420
|
+
narrowly with `createSignal(v, { ownedWrite: true })` for a signal that
|
|
421
|
+
genuinely is internal state.
|
|
302
422
|
Signal writes flush on a microtask: a handler that sets a signal and
|
|
303
423
|
immediately reads it back gets the OLD value. Read it in an effect, or
|
|
304
424
|
call `flush()` (from @solidjs/signals) to force it through.
|
|
@@ -316,7 +436,11 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
316
436
|
TypeScript with no @solidrt imports, with the JSX reading it and the
|
|
317
437
|
per-frame hook doing one state step plus a handful of property writes.
|
|
318
438
|
That shape runs under bun with no window (tests, audits, replaying a whole
|
|
319
|
-
session in seconds) and it is what keeps per-frame work cheap.
|
|
439
|
+
session in seconds) and it is what keeps per-frame work cheap. For a game
|
|
440
|
+
it is the tuning loop itself: minutes of play run headless in a fraction
|
|
441
|
+
of a second, so a difficulty curve or a spawn table is tuned against
|
|
442
|
+
scripted policies in seconds instead of by playing, and the design bugs
|
|
443
|
+
that only show at scale surface there first.
|
|
320
444
|
- Animation is target-shaped first: declare `transition` on the element and
|
|
321
445
|
write targets, and the runtime animates natively with no per-frame JS.
|
|
322
446
|
Enter and exit are per-property entries on that same spec: `from` is the
|
|
@@ -328,9 +452,14 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
328
452
|
descendants declare `from`/`exit`. An enter plays once per mount; to
|
|
329
453
|
replay it, remount the subtree (`<Show when={epoch()} keyed>` around it,
|
|
330
454
|
then bump `epoch`). See examples/stagger.tsx.
|
|
331
|
-
|
|
455
|
+
Per-frame work is for genuinely procedural motion, and a game or a
|
|
456
|
+
simulation is that in full: its natural shape is ONE `onFrame` doing one
|
|
457
|
+
sim step and N property (or sprite) writes, which is not a smell but the
|
|
458
|
+
budget the runtime is built for - agents/performance.md rule 4 has the
|
|
459
|
+
rules, @solidrt/2d's notes the measured write costs at scale.
|
|
332
460
|
`onFrame((tick, frame, rate) => {})` is the native hook (runtime-paced,
|
|
333
|
-
returns a cleanup, auto-cleaned inside
|
|
461
|
+
returns a cleanup, auto-cleaned inside an owned scope; from an effect's
|
|
462
|
+
apply, return it so the loop runs only while the condition holds); `rate` is the
|
|
334
463
|
display's nominal refresh rate in Hz, which a fixed-timestep loop needs
|
|
335
464
|
(see @solidrt/cli agents/debugging.md on stepping); `requestAnimationFrame`
|
|
336
465
|
exists as a web-standard one-shot but is not the preferred driver. A JS
|
|
@@ -340,10 +469,11 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
340
469
|
Window state: onResize, onLayout, onWindowFocus, onWindowBlur exist, but
|
|
341
470
|
prefer the reactive reads (`env`/`capabilities` above, or the accessors
|
|
342
471
|
`windowSize()`, `safeArea()`, `displayScale()`, `windowFocused()`,
|
|
343
|
-
`keyboardHeight()`, `pointerLocked()`) for reading
|
|
344
|
-
|
|
345
|
-
(
|
|
346
|
-
reporting motion
|
|
472
|
+
`keyboardHeight()`, `pointerLocked()`) for reading window state;
|
|
473
|
+
onLayout's real job is the measure-then-draw pattern above. For mouse
|
|
474
|
+
look, `lockPointer(true)` enters relative mouse mode (cursor hidden and
|
|
475
|
+
confined, positions freeze) and pointer events keep reporting motion
|
|
476
|
+
through `movementX`/`movementY`.
|
|
347
477
|
|
|
348
478
|
- `createPortal` cannot mount during the app's initial render (it throws
|
|
349
479
|
"no mount target"): gate portal content behind a signal that starts false
|
package/agents/painting.md
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
Read this before building any screen whose look matters: a background, a
|
|
4
4
|
decoration, an effect, a chart, anything you would have reached for CSS for.
|
|
5
5
|
|
|
6
|
-
Layout and props are half the model. There is no stylesheet: no
|
|
7
|
-
|
|
6
|
+
Layout and props are half the model. There is no stylesheet: no selectors, no
|
|
7
|
+
keyframes, no canvas. The visual range a web app gets from CSS
|
|
8
8
|
comes from the tiers below instead, and reaching past tier 1 is ordinary
|
|
9
9
|
app-building here, not optimization - a screen built only from view
|
|
10
10
|
backgrounds and text is using a fraction of the runtime. Pick the tier the
|
|
11
|
-
CONTENT calls for, not the one that looks safest.
|
|
11
|
+
CONTENT calls for, not the one that looks safest. The tiers compose per
|
|
12
|
+
ELEMENT, not per screen: a dashboard's cards, rows and labels stay tier-1
|
|
13
|
+
laid-out structure even when every card holds a tier-2 chart or a tier-3
|
|
14
|
+
shader. Escalate the element the content calls for, never the whole screen.
|
|
12
15
|
|
|
13
16
|
1. Laid-out elements - `<view>`/`<text>`, with `<rect>` (or a filling
|
|
14
17
|
`<d-rect>` child) for background, border, radius. The structure of a
|
|
@@ -47,15 +50,23 @@ no matter how complex the effect, which is why the performance model
|
|
|
47
50
|
readable text over any fill.
|
|
48
51
|
- gradient background -> a gradient `color` on a `d-rect` (gradients are
|
|
49
52
|
paint values, usable anywhere a color is)
|
|
50
|
-
- `filter: blur/grayscale/hue-rotate
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
- `filter: blur/grayscale/hue-rotate/...` -> the `filter` prop on a view
|
|
54
|
+
(object form: `filter={{ blur: 4, grayscale: 1 }}`; hueRotate in radians).
|
|
55
|
+
For processing beyond the CSS filter functions, a `shader` on the view
|
|
56
|
+
(requires repaintBoundary="snapshot"), or on `<window>` for the whole frame
|
|
57
|
+
- `box-shadow` -> the `shadow` prop on the shape that IS the surface:
|
|
58
|
+
`<rect radius={12} color="white" shadow={{ y: 4, blur: 16, color:
|
|
59
|
+
"#0006" }}/>` (also on oval and path; fields are CSS box-shadow's, color
|
|
60
|
+
required). `text-shadow` / glow -> still no prop: draw an offset `d-*`
|
|
61
|
+
shape under the content, or a view shader with `outset` (the transparent
|
|
62
|
+
margin an effect bleeds into)
|
|
63
|
+
- `backdrop-filter` -> the `backdropFilter` prop on a view (same keys as
|
|
64
|
+
`filter`): frosted glass with real see-through semantics. It reads the
|
|
65
|
+
pixels already painted beneath, so it costs an offscreen capture - a
|
|
66
|
+
deliberate panel, not a casual style - and inside a
|
|
67
|
+
repaintBoundary="snapshot" subtree it sees only that boundary's own
|
|
68
|
+
content. A view `shader` cannot do this at all (it sees only its own
|
|
69
|
+
subtree's pixels)
|
|
59
70
|
- CSS `transition` -> the `transition` prop: declare it on the element and
|
|
60
71
|
keep writing targets; the runtime animates natively (performance rule 1)
|
|
61
72
|
- `@keyframes` -> a `transition` prop when the motion is target-shaped;
|
|
@@ -63,6 +74,12 @@ no matter how complex the effect, which is why the performance model
|
|
|
63
74
|
uniform when the animation is continuous and visual
|
|
64
75
|
- `<canvas>` 2D -> `d-*` primitives (rebuild one `d-path` string per frame
|
|
65
76
|
rather than animating N elements)
|
|
77
|
+
- leader lines, connectors, an annotation or badge hugging an element, any
|
|
78
|
+
drawing that must align with laid-out content -> measure, do not
|
|
79
|
+
calculate: read the boxes with `getBoundingBox` inside `onLayout` and
|
|
80
|
+
drive the `d-*` overlay from the result (core AGENTS.md "Measuring
|
|
81
|
+
layout", examples/on-layout-connect.tsx). Mirroring the layout math in JS
|
|
82
|
+
to predict where an element ends up is always wrong somewhere
|
|
66
83
|
- `<canvas>` WebGL, three.js -> `createPipelineTexture`, or `@solidrt/3d`
|
|
67
84
|
- video background, animated hero, particle field -> a shader texture; this
|
|
68
85
|
is the case the runtime is built for
|
package/agents/performance.md
CHANGED
|
@@ -88,8 +88,11 @@ Rules, in order of leverage:
|
|
|
88
88
|
springs need no pump at all - that is rule 1, and the runtime requests
|
|
89
89
|
frames only while tracks run. For genuinely procedural per-frame motion,
|
|
90
90
|
use a self-rechaining one-shot requestAnimationFrame that stops
|
|
91
|
-
re-requesting when its work list empties.
|
|
92
|
-
|
|
91
|
+
re-requesting when its work list empties. A loop that should run only
|
|
92
|
+
while a condition holds is an effect returning it:
|
|
93
|
+
`createEffect(() => active(), on => { if (!on) return; return onFrame(...) })`
|
|
94
|
+
- the apply phase has no owner, so onFrame registers nothing there and
|
|
95
|
+
the returned cleanup is the handle the effect disposes.
|
|
93
96
|
5. repaintBoundary works like Flutter's: transforms and opacity on the
|
|
94
97
|
boundary node itself (or any ancestor) are hoisted out of the cache and
|
|
95
98
|
applied at composite time, so animating x/y/scale/rotate/opacity of a
|
|
@@ -114,7 +117,15 @@ Rules, in order of leverage:
|
|
|
114
117
|
node - the hoisting described above.
|
|
115
118
|
6. "snapshot" boundaries pay first-frame texture allocation + raster:
|
|
116
119
|
creating many at once (dealing a board of 64 sprites) is a visible
|
|
117
|
-
one-frame hiccup - pool or pre-warm if that moment matters.
|
|
120
|
+
one-frame hiccup - pool or pre-warm if that moment matters. The texture
|
|
121
|
+
is sized by the view's box: the layout box on a view, the inherited
|
|
122
|
+
frame on a d-view (so a d-view directly under the window snapshots the
|
|
123
|
+
whole window - box it tighter when the content is small). Confirm a
|
|
124
|
+
boundary actually engaged rather than trusting the prop: get_render_tree
|
|
125
|
+
props reads
|
|
126
|
+
repaintBoundary back, get_stats' nodesPainted drops when the fenced
|
|
127
|
+
subtree stops being walked, and the dev overlay's BND/SNP lines count
|
|
128
|
+
boundary reuse per frame.
|
|
118
129
|
7. Shading pixels the app already drew is a different mechanism from rule 2's
|
|
119
130
|
generated textures, and both forms are a `shader` prop taking a linked
|
|
120
131
|
program from compileShader/linkProgram (@solidrt/core/gpu), not a
|
|
@@ -21,11 +21,14 @@ because there is nothing in the layout to reflow. The same content as a
|
|
|
21
21
|
laid-out `<rect>` with `left={x()}` would put the layout engine on the path of
|
|
22
22
|
every frame.
|
|
23
23
|
|
|
24
|
-
**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
**The layout form is the default: most of a screen - structure, panels,
|
|
25
|
+
labels, anything a flex or grid arrangement should place - is laid-out
|
|
26
|
+
elements.** Reach for the detached form for anything that moves at animation
|
|
27
|
+
frequency, and for free-form geometry drawn at coordinates - a chart's marks,
|
|
28
|
+
a diagram, an svg drawing, a particle field. A `designSize` view is not such
|
|
29
|
+
a reason: laid-out children resolve in design units too, so even a
|
|
30
|
+
fixed-design screen is built from laid-out elements, with detached geometry
|
|
31
|
+
only where content is genuinely drawn rather than arranged.
|
|
29
32
|
|
|
30
33
|
## What changes
|
|
31
34
|
|
|
@@ -91,7 +91,7 @@ for every pixel source in the engine.
|
|
|
91
91
|
## Logo
|
|
92
92
|
|
|
93
93
|
The SolidRT brand mark, the same seven-segment puzzle the scaffold's welcome
|
|
94
|
-
screen and the
|
|
94
|
+
screen and the player draw, as a component: a square `view` of `size`
|
|
95
95
|
pixels with the segments as gradient-filled `d-path`s.
|
|
96
96
|
|
|
97
97
|
{{ decl packages/core/src/logo.tsx LogoProps }}
|
|
@@ -51,6 +51,8 @@ Three of those props are worth knowing before you need them:
|
|
|
51
51
|
reflow.
|
|
52
52
|
- `repaintBoundary` retains the subtree's display list, and in its
|
|
53
53
|
`"snapshot"` forms its rasterized pixels too. It is the lever for putting
|
|
54
|
-
heavy static content next to content that changes every frame.
|
|
54
|
+
heavy static content next to content that changes every frame. On a
|
|
55
|
+
`d-view` the snapshot boxes the inherited frame: the nearest laid-out
|
|
56
|
+
ancestor's box, or the design size under a `designSize` view.
|
|
55
57
|
- `clipRadius` rounds the clip, and only does anything when `overflow` is
|
|
56
58
|
non-visible.
|
package/docs/reference/gpu.md
CHANGED
|
@@ -59,6 +59,8 @@ the dev tooling's GPU inventory and in engine log messages.
|
|
|
59
59
|
|
|
60
60
|
{{ decl packages/core/src/gpu.ts createTexture }}
|
|
61
61
|
|
|
62
|
+
{{ decl packages/core/src/gpu.ts createCubeTexture }}
|
|
63
|
+
|
|
62
64
|
{{ decl packages/core/src/gpu.ts createMutableTexture }}
|
|
63
65
|
|
|
64
66
|
{{ decl packages/core/src/gpu.ts createShaderTexture }}
|
|
@@ -147,6 +149,14 @@ clamped target in one consumer. The texture's own state stays what
|
|
|
147
149
|
`<texture>` paints and what every other binding uses. `mipmap` is not
|
|
148
150
|
overridable: the chain either exists on the id or it does not.
|
|
149
151
|
|
|
152
|
+
A cube map (`createCubeTexture`) is the same id currency with a different
|
|
153
|
+
sampler: declare `uniform samplerCube` and look it up by direction. The
|
|
154
|
+
declared sampler type is what a binding is checked against - a cube map on
|
|
155
|
+
a `sampler2D` throws, as does a 2D texture on a `samplerCube` - and a cube
|
|
156
|
+
map is sampler-only: nothing displays, reads back, copies or re-uploads it.
|
|
157
|
+
`wrap` does not apply to it (cube filtering is seamless across faces);
|
|
158
|
+
`filter`, `mipmap` and `anisotropy` do.
|
|
159
|
+
|
|
150
160
|
## Blending
|
|
151
161
|
|
|
152
162
|
Combining passes is a render-tree job: stack `<texture>` elements and set
|
|
@@ -202,6 +212,7 @@ instead - for a UI subtree, `snapshotTexture`.
|
|
|
202
212
|
## Limits
|
|
203
213
|
|
|
204
214
|
`limits` holds the device ceilings queried at startup: maximum texture and
|
|
205
|
-
target size, sampler inputs per pass, vertex attributes
|
|
215
|
+
target size, cube map face size, sampler inputs per pass, vertex attributes
|
|
216
|
+
per pipeline. Creates
|
|
206
217
|
and binds validate against them and throw naming the limit; read them to
|
|
207
218
|
size within the device instead.
|
package/docs/reference/layout.md
CHANGED
|
@@ -42,3 +42,31 @@ wrap around.
|
|
|
42
42
|
## Grid
|
|
43
43
|
|
|
44
44
|
{{ decl packages/core/src/types.d.ts GridProps }}
|
|
45
|
+
|
|
46
|
+
## Measuring
|
|
47
|
+
|
|
48
|
+
Positions come from the engine, not from arithmetic mirrored in app code.
|
|
49
|
+
`getBoundingBox(node)` returns the box from the most recently computed layout
|
|
50
|
+
as `{ x, y, width, height }`, transforms composed, relative to the nearest
|
|
51
|
+
`position="relative"` ancestor (falling back to the window), or `null` before
|
|
52
|
+
the first layout. `getBoundingBoxViewport(node)` is the same box always
|
|
53
|
+
window-relative, the space pointer `clientX`/`clientY` report in. Both are
|
|
54
|
+
snapshot reads, not reactive.
|
|
55
|
+
|
|
56
|
+
The moment to read them is `onLayout(fn)`: it fires after layout is computed
|
|
57
|
+
but before paint, so a measurement can still shape what the current frame
|
|
58
|
+
draws - the pattern for a connector between two laid-out cards, or an
|
|
59
|
+
annotation pinned to a bar the layout placed. Write the result to something
|
|
60
|
+
that does not affect layout (detached geometry, a `d` string, a transform)
|
|
61
|
+
and call `flush()` so the write lands before the display list is built;
|
|
62
|
+
writing a layout prop from `onLayout` costs an extra layout pass.
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
onLayout(() => {
|
|
66
|
+
let a = getBoundingBox(boxA)
|
|
67
|
+
let b = getBoundingBox(boxB)
|
|
68
|
+
if (!a || !b) return
|
|
69
|
+
setD(`M ${a.x + a.width / 2} ${a.y + a.height / 2} L ${b.x} ${b.y}`)
|
|
70
|
+
flush()
|
|
71
|
+
})
|
|
72
|
+
```
|
package/examples/README.md
CHANGED
|
@@ -25,6 +25,9 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
25
25
|
## Pointer input
|
|
26
26
|
- `pointer-local-coords.tsx` - the three pointer coordinate frames (`clientX` window, `localX` the handling node's own frame, `parentX` its path-parent's frame - where the node's x/y live) and the transform-proof drag idiom: grab offset from `localX` at down, place with `parentX - offset` on moves. Exact inside rotated/scaled ancestors and when the pointer leaves the node mid-drag.
|
|
27
27
|
|
|
28
|
+
## Gamepads
|
|
29
|
+
- `gamepad.tsx` - the pad test bed: every connected pad's raw `gamepads()` snapshot (slots, buttons, axes) and two players seated by `gamepad.next()` - press any button on a pad to join player 1, another pad joins player 2 - each an input map (`move` on the left stick and dpad, `zoom` on the triggers, `jump` on south) printing its values live and walking a marker with `move`. Nothing moves until a pad does.
|
|
30
|
+
|
|
28
31
|
## Performance
|
|
29
32
|
- `snapshot-texture.tsx` - `snapshotTexture(ref)`: a `repaintBoundary="snapshot"` view's rasterized pixels as a live texture id; a shader texture samples the panel and a sibling `<texture>` shows the warped copy. The boundary re-rasterizes only when its content changes.
|
|
30
33
|
- `repaint-boundary.tsx` - `repaintBoundary` on a `<view>` to keep static content from rebuilding while a neighbor animates: `{true}` retains the recorded draw list, `"snapshot"` also retains the rasterized pixels as a GPU texture (for raster-expensive, screen-aligned, static subtrees). `"snapshot-no-aa"` rasterizes without anti-aliasing: cheaper, fine for text and axis-aligned rects, hard-edged on vector content.
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// you place it yourself with x/y. Because a detached node does not participate in
|
|
3
3
|
// layout, moving it (animating x/y) does not reflow its siblings - which makes
|
|
4
4
|
// d- elements the right choice for overlays, badges, and anything that moves
|
|
5
|
-
// independently.
|
|
5
|
+
// independently. They are the exception, not the default: screen structure
|
|
6
|
+
// (panels, cards, labels) stays laid out with <view>/<text>; a d- subtree is
|
|
7
|
+
// for drawn or moving content. The rule: a detached node can only contain other detached
|
|
6
8
|
// nodes. Everything under a d- element must itself be a d- element (here d-rect +
|
|
7
9
|
// d-text) - nesting a plain <view> or <text> inside a d- element is an error.
|
|
8
10
|
import { render } from "@solidrt/core"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Gamepad test bed: every connected pad's raw snapshot (gamepads()) at the
|
|
2
|
+
// top, and two players seated by gamepad.next() below - press any button
|
|
3
|
+
// on a pad and it joins player 1, another pad joins player 2. Each player
|
|
4
|
+
// is an input map (`move` vec2 on the left stick and the dpad, `zoom` axis
|
|
5
|
+
// on the triggers, `jump` button on south) whose values print live, plus
|
|
6
|
+
// a marker that walks with `move`, so what a pad does is on screen.
|
|
7
|
+
// Nothing moves until a pad does.
|
|
8
|
+
import { createInputMap, createSignal, gamepad, gamepads, onFrame, render, untrack, windowSize, For } from "@solidrt/core"
|
|
9
|
+
import type { Vec2 } from "@solidrt/core"
|
|
10
|
+
|
|
11
|
+
const PLAYERS = 2
|
|
12
|
+
// Marker travel at full stick deflection, pixels per second.
|
|
13
|
+
const SPEED = 300
|
|
14
|
+
const MARKER = 28
|
|
15
|
+
const COLORS = ["#ffcc4d", "#66e6ff"]
|
|
16
|
+
|
|
17
|
+
let fmt = (v: number) => (v < 0 ? "" : " ") + v.toFixed(2)
|
|
18
|
+
let clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v))
|
|
19
|
+
|
|
20
|
+
function App() {
|
|
21
|
+
let players = Array.from({ length: PLAYERS }, (_, i) => {
|
|
22
|
+
let pad = gamepad.next()
|
|
23
|
+
let input = createInputMap({ move: "vec2", zoom: "axis", jump: "button" })
|
|
24
|
+
input.bind("move", pad.leftStick, pad.dpad)
|
|
25
|
+
input.bind("zoom", pad.triggers)
|
|
26
|
+
input.bind("jump", pad.button("south"))
|
|
27
|
+
// The marker: plain numbers stepped per frame, a signal to redraw.
|
|
28
|
+
let x = 80 + i * 120
|
|
29
|
+
let y = 260
|
|
30
|
+
let [pos, setPos] = createSignal<Vec2>([x, y])
|
|
31
|
+
let [jumps, setJumps] = createSignal(0)
|
|
32
|
+
input.onPress("jump", () => setJumps(n => n + 1))
|
|
33
|
+
let step = (dt: number) => {
|
|
34
|
+
let [mx, my] = untrack(() => input.value("move"))
|
|
35
|
+
if (mx === 0 && my === 0) return
|
|
36
|
+
let size = untrack(windowSize)
|
|
37
|
+
x = clamp(x + mx * SPEED * dt, 0, Math.max(0, size.width - MARKER))
|
|
38
|
+
y = clamp(y + my * SPEED * dt, 0, Math.max(0, size.height - MARKER))
|
|
39
|
+
setPos([x, y])
|
|
40
|
+
}
|
|
41
|
+
return { pad, input, pos, jumps, step }
|
|
42
|
+
})
|
|
43
|
+
let last: number | null = null
|
|
44
|
+
onFrame(tick => {
|
|
45
|
+
let dt = last === null ? 0 : (tick - last) / 1000
|
|
46
|
+
last = tick
|
|
47
|
+
for (let p of players) p.step(dt)
|
|
48
|
+
})
|
|
49
|
+
let padLine = (slot: number, pad: { name: string; buttons: string[]; axes: Record<string, number> } | null) => {
|
|
50
|
+
if (!pad) return `slot ${slot}: empty`
|
|
51
|
+
let a = pad.axes
|
|
52
|
+
return `slot ${slot}: ${pad.name} - buttons [${pad.buttons.join(" ")}] left${fmt(a.leftX ?? 0)},${fmt(a.leftY ?? 0)} right${fmt(a.rightX ?? 0)},${fmt(a.rightY ?? 0)} triggers${fmt(a.leftTrigger ?? 0)},${fmt(a.rightTrigger ?? 0)}`
|
|
53
|
+
}
|
|
54
|
+
return (
|
|
55
|
+
<window>
|
|
56
|
+
<d-rect color="#181820" />
|
|
57
|
+
<view flexDirection="column" gap={10} padding={24}>
|
|
58
|
+
<text color="#eef4ff" fontSize={22} fontWeight={700}>
|
|
59
|
+
Gamepads
|
|
60
|
+
</text>
|
|
61
|
+
<text color="#a9bcd6" fontSize={14}>
|
|
62
|
+
press any button on a pad to join as player 1, another pad for player 2 - left stick or dpad walks the marker, triggers read zoom, south counts jumps
|
|
63
|
+
</text>
|
|
64
|
+
<view flexDirection="column" gap={2}>
|
|
65
|
+
<text color="#a9bcd6" fontSize={14}>
|
|
66
|
+
connected: {String(gamepads().filter(p => p !== null).length)}
|
|
67
|
+
</text>
|
|
68
|
+
<For each={gamepads()} keyed={false}>
|
|
69
|
+
{(pad, i) => (
|
|
70
|
+
<text color="#eef4ff" fontSize={14}>
|
|
71
|
+
{padLine(i, pad())}
|
|
72
|
+
</text>
|
|
73
|
+
)}
|
|
74
|
+
</For>
|
|
75
|
+
</view>
|
|
76
|
+
<For each={players}>
|
|
77
|
+
{(p, i) => (
|
|
78
|
+
<view flexDirection="column" gap={2}>
|
|
79
|
+
<text color={COLORS[i()]} fontSize={16} fontWeight={700}>
|
|
80
|
+
player {String(i() + 1)}: {p.pad.slot === undefined ? "press any button on a pad to join" : `pad ${p.pad.slot}`}
|
|
81
|
+
</text>
|
|
82
|
+
<text color="#eef4ff" fontSize={14}>
|
|
83
|
+
move{fmt(p.input.value("move")[0])},{fmt(p.input.value("move")[1])} zoom{fmt(p.input.value("zoom"))} jump {p.input.pressed("jump") ? "down" : "up"} (x{String(p.jumps())})
|
|
84
|
+
</text>
|
|
85
|
+
</view>
|
|
86
|
+
)}
|
|
87
|
+
</For>
|
|
88
|
+
</view>
|
|
89
|
+
<For each={players}>{(p, i) => <d-rect x={p.pos()[0]} y={p.pos()[1]} w={MARKER} h={MARKER} radius={6} color={COLORS[i()]} />}</For>
|
|
90
|
+
</window>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
render(() => <App />)
|
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
// geometry - diagrams, slides, dashboards, game boards, emulator screens - fit
|
|
31
31
|
// one design space instead.
|
|
32
32
|
//
|
|
33
|
+
// Design space changes the units, not the element choice: laid-out views and
|
|
34
|
+
// text stay the default under a designSize (fact 3). The d-* scene below is
|
|
35
|
+
// free-form drawn geometry; panels, cards and labels would still be built
|
|
36
|
+
// from laid-out elements, as the flex row at the bottom shows.
|
|
37
|
+
//
|
|
33
38
|
// Resize the window and watch: the scene keeps its aspect ratio, everything
|
|
34
39
|
// including text scales together, and the readout reports the same design
|
|
35
40
|
// coordinates for the same spot on the scene at any window size.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.57",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
"./data": "./src/data.ts",
|
|
14
14
|
"./gpu": "./src/gpu.ts",
|
|
15
15
|
"./image": "./src/image.ts",
|
|
16
|
+
"./input": "./src/input.ts",
|
|
16
17
|
"./audio": "./src/audio.ts",
|
|
17
18
|
"./microphone": "./src/microphone.ts",
|
|
18
19
|
"./speech-recognition": "./src/speech-recognition.ts",
|
|
19
20
|
"./text-input": "./src/text-input.ts",
|
|
21
|
+
"./video": "./src/video.ts",
|
|
20
22
|
"./jsx-runtime": "./jsx-runtime.d.ts",
|
|
21
23
|
"./jsx-runtime-dev": "./jsx-runtime.d.ts"
|
|
22
24
|
},
|
|
@@ -29,11 +31,11 @@
|
|
|
29
31
|
"AGENTS.md"
|
|
30
32
|
],
|
|
31
33
|
"devDependencies": {
|
|
32
|
-
"@solidrt/flux-types": "0.0.
|
|
34
|
+
"@solidrt/flux-types": "0.0.57"
|
|
33
35
|
},
|
|
34
36
|
"peerDependencies": {
|
|
35
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
36
|
-
"@solidjs/universal": "2.0.0-rc.
|
|
37
|
-
"solid-js": "2.0.0-rc.
|
|
37
|
+
"@solidjs/signals": "2.0.0-rc.6",
|
|
38
|
+
"@solidjs/universal": "2.0.0-rc.6",
|
|
39
|
+
"solid-js": "2.0.0-rc.6"
|
|
38
40
|
}
|
|
39
41
|
}
|