@solidrt/core 0.0.50 → 0.0.52
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 +102 -21
- package/README.md +1 -1
- package/agents/painting.md +61 -0
- package/agents/performance.md +216 -0
- package/docs/index.md +154 -0
- package/docs/reference/detached.md +85 -0
- package/docs/reference/drawing.md +95 -0
- package/docs/reference/elements.md +56 -0
- package/docs/reference/gpu.md +204 -0
- package/docs/reference/index.md +50 -0
- package/docs/reference/input.md +58 -0
- package/docs/reference/layout.md +44 -0
- package/docs/reference/shaders.md +46 -0
- package/docs/reference/text.md +46 -0
- package/docs/reference/transforms.md +35 -0
- package/docs/reference/types.md +34 -0
- package/examples/README.md +7 -5
- package/examples/{sound.tsx → audio.tsx} +1 -1
- package/examples/gpu-pipeline.tsx +2 -2
- package/examples/gpu-sprites.tsx +102 -0
- package/examples/line-points.tsx +145 -0
- package/examples/parse-svg.tsx +6 -6
- package/examples/responsive-grid.tsx +1 -1
- package/examples/scroll.tsx +2 -2
- package/examples/snapshot-texture.tsx +72 -0
- package/examples/{view-viewbox.tsx → view-design-size.tsx} +33 -15
- package/jsx-runtime.d.ts +15 -14
- package/package.json +11 -9
- package/src/{sound.ts → audio.ts} +68 -15
- package/src/color.ts +17 -18
- package/src/core.ts +40 -3
- package/src/data.ts +99 -0
- package/src/gpu.ts +88 -34
- package/src/index.ts +9 -3
- package/src/logo.tsx +92 -0
- package/src/renderer.ts +219 -53
- package/src/runtime-modules.d.ts +7 -2
- package/src/scroll.ts +51 -15
- package/src/svg.ts +1 -1
- package/src/text-input.ts +297 -61
- package/src/types.d.ts +291 -31
- package/src/window.ts +110 -14
package/AGENTS.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# @solidrt/core - agent notes
|
|
2
2
|
|
|
3
|
-
Dense, self-contained facts for writing a SolidRT app.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Dense, self-contained facts for writing a SolidRT app. The prose lives in
|
|
4
|
+
docs/ (also the website); when this conflicts with it, trust this file and
|
|
5
|
+
the types in src/types.d.ts and jsx-runtime.d.ts.
|
|
6
6
|
|
|
7
7
|
SolidRT is a custom SolidJS renderer: it paints through a Rust runtime, not the
|
|
8
8
|
DOM. There is no HTML, no CSS cascade, no `className`.
|
|
9
9
|
|
|
10
|
+
Two companion files carry the depth this one leaves out; read the one that
|
|
11
|
+
matches the work before starting it:
|
|
12
|
+
- agents/painting.md - what you paint with, and what replaces each CSS
|
|
13
|
+
reflex. Read before styling a screen: a background, a gradient, a shadow,
|
|
14
|
+
an effect, vector art, a chart.
|
|
15
|
+
- agents/performance.md - the performance model, in order of leverage. Read
|
|
16
|
+
before writing any per-frame code, any animation, or anything that writes
|
|
17
|
+
properties in a loop.
|
|
18
|
+
|
|
10
19
|
## The window is device-sized - design fluid
|
|
11
20
|
|
|
12
21
|
A SolidRT window is host-sized and resizable, and the SAME app runs on phones,
|
|
@@ -26,12 +35,19 @@ hardcode desktop pixels.
|
|
|
26
35
|
|
|
27
36
|
Exception - fixed-aspect content. For content with fixed internal geometry
|
|
28
37
|
(diagrams, slides, dashboards, games, emulators), do not branch on window size
|
|
29
|
-
at all: author everything in one design space and let `
|
|
30
|
-
`<view flex={1}
|
|
38
|
+
at all: author everything in one design space and let `designSize` fit it.
|
|
39
|
+
`<view flex={1} designSize={[1280, 800]}>` uniformly scales and centers the
|
|
31
40
|
children (letterboxed), pointer events on them arrive in design coordinates,
|
|
32
|
-
and the same code runs unchanged from a desktop window to a phone.
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
and the same code runs unchanged from a desktop window to a phone. Laid-out
|
|
42
|
+
children (flex, percentages, text wrap) resolve against the design size too,
|
|
43
|
+
so a whole panel scales into a smaller box without reflowing; the view itself
|
|
44
|
+
sizes like a replaced element whose intrinsic size is the design size. One
|
|
45
|
+
trap from flexbox, not from designSize: in a flex row a width-only design-size view
|
|
46
|
+
is stretched to the line's height under the default alignment, so give the
|
|
47
|
+
view `alignSelf="flex-start"` (or the row a non-stretch `alignItems`) to get
|
|
48
|
+
the design aspect - `aspectRatio` does not override stretch. Reach for
|
|
49
|
+
`windowSizeClass` branching only when the layout genuinely reflows across
|
|
50
|
+
form factors.
|
|
35
51
|
|
|
36
52
|
`env` and `capabilities` (both exported from `@solidrt/core`) are the two
|
|
37
53
|
objects that expose this. They are plain objects with REACTIVE GETTERS, not
|
|
@@ -64,7 +80,7 @@ you need the raw fact (e.g. `env.displayScale` for asset sizing below).
|
|
|
64
80
|
Because the drawn size is fluid and the display DPI varies, asset format is a
|
|
65
81
|
real design decision, not an afterthought:
|
|
66
82
|
|
|
67
|
-
- Prefer VECTORS (`parseSvg` draws mapped to `<d-path>` in a `
|
|
83
|
+
- Prefer VECTORS (`parseSvg` draws mapped to `<d-path>` in a `designSize` view)
|
|
68
84
|
whenever the render size is fluid or DPI varies - they stay crisp at any
|
|
69
85
|
size x `displayScale()`.
|
|
70
86
|
- RASTER (`<texture>` / `createImage`) needs source resolution >= displayed size
|
|
@@ -79,8 +95,9 @@ bun add @solidrt/core # the renderer
|
|
|
79
95
|
bun add -d @solidrt/cli # the `srt` tool (see its AGENTS.md)
|
|
80
96
|
```
|
|
81
97
|
|
|
82
|
-
|
|
83
|
-
(
|
|
98
|
+
Core primitives alone are enough to build a full app. The optional extensions
|
|
99
|
+
(each with its own AGENTS.md) build on it: `@solidrt/components` (themed
|
|
100
|
+
widgets), `@solidrt/2d` (2D graphics and games), `@solidrt/3d` (scene graph).
|
|
84
101
|
|
|
85
102
|
tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
|
|
86
103
|
|
|
@@ -94,13 +111,23 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
|
|
|
94
111
|
```
|
|
95
112
|
|
|
96
113
|
Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
97
|
-
2.0.0-rc.
|
|
114
|
+
2.0.0-rc.1); bun resolves them from peerDependencies.
|
|
98
115
|
|
|
99
116
|
## Element model (the parts that are easy to get wrong)
|
|
100
117
|
|
|
101
118
|
- `render(() => <App />)`. The returned root MUST be a `<window>` or it throws.
|
|
102
119
|
Call render once, at the top level.
|
|
103
120
|
|
|
121
|
+
- Errors never halt the app. One thrown while computing an element's props or
|
|
122
|
+
a child expression is contained at that element: it keeps its last good
|
|
123
|
+
value, one `Contained error` log line names the node and the .tsx line,
|
|
124
|
+
and it recovers when the expression computes again. Anything unclaimed
|
|
125
|
+
beyond that (a throwing `createEffect`, an error while mounting) reaches
|
|
126
|
+
render()'s root boundary, which replaces the app's window with an error
|
|
127
|
+
window (message, stack, a Reset button that retries the failed
|
|
128
|
+
computations) and logs `Uncaught error`. `<Errored>` gives a subtree its
|
|
129
|
+
own in-place fallback.
|
|
130
|
+
|
|
104
131
|
- Two kinds of element:
|
|
105
132
|
- Containers - `<window>`, `<view>`. Do layout + transform + pointer events.
|
|
106
133
|
THEY DO NOT PAINT. A `<view>` has no background/fill prop.
|
|
@@ -113,11 +140,13 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
|
113
140
|
Outlines: `drawStyle="stroke"` (or "stroke-and-fill") plus `strokeWidth`.
|
|
114
141
|
Corner radius on draw primitives: `radius` (number or [tl, tr, br, bl]).
|
|
115
142
|
|
|
116
|
-
- Registered JSX intrinsics: `window`, `view`, `text`, `
|
|
117
|
-
`path`, `texture`, `audio`, plus the `d-` variants `d-view`, `d-rect`,
|
|
143
|
+
- Registered JSX intrinsics: `window`, `view`, `text`, `span`, `rect`, `oval`,
|
|
144
|
+
`line`, `path`, `texture`, `audio`, plus the `d-` variants `d-view`, `d-rect`,
|
|
118
145
|
`d-oval`, `d-line`, `d-path`, `d-texture`, `d-text`. Line endpoints
|
|
119
146
|
(`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.
|
|
147
|
+
endpoint props and spans its layout box corner to corner. `points` (a flat
|
|
148
|
+
`[x0, y0, x1, y1, ...]` array, plus `closed`) turns either form into a
|
|
149
|
+
polyline and wins over the endpoints while set.
|
|
121
150
|
|
|
122
151
|
- Plain vs `d-` variant (the `d-` prefix means "detached" - detached from the
|
|
123
152
|
layout engine, Taffy): a plain element (e.g. `rect`) is `RectProps &
|
|
@@ -135,6 +164,8 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
|
135
164
|
box center; a d-view has no box). To scale a detached group around its
|
|
136
165
|
content's center, set the origin explicitly in pixels, e.g.
|
|
137
166
|
`originX={100} originY={50}` for content drawn in a 200x100 local space.
|
|
167
|
+
Avoid pct()/keyword origins on a d-view - they resolve against the box
|
|
168
|
+
inherited from the nearest laid-out ancestor.
|
|
138
169
|
|
|
139
170
|
- Layout-affecting vs not (this matters for per-frame work). Props fall in three
|
|
140
171
|
buckets, split by where they take effect:
|
|
@@ -154,13 +185,18 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
|
154
185
|
`position:absolute` at `left:0,top:0`, or just let normal flow place it) and
|
|
155
186
|
then translate it with `x`/`y`.
|
|
156
187
|
|
|
188
|
+
- Text `lineHeight` is a MULTIPLIER of fontSize (1.3-1.6 is typical), not
|
|
189
|
+
pixels. A CSS-reflex value like 22 makes each line box 22x the font size:
|
|
190
|
+
the text becomes blank space and the parent balloons.
|
|
191
|
+
|
|
157
192
|
- JSX text children collapse whitespace (ordinary JSX semantics): runs of
|
|
158
193
|
spaces become one, so space-padding a mono label collapses silently. An
|
|
159
194
|
expression container preserves it - `<d-text>{"one two"}</d-text>` - and
|
|
160
195
|
`\n` inside one produces a hard line break.
|
|
161
196
|
|
|
162
197
|
- Rich text: `<span>` inside `<text>` restyles a run (`color`, `fontFamily`,
|
|
163
|
-
`fontSize`, `fontWeight`, `fontStyle`, `lineHeight
|
|
198
|
+
`fontSize`, `fontWeight`, `fontStyle`, `lineHeight`,
|
|
199
|
+
`textDecoration="underline"`); spans nest and
|
|
164
200
|
inherit inward from the `<text>`. Never lay a paragraph out word by word in
|
|
165
201
|
a wrapping row to mix styles - one `<text>` with spans wraps as a whole.
|
|
166
202
|
A span is content, not a box (no layout or `d-` form, no size, no
|
|
@@ -206,9 +242,55 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
|
206
242
|
TRACKED compute that reads signals and returns a value, then an UNTRACKED
|
|
207
243
|
effect that receives it - `createEffect(() => count(), (c) => ...)`. The 1.x
|
|
208
244
|
single-callback form `createEffect(() => { ...count()... })` does NOT track
|
|
209
|
-
here.
|
|
210
|
-
|
|
211
|
-
|
|
245
|
+
here.
|
|
246
|
+
Reading a signal/prop/store at the top level of a component body (not
|
|
247
|
+
inside JSX, a `createMemo`, or an effect's compute phase) reads it
|
|
248
|
+
untracked: it silently freezes at the initial value.
|
|
249
|
+
Writing a signal or store from inside an owned scope (a component body, a
|
|
250
|
+
`createMemo`, an effect's compute phase) throws
|
|
251
|
+
`REACTIVE_WRITE_IN_OWNED_SCOPE` in dev; a loader called in the component
|
|
252
|
+
body that sets state is the classic React / Solid 1.x reflex that hits
|
|
253
|
+
this. Move the write into an event handler, an effect's apply phase,
|
|
254
|
+
`onSettled`, or an `untrack` block; opt in narrowly with
|
|
255
|
+
`createSignal(v, { ownedWrite: true })` for a signal that genuinely is
|
|
256
|
+
internal state.
|
|
257
|
+
Signal writes flush on a microtask: a handler that sets a signal and
|
|
258
|
+
immediately reads it back gets the OLD value. Read it in an effect, or
|
|
259
|
+
call `flush()` (from @solidjs/signals) to force it through.
|
|
260
|
+
|
|
261
|
+
- An element-valued prop (children, a content/icon slot) compiles to a getter
|
|
262
|
+
that builds a fresh native subtree on EVERY read, and a subtree that is
|
|
263
|
+
never inserted is never freed - native nodes are not garbage collected, so
|
|
264
|
+
what is only wasted work in DOM Solid is a permanent memory leak here. Read
|
|
265
|
+
such props exactly once, at the place they are mounted. To inspect
|
|
266
|
+
children (a typeof probe, counting), resolve them first with the
|
|
267
|
+
`children()` helper (re-exported from @solidrt/core) and probe the resolved
|
|
268
|
+
memo - never `typeof props.children` on the raw prop.
|
|
269
|
+
|
|
270
|
+
- Animation is target-shaped first: declare `transition` on the element and
|
|
271
|
+
write targets, and the runtime animates natively with no per-frame JS.
|
|
272
|
+
Reach for per-frame work only for genuinely procedural motion:
|
|
273
|
+
`onFrame((tick, frame) => {})` is the native hook (runtime-paced, returns a
|
|
274
|
+
cleanup, auto-cleaned inside a reactive scope); `requestAnimationFrame`
|
|
275
|
+
exists as a web-standard one-shot but is not the preferred driver. A JS
|
|
276
|
+
tween loop or an animation library pushing interpolated values through
|
|
277
|
+
signals is the single most expensive mistake available here - read
|
|
278
|
+
agents/performance.md before writing either.
|
|
279
|
+
Window state: onResize, onLayout, onWindowFocus, onWindowBlur exist, but
|
|
280
|
+
prefer the reactive reads (`env`/`capabilities` above, or the accessors
|
|
281
|
+
`windowSize()`, `safeArea()`, `displayScale()`, `windowFocused()`,
|
|
282
|
+
`keyboardHeight()`, `pointerLocked()`) for reading layout and window
|
|
283
|
+
state. For mouse look, `lockPointer(true)` enters relative mouse mode
|
|
284
|
+
(cursor hidden and confined, positions freeze) and pointer events keep
|
|
285
|
+
reporting motion through `movementX`/`movementY`.
|
|
286
|
+
|
|
287
|
+
- `createPortal` cannot mount during the app's initial render (it throws
|
|
288
|
+
"no mount target"): gate portal content behind a signal that starts false
|
|
289
|
+
and open it after startup - overlay content is opened, not born open.
|
|
290
|
+
`createScroll` containers need an explicit main-axis size (a height, or
|
|
291
|
+
flex inside a sized parent); with neither they resolve to 0 and the
|
|
292
|
+
content silently vanishes (`maxHeight` alone does not size it). The
|
|
293
|
+
runtime warns when this happens.
|
|
212
294
|
|
|
213
295
|
- Device/GPU access via subpath imports: @solidrt/core/camera, /microphone,
|
|
214
296
|
/speech, /gpu. Image flow: `decodeImage(bytes)` ->
|
|
@@ -241,5 +323,4 @@ render(() => <App />)
|
|
|
241
323
|
Note the two `<d-rect>` underlays: a `<view>`/`<window>` does not paint, so a
|
|
242
324
|
background is a draw-primitive child placed behind the content.
|
|
243
325
|
|
|
244
|
-
To run and verify (incl. headless), see @solidrt/cli (its AGENTS.md).
|
|
245
|
-
higher-level components, see @solidrt/components (its AGENTS.md).
|
|
326
|
+
To run and verify (incl. headless), see @solidrt/cli (its AGENTS.md).
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Optionally, create a `tsconfig.json` to enable type recognition for SolidRT elem
|
|
|
53
53
|
|
|
54
54
|
## API
|
|
55
55
|
|
|
56
|
-
See [docs/
|
|
56
|
+
See [docs/](https://github.com/wellawaretech/solidrt/blob/main/packages/core/docs/index.md) (shipped in the package) for the full API reference.
|
|
57
57
|
|
|
58
58
|
## License
|
|
59
59
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# What you paint with (there is no CSS layer)
|
|
2
|
+
|
|
3
|
+
Read this before building any screen whose look matters: a background, a
|
|
4
|
+
decoration, an effect, a chart, anything you would have reached for CSS for.
|
|
5
|
+
|
|
6
|
+
Layout and props are half the model. There is no stylesheet: no filters, no
|
|
7
|
+
box-shadow, no keyframes, no canvas. The visual range a web app gets from CSS
|
|
8
|
+
comes from the tiers below instead, and reaching past tier 1 is ordinary
|
|
9
|
+
app-building here, not optimization - a screen built only from view
|
|
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.
|
|
12
|
+
|
|
13
|
+
1. Laid-out elements - `<view>`/`<text>`, with `<rect>` (or a filling
|
|
14
|
+
`<d-rect>` child) for background, border, radius. The structure of a
|
|
15
|
+
screen, not its finish.
|
|
16
|
+
2. Vector art, detached from layout - `d-path`/`d-rect`/`d-oval`/`d-line`,
|
|
17
|
+
whose `color` takes a gradient (createLinearGradient /
|
|
18
|
+
createRadialGradient) and which honour `blendMode`, plus `parseSvg` to
|
|
19
|
+
draw a whole SVG document as one subtree. Free-form shapes, decoration,
|
|
20
|
+
diagrams, charts, anything positioned rather than flowed. Examples:
|
|
21
|
+
parse-svg, detached-positioning, text-paint-styling.
|
|
22
|
+
3. GPU textures - `createShaderTexture` puts a fragment shader in a
|
|
23
|
+
`<texture>` (moving gradients, noise, glow, dissolves, a background that
|
|
24
|
+
is alive), `createPipelineTexture` draws geometry you generate yourself
|
|
25
|
+
(particles, point clouds, splats), and the `shader` prop post-processes
|
|
26
|
+
content that already exists: on a `<view>` it grades, warps or dissolves
|
|
27
|
+
that subtree, on `<window>` the whole frame. Stack `<texture>` elements
|
|
28
|
+
with `blendMode` to combine passes. Examples: gpu-shader, gpu-particles,
|
|
29
|
+
gpu-pipeline, gpu-instancing, gpu-texture-blend, view-shader,
|
|
30
|
+
window-shader.
|
|
31
|
+
4. 3D scenes - add `@solidrt/3d` (not a scaffold dependency): meshes,
|
|
32
|
+
materials and a camera declared as Solid components, rendered into a
|
|
33
|
+
texture that sits in the UI tree like any other element.
|
|
34
|
+
|
|
35
|
+
Tier 3 is cheaper than it looks. A shader costs one property write per frame
|
|
36
|
+
no matter how complex the effect, which is why the performance model
|
|
37
|
+
(agents/performance.md) reaches for it first rather than as a last resort.
|
|
38
|
+
|
|
39
|
+
## Web reflexes and what replaces them
|
|
40
|
+
|
|
41
|
+
- gradient background -> a gradient `color` on a `d-rect` (gradients are
|
|
42
|
+
paint values, usable anywhere a color is)
|
|
43
|
+
- `filter: blur/grayscale/hue-rotate`, and any "make this look processed" ->
|
|
44
|
+
a `shader` on the view (requires repaintBoundary="snapshot"), or on
|
|
45
|
+
`<window>` for the whole frame
|
|
46
|
+
- `box-shadow` / `text-shadow` / glow -> no shadow prop exists: draw an
|
|
47
|
+
offset `d-*` shape under the content, or a view shader with `outset` (the
|
|
48
|
+
transparent margin an effect bleeds into)
|
|
49
|
+
- `backdrop-filter` -> no equivalent. A view shader sees only its own
|
|
50
|
+
subtree's pixels, never what is behind it. Frost the whole frame with a
|
|
51
|
+
window shader, or fake the layer with your own content
|
|
52
|
+
- CSS `transition` -> the `transition` prop: declare it on the element and
|
|
53
|
+
keep writing targets; the runtime animates natively (performance rule 1)
|
|
54
|
+
- `@keyframes` -> a `transition` prop when the motion is target-shaped;
|
|
55
|
+
`onFrame` writing a signal for genuinely procedural sequences; a `uTime`
|
|
56
|
+
uniform when the animation is continuous and visual
|
|
57
|
+
- `<canvas>` 2D -> `d-*` primitives (rebuild one `d-path` string per frame
|
|
58
|
+
rather than animating N elements)
|
|
59
|
+
- `<canvas>` WebGL, three.js -> `createPipelineTexture`, or `@solidrt/3d`
|
|
60
|
+
- video background, animated hero, particle field -> a shader texture; this
|
|
61
|
+
is the case the runtime is built for
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Performance model (JS is the slow lane)
|
|
2
|
+
|
|
3
|
+
Read this before writing any per-frame code, any animation, or anything that
|
|
4
|
+
writes properties in a loop.
|
|
5
|
+
|
|
6
|
+
The JS engine is interpreted and every property write crosses an FFI boundary
|
|
7
|
+
into the runtime, so per-frame JS work is the expensive path while GPU work is
|
|
8
|
+
nearly free. That holds on desktop and on current mobile hardware; "Where GPU
|
|
9
|
+
work stops being free" below is where it does not. The design answer is not
|
|
10
|
+
"write less JS" but "keep JS off the per-frame path": the platform animates
|
|
11
|
+
(transitions), caches (repaint boundaries), shades (GPU) and computes
|
|
12
|
+
(isolates, wasm) natively, and JS stays the coordinator that sets targets.
|
|
13
|
+
Rules, in order of leverage:
|
|
14
|
+
|
|
15
|
+
1. Motion between states (position, size, opacity, transform components,
|
|
16
|
+
solid colors, enter/exit) belongs in a native transition, never in
|
|
17
|
+
per-frame JS. Declare `transition` on the element and keep writing
|
|
18
|
+
targets the ordinary way; the runtime interpolates every frame on the
|
|
19
|
+
Rust side, so JS runs only when a target changes and the running
|
|
20
|
+
animation costs no JS and no property writes per frame, however many
|
|
21
|
+
elements move. Flat spec, ms durations, kind inferred:
|
|
22
|
+
`{ duration }` / `{ duration, bounce }` is a spring (the default kind;
|
|
23
|
+
springs carry velocity, so a retarget mid-flight stays continuous -
|
|
24
|
+
use them for anything interactive), `{ duration, curve }` is a tween
|
|
25
|
+
(`linear | ease | ease-in | ease-out | ease-in-out` or a cubic-bezier
|
|
26
|
+
array; tweens restart from the current value on retarget, CSS
|
|
27
|
+
semantics). Keys are property names plus `all` as catch-all; a string
|
|
28
|
+
is shorthand (`transition="300ms ease-out"`); `delay` holds each
|
|
29
|
+
write, `from` animates the first attach in (enter), `exit` animates
|
|
30
|
+
removal out before the node frees, `stagger` on a parent cascades its
|
|
31
|
+
children's enters/exits, and `onTransitionEnd` fires per settled
|
|
32
|
+
property. The initial value never animates without `from`; a write to
|
|
33
|
+
a property without a transition snaps, as always. A JS tween loop or
|
|
34
|
+
animation library pushing interpolated values through signals pays the
|
|
35
|
+
whole write path per element per frame - port it to this.
|
|
36
|
+
2. Continuous effects (snow, particles, animated backgrounds) belong in a
|
|
37
|
+
fragment shader: createShaderTexture (from @solidrt/core/gpu) + `<texture
|
|
38
|
+
params={{ uTime }}>` (the shader declares `uniform float uTime;` itself -
|
|
39
|
+
the preamble declares only what the runtime fills). The whole effect then
|
|
40
|
+
costs one setProperty per frame - the uTime write - regardless of visual
|
|
41
|
+
complexity. Shader output
|
|
42
|
+
must be premultiplied alpha (white flakes are `vec4(vec3(a), a)`);
|
|
43
|
+
straight alpha (`vec4(1,1,1,a)`) composites as opaque white. A source that
|
|
44
|
+
starts with `#version 300 es` is compiled exactly as written - no preamble
|
|
45
|
+
is injected, though the built-in vertex stage still supplies `vUV` - so a
|
|
46
|
+
shader ported from elsewhere keeps its own uniform names without dropping
|
|
47
|
+
to compileShader/linkProgram. Params drive any uniform type: a number
|
|
48
|
+
fills a `float`/`int` scalar, a flat number array fills `vec2`/`vec3`/
|
|
49
|
+
`vec4` (2/3/4 numbers) or `mat4` (16, column-major), dispatched by the
|
|
50
|
+
shader's own declaration - a ported shader's `vec2 uCenter` or
|
|
51
|
+
`vec3 iResolution` needs no splitting into scalars. To combine several
|
|
52
|
+
GPU passes, stack `<texture>` elements and set `blendMode` (e.g. a base
|
|
53
|
+
pass plus an additive `blendMode="plus"` pass) rather than writing a
|
|
54
|
+
compositing shader. Within one pipeline draw, createPipelineTexture's
|
|
55
|
+
`blend: "add"` accumulates overlapping geometry additively (soft point
|
|
56
|
+
splats, glow) - pair it with `depthWrite: false` when depth-tested;
|
|
57
|
+
neither option implies the other. A pipeline's own vertex stage writes
|
|
58
|
+
into a y-down clip space: `gl_Position` y = -1 is the top row of the
|
|
59
|
+
target and +1 the bottom, so camera-up geometry must negate y (or fold
|
|
60
|
+
the flip into its projection) or it draws upside down. Sampling is a
|
|
61
|
+
create-time option on every texture: `{ filter: "nearest" }` for
|
|
62
|
+
hard-pixel upscaling (render a small target, display it big - the
|
|
63
|
+
retro/pixel-art path) and `{ wrap: "repeat" }` to tile outside 0..1 in
|
|
64
|
+
shaders; the defaults are linear and clamp, and the choice applies both
|
|
65
|
+
on screen and to shaders sampling the texture.
|
|
66
|
+
3. Reduce setProperty calls wherever possible: one path string rebuilt per
|
|
67
|
+
frame beats N elements with N animated positions; a shader beats the path
|
|
68
|
+
string. get_stats' setPropsPerFrame is the counter to watch. Compiled JSX
|
|
69
|
+
attribute expressions diff before writing, so a per-frame expression that
|
|
70
|
+
returns an unchanged value costs no property write - setPropsPerFrame
|
|
71
|
+
counts values that actually changed, not expressions re-run.
|
|
72
|
+
4. Never leave onFrame registered while nothing animates: a pending onFrame
|
|
73
|
+
is a standing frame request, so the runtime renders and presents every
|
|
74
|
+
vsync even when the callback body does nothing - an invisible 60fps GPU
|
|
75
|
+
burn that also drags the OS compositor along with it. Tweens and
|
|
76
|
+
springs need no pump at all - that is rule 1, and the runtime requests
|
|
77
|
+
frames only while tracks run. For genuinely procedural per-frame motion,
|
|
78
|
+
use a self-rechaining one-shot requestAnimationFrame that stops
|
|
79
|
+
re-requesting when its work list empties. (Registering onFrame outside a
|
|
80
|
+
component body also warns NO_OWNER_CLEANUP - it assumes a reactive owner.)
|
|
81
|
+
5. repaintBoundary works like Flutter's: transforms and opacity on the
|
|
82
|
+
boundary node itself (or any ancestor) are hoisted out of the cache and
|
|
83
|
+
applied at composite time, so animating x/y/scale/rotate/opacity of a
|
|
84
|
+
boundary does NOT re-raster it (verified by A/B measurement - the damage
|
|
85
|
+
system classifies these as Transform and keeps the node's own cache).
|
|
86
|
+
What DOES invalidate the cache is any paint or content change inside the
|
|
87
|
+
subtree - colors, path data, text, a Show toggling - so drive animation
|
|
88
|
+
with transforms and keep the cached content itself static. Off a boundary,
|
|
89
|
+
`opacity` on a view is NOT cheap: it wraps the subtree in a compositing
|
|
90
|
+
layer (save_layer) for as long as it is below 1. To fade a single
|
|
91
|
+
primitive, put the alpha in its `color` (`rgba(...)`) - paint alpha is
|
|
92
|
+
free; reserve view `opacity` for fading a genuine group as a whole.
|
|
93
|
+
Placement rule for animation-heavy screens: a boundary around a node
|
|
94
|
+
that animates its own paint (a moving d-*, a changing color) is useless
|
|
95
|
+
- its interior is damaged every frame, so the cache never survives. The
|
|
96
|
+
win is a boundary around the static bulk NEXT TO the animators: the
|
|
97
|
+
frame then re-records only the moving nodes and replays the fenced
|
|
98
|
+
content as one cached draw, an order-of-magnitude cut when static
|
|
99
|
+
content dominates the node count. get_stats' nodesPainted shows exactly
|
|
100
|
+
what the paint walk still enters. The exception where a boundary on the
|
|
101
|
+
animator itself pays is transform/opacity animation of the boundary
|
|
102
|
+
node - the hoisting described above.
|
|
103
|
+
6. "snapshot" boundaries pay first-frame texture allocation + raster:
|
|
104
|
+
creating many at once (dealing a board of 64 sprites) is a visible
|
|
105
|
+
one-frame hiccup - pool or pre-warm if that moment matters.
|
|
106
|
+
7. Shading pixels the app already drew is a different mechanism from rule 2's
|
|
107
|
+
generated textures, and both forms are a `shader` prop taking a linked
|
|
108
|
+
program from compileShader/linkProgram (@solidrt/core/gpu), not a
|
|
109
|
+
createShaderTexture source. On `<window>`, `shader={{ program, params }}`
|
|
110
|
+
runs the finished frame through the program as the last step before it
|
|
111
|
+
reaches the screen: the frame binds as `uniform sampler2D uSource`,
|
|
112
|
+
`iResolution` fills by name, and `previous: true` retains the last frame as
|
|
113
|
+
`uPrevious` for motion echo or frame differencing. On a `<view>` the same
|
|
114
|
+
prop shades that subtree in place and REQUIRES repaintBoundary="snapshot"
|
|
115
|
+
(without it the shader is ignored with a warning); the pass sees only the
|
|
116
|
+
subtree's own pixels - grading, warping or dissolving the panel works,
|
|
117
|
+
anything needing what is behind it does not - and is split from content
|
|
118
|
+
invalidation, so a params-only change re-runs the pass against the cached
|
|
119
|
+
snapshot instead of re-rasterizing. A window shader's output is invisible
|
|
120
|
+
to get_snapshot and every other MCP tool; `bunx srt render` is the only
|
|
121
|
+
way to see it (see @solidrt/cli AGENTS.md).
|
|
122
|
+
8. `flux:wasm` runs a pure interpreter (wasmi, no JIT), so temper browser
|
|
123
|
+
expectations - but do not write it off for compute. A genuinely numeric
|
|
124
|
+
kernel (typed-array math, tight inner loops, no host calls inside the
|
|
125
|
+
loop) compiled from a systems language can come out a real multiple
|
|
126
|
+
faster than the same loop in interpreted JavaScript, and when profiling
|
|
127
|
+
shows such a kernel is what the app is spending its time on, that
|
|
128
|
+
multiple is worth having: measure the JS loop, port the kernel, measure
|
|
129
|
+
again, keep whichever wins. What wasm does not do is speed up
|
|
130
|
+
render-path work (rules 1-3 are that leverage), and every host call
|
|
131
|
+
costs marshalling, so batch at the boundary - one call over a byte
|
|
132
|
+
buffer, not a call per element. It is also the way to ship one compiled
|
|
133
|
+
module across every target with no native toolchain, and it pairs with
|
|
134
|
+
an isolate when a call runs long enough to block.
|
|
135
|
+
9. `flux:ffi` (dlopen of a native library) is a binding tool, not a
|
|
136
|
+
performance tool. It needs a shared library compiled per platform and
|
|
137
|
+
architecture and shipped under each target's packing rules (Android
|
|
138
|
+
loads only what arrives inside the APK as a lib*.so), so reaching for
|
|
139
|
+
it "to make something fast" buys a build-and-packaging problem on every
|
|
140
|
+
platform the app targets. Use it when the app must call a native
|
|
141
|
+
library that already exists and already ships for those targets; for
|
|
142
|
+
speed, everything above comes first.
|
|
143
|
+
|
|
144
|
+
## Isolates: heavy work off the JS thread
|
|
145
|
+
|
|
146
|
+
A long synchronous computation (a big parse, a simulation step, a blocking
|
|
147
|
+
`flux:ffi`/`flux:wasm` call) freezes rendering and input for its duration.
|
|
148
|
+
Move it into an isolate module: a file whose first statement is the
|
|
149
|
+
`"use isolate"` directive runs in a second runtime on its own thread, and
|
|
150
|
+
main calls its exports as async functions.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// src/worker.ts
|
|
154
|
+
"use isolate"
|
|
155
|
+
export function crunch(data: Uint8Array): number { /* ... */ }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// src/index.tsx
|
|
160
|
+
import { isolate } from "flux:isolate"
|
|
161
|
+
import type * as Worker from "./worker"
|
|
162
|
+
let worker = isolate<typeof Worker>("worker") // id = path from src/, no extension
|
|
163
|
+
let n = await worker.crunch(bytes) // main keeps rendering meanwhile
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The bundler builds each such module as its own bundle and ships it with the
|
|
167
|
+
app (dev pushes and `srt pack` alike). Rules: main may only `import type`
|
|
168
|
+
from an isolate module (a value import is a build error); arguments and
|
|
169
|
+
results are copies (numbers, strings, byte buffers, arrays, plain objects -
|
|
170
|
+
no functions, no class instances); the child has the non-gui `flux:*`
|
|
171
|
+
modules only, so it never touches the render tree; module state persists
|
|
172
|
+
between calls and each `isolate()` call is its own instance. An
|
|
173
|
+
`async function*` export is a stream: `for await (let p of worker.progress())`
|
|
174
|
+
pulls one item per step (progress, ticks, a subscription), `break` ends it in
|
|
175
|
+
the isolate, and streams never block plain calls. Full contract:
|
|
176
|
+
node_modules/@solidrt/flux-types/modules/isolate.d.ts.
|
|
177
|
+
|
|
178
|
+
## Where GPU work stops being free
|
|
179
|
+
|
|
180
|
+
"GPU work is nearly free" is a property of the hardware, not of the engine, and
|
|
181
|
+
the spread is wide enough to design against rather than discover late. The same
|
|
182
|
+
app - two point-cloud pipelines, 233,600 vertices, one params write each per
|
|
183
|
+
onFrame, i.e. exactly what rule 2 recommends - measured 16.7 ms/frame (60 fps,
|
|
184
|
+
vsync-locked) on both desktop and a mid-range 2020 tablet, and 120 ms/frame
|
|
185
|
+
(8.3 fps) on a 2017 Android TV. Roughly 8x for identical work, with the tablet
|
|
186
|
+
indistinguishable from desktop. Measure on a target device if it matters; do
|
|
187
|
+
not infer it from the desktop number.
|
|
188
|
+
|
|
189
|
+
- **On a tiled GPU the budget is primitive count, not pixels.** Every point or
|
|
190
|
+
triangle costs the tiler regardless of how few pixels it covers. On that TV,
|
|
191
|
+
frame time against total vertices with a trivial vertex shader: 20k -> 80 ms,
|
|
192
|
+
35k -> 100 ms, 100k -> 380 ms. Meanwhile `gl_PointSize = 3.0` - nine times
|
|
193
|
+
the fill - measured within one vsync of 1.0, and rendering into a
|
|
194
|
+
quarter-size target measured identical to full size. So for a heavy pass the
|
|
195
|
+
lever is fewer primitives; shrinking the target or the splat usually is not,
|
|
196
|
+
and coverage is far cheaper bought with point size than with more points.
|
|
197
|
+
- **A device's compositor can set the frame budget outright**, in which case
|
|
198
|
+
none of the above moves. That TV never presents faster than every 80 ms -
|
|
199
|
+
four refresh periods blocked inside `eglSwapBuffers` - even for a near-empty
|
|
200
|
+
scene, so its ceiling is ~12 fps whatever you draw. Recognise it by a
|
|
201
|
+
content-independent floor: if a trivial scene and a heavy one present at
|
|
202
|
+
nearly the same rate, you are compositor-bound and tuning the scene is
|
|
203
|
+
wasted effort.
|
|
204
|
+
- **Per-frame writes are gated on the raster thread, not on vsync**, so a pass
|
|
205
|
+
that costs more than a refresh period does not silently pile up. If
|
|
206
|
+
`rasterQueue` climbs across queries while fps drops the raster thread is
|
|
207
|
+
behind; if `fenceTimeoutsPerSec` (in get_stats' window block) is nonzero,
|
|
208
|
+
the GPU is over its pacing budget right now.
|
|
209
|
+
|
|
210
|
+
Finding your own numbers: `get_stats` gives fps, frameMs, setPropsPerFrame,
|
|
211
|
+
the window summary (worst frame, percentiles, GPU rates), rasterQueue and
|
|
212
|
+
fenceTimeouts. When those disagree with what the screen is
|
|
213
|
+
visibly doing, ground truth on Android is
|
|
214
|
+
`adb shell dumpsys SurfaceFlinger --latency <layer>` for real present
|
|
215
|
+
timestamps - engine-reported phase timings can each be honest and still not add
|
|
216
|
+
up to the frame period, because work outside the frame call is not in them.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Core
|
|
2
|
+
|
|
3
|
+
`@solidrt/core` is the spine of SolidRT. It links SolidJS reactivity
|
|
4
|
+
to the native rendertree: an element vocabulary, layout, input, frames, and
|
|
5
|
+
the environment model for adapting to the device you are running on.
|
|
6
|
+
|
|
7
|
+
If you only learn one layer, learn this one. Extensions and tools are built
|
|
8
|
+
on it and are replaceable; Core is the part that changes least (SolidRT is
|
|
9
|
+
in alpha, so "least" is not "never").
|
|
10
|
+
|
|
11
|
+
## Elements
|
|
12
|
+
|
|
13
|
+
There is no DOM. JSX elements are native rendertree nodes, and the
|
|
14
|
+
vocabulary is deliberately small:
|
|
15
|
+
|
|
16
|
+
| Element | Purpose |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `window` | The app window. One per app, the root of the tree. |
|
|
19
|
+
| `view` | Layout and input. Boxes, flex containers, hit targets. |
|
|
20
|
+
| `text`, `span` | A shaped paragraph, and a styled run inside it. |
|
|
21
|
+
| `rect`, `oval`, `line`, `path` | Painted shapes. |
|
|
22
|
+
| `texture` | A GPU texture: a decoded image, a camera frame, a shader target. |
|
|
23
|
+
|
|
24
|
+
Layout and paint are separate jobs, which is the one place the vocabulary
|
|
25
|
+
diverges sharply from HTML. A `view` never paints, so there is no
|
|
26
|
+
`backgroundColor`; you put a `rect` behind the content, and by default a
|
|
27
|
+
shape fills the layout box it sits in:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<view padding={16} alignItems="center">
|
|
31
|
+
<rect color="#1b2440" radius={12} />
|
|
32
|
+
<text color="white">Boxed</text>
|
|
33
|
+
</view>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Each painting element also has a detached twin: `d-view`, `d-rect`,
|
|
37
|
+
`d-path`, `d-text`, and so on. Detached elements are positioned by their
|
|
38
|
+
parent's coordinate system rather than by layout, so changing one costs no
|
|
39
|
+
reflow. Use them for anything that moves at animation frequency.
|
|
40
|
+
|
|
41
|
+
## Reactivity
|
|
42
|
+
|
|
43
|
+
Props are reactive values, not snapshots. A signal read inside JSX
|
|
44
|
+
subscribes exactly one native property to exactly one signal, and an update
|
|
45
|
+
writes that property directly. Nothing re-renders, and there is no virtual
|
|
46
|
+
DOM to diff:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
let [x, setX] = createSignal(0)
|
|
50
|
+
|
|
51
|
+
<d-rect x={x()} w={40} h={40} color="tomato" />
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The reactive and control-flow vocabulary comes from SolidJS 2.0 and is
|
|
55
|
+
re-exported from `@solidrt/core`, so an app imports everything from one
|
|
56
|
+
place: `createSignal`, `createMemo`, `createEffect`, `createStore`,
|
|
57
|
+
`onCleanup`, and the control-flow components `For`, `Show`, `Switch`,
|
|
58
|
+
`Match`, `Loading`, `Errored`.
|
|
59
|
+
|
|
60
|
+
Because props are values rather than accessors, the usual Solid rules apply:
|
|
61
|
+
do not destructure props, and read reactive values inside the expression
|
|
62
|
+
that uses them.
|
|
63
|
+
|
|
64
|
+
An error thrown while computing an element's props or a child expression is
|
|
65
|
+
contained at that element: it keeps its last good value, the error is logged
|
|
66
|
+
once with the node and source line, and the element recovers when the
|
|
67
|
+
expression computes again. An error nothing claims (a throwing effect, an
|
|
68
|
+
error during mount) replaces the app's window with an error window showing
|
|
69
|
+
the message and stack, with a Reset that retries; the reactive system keeps
|
|
70
|
+
running either way. `<Errored>` gives a subtree its own fallback.
|
|
71
|
+
|
|
72
|
+
## Layout
|
|
73
|
+
|
|
74
|
+
Layout is flexbox, plus a line-based subset of CSS grid, over the whole
|
|
75
|
+
element tree. Prop names match CSS: `flexDirection`, `alignItems`,
|
|
76
|
+
`justifyContent`, `gap`, `padding`, `width`, `position`, `top`.
|
|
77
|
+
|
|
78
|
+
Units are simpler than CSS. A bare number is pixels; a percentage is
|
|
79
|
+
`pct(50)`, a branded value rather than a parsed string:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<view flexDirection="row" gap={8} padding={16}>
|
|
83
|
+
<view width={pct(50)} />
|
|
84
|
+
</view>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`position` has `relative` and `absolute` only, and an absolute element does
|
|
88
|
+
not itself become a containing block: it resolves against the nearest
|
|
89
|
+
ancestor with `position="relative"`.
|
|
90
|
+
|
|
91
|
+
## Input
|
|
92
|
+
|
|
93
|
+
Pointer, wheel, and key events are props on any element:
|
|
94
|
+
`onPointerDown`, `onPointerMove`, `onPointerUp`, `onPointerEnter`,
|
|
95
|
+
`onPointerLeave`, `onWheel`, `onKeyDown`, `onKeyUp`. Events travel from the
|
|
96
|
+
hit leaf up to the root, and `stopPropagation()` ends the walk. Key events
|
|
97
|
+
bubble the same way, starting at the focused node - or at the window root
|
|
98
|
+
when nothing is focused, so `onKeyDown` on the window is where app-global
|
|
99
|
+
shortcuts live.
|
|
100
|
+
|
|
101
|
+
Text entry goes to the focused node's `onTextInput`. Focusing a field never
|
|
102
|
+
raises the on-screen keyboard by itself - a tap on the field (or an explicit
|
|
103
|
+
`startTextInput()`) does, and never while a physical keyboard is attached.
|
|
104
|
+
|
|
105
|
+
Coordinates are logical points, so a handler reads the same numbers on a
|
|
106
|
+
high-density phone screen as on a desktop monitor.
|
|
107
|
+
|
|
108
|
+
## Frames and animation
|
|
109
|
+
|
|
110
|
+
`onFrame(callback)` runs before every painted frame with the frame time in
|
|
111
|
+
ms, the frame count, and the display refresh rate; it returns a disposer and
|
|
112
|
+
cleans itself up with the reactive scope it was called in. Rendering is
|
|
113
|
+
demand-driven: the runtime does not spin a render loop when nothing changed,
|
|
114
|
+
so an idle app is genuinely idle.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
let [t, setT] = createSignal(0)
|
|
118
|
+
onFrame((tick) => setT(tick))
|
|
119
|
+
|
|
120
|
+
<d-view rotate={t() / 1000}>...</d-view>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Environment and devices
|
|
124
|
+
|
|
125
|
+
`env` and `capabilities` describe where the app is running: `env` is what
|
|
126
|
+
is observed (system theme, text scale, orientation, visibility, connected
|
|
127
|
+
input devices), `capabilities` what follows from it for behavior (hover,
|
|
128
|
+
touch, precise pointer, keyboard navigation, window size class). Which
|
|
129
|
+
runtime features exist on this build is `Flux.capabilities`, by name, never
|
|
130
|
+
by guessing from the OS.
|
|
131
|
+
|
|
132
|
+
Window-shaped values are reactive too: `windowSize()`, `safeArea()`,
|
|
133
|
+
`displayScale()`, `keyboardHeight()`, `windowFocused()`.
|
|
134
|
+
|
|
135
|
+
Device access follows the same reactive shape, as `create*` primitives
|
|
136
|
+
imported from Core subpaths rather than an imperative API:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { createCamera } from "@solidrt/core/camera"
|
|
140
|
+
|
|
141
|
+
let camera = createCamera()
|
|
142
|
+
|
|
143
|
+
<texture src={camera.texture()} fit="cover" />
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The same pattern covers `@solidrt/core/microphone`, `/sound`,
|
|
147
|
+
`/speech-recognition`, `/text-input`, `/image`, `/color`, and `/gpu`.
|
|
148
|
+
|
|
149
|
+
## Reference
|
|
150
|
+
|
|
151
|
+
The [reference](/core/reference/) covers the API by subject: the element
|
|
152
|
+
vocabulary, drawing, text, detached elements, layout, transforms, input,
|
|
153
|
+
shaders, the GPU module, and the shared types. It shows the shipped declarations themselves,
|
|
154
|
+
so it says exactly what your editor says.
|