@solidrt/core 0.0.54 → 0.0.56
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 +100 -22
- package/agents/painting.md +36 -12
- package/agents/performance.md +36 -6
- package/docs/reference/detached.md +20 -10
- package/docs/reference/drawing.md +10 -3
- package/docs/reference/elements.md +3 -1
- package/docs/reference/gpu.md +12 -1
- package/docs/reference/layout.md +28 -0
- package/docs/reference/text.md +34 -0
- package/examples/README.md +5 -0
- package/examples/detached-positioning.tsx +3 -1
- package/examples/line-points.tsx +21 -0
- package/examples/stagger.tsx +45 -0
- package/examples/text-flow.tsx +90 -0
- package/examples/text-glyphs.tsx +73 -0
- package/examples/view-design-size.tsx +5 -0
- package/package.json +6 -5
- package/src/color.ts +17 -0
- package/src/core.ts +28 -7
- package/src/environment.ts +87 -69
- package/src/gamepad.ts +10 -8
- package/src/gpu.ts +130 -21
- package/src/index.ts +14 -4
- package/src/pan.ts +20 -5
- package/src/renderer.ts +16 -2
- package/src/runtime-modules.d.ts +7 -7
- package/src/svg.ts +3 -2
- package/src/text-input.ts +43 -8
- package/src/transform.ts +52 -23
- package/src/types.d.ts +158 -44
- 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
|
|
@@ -165,10 +165,19 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
165
165
|
LayoutProps`, so it draws AND is laid out by Taffy. The detached variant
|
|
166
166
|
(`d-rect`) is `RectProps` only - it draws but is NOT in the layout pass; you
|
|
167
167
|
place it yourself with `x`/`y` (omit them and it fills the parent, which is
|
|
168
|
-
how backgrounds work).
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
how backgrounds work). Laid-out elements are the DEFAULT for building a
|
|
169
|
+
screen: structure, cards, panels, labels - anything flex/gap/padding
|
|
170
|
+
should place - is `<view>`/`<text>` and plain primitives. Reach for `d-`
|
|
171
|
+
only with a concrete reason: a background/underlay, free-form drawn
|
|
172
|
+
geometry (a chart's marks, a diagram, vector art), an overlay pinned at
|
|
173
|
+
coordinates, or as a performance lever for many directly-positioned,
|
|
174
|
+
often-animating elements (e.g. hundreds of balls), where `d-` skips the
|
|
175
|
+
per-element layout plain elements would incur. Hand-positioning what flex
|
|
176
|
+
would place - a card built from d-rects and d-texts at computed offsets -
|
|
177
|
+
forfeits wrapping, sizing and resize behavior for nothing: inside a chart
|
|
178
|
+
the marks and axis labels are d-*, but the card around it is a `<view>`.
|
|
179
|
+
A `designSize` view is NOT a reason to go detached - laid-out children
|
|
180
|
+
resolve in design units too.
|
|
172
181
|
|
|
173
182
|
- Transform origin on a `d-view`: with `originX`/`originY` unset, scale/rotate
|
|
174
183
|
pivot at the view's local (0,0) - the origin its children's coordinates are
|
|
@@ -179,6 +188,22 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
179
188
|
Avoid pct()/keyword origins on a d-view - they resolve against the box
|
|
180
189
|
inherited from the nearest laid-out ancestor.
|
|
181
190
|
|
|
191
|
+
- Transforms live on views only. A draw primitive (`d-rect`, `d-text`,
|
|
192
|
+
`d-path`, ...) paints in its parent's space and has no `rotate`, `scale`
|
|
193
|
+
or origin of its own; to spin or scale one in place, wrap it in a `d-view`
|
|
194
|
+
carrying the transform, with the origin set in pixels as above. So a
|
|
195
|
+
rotating glyph is two nodes, the view at the pivot and the text placed
|
|
196
|
+
around it - `<d-view x={cx} y={cy} rotate={r}><d-text anchor="middle"
|
|
197
|
+
y={-h / 2}>{ch}</d-text></d-view>`.
|
|
198
|
+
|
|
199
|
+
- A `d-text` label is anchored, not boxed: `anchor="start" | "middle" |
|
|
200
|
+
"end"` makes `x` a point on the line (SVG's text-anchor), the text shapes
|
|
201
|
+
at its natural width (no wrap) and its lines align to the anchor's side.
|
|
202
|
+
Without `anchor` a d-text is a paragraph box wrapping at `w` or at the
|
|
203
|
+
inherited box, and `textAlign` aligns inside that width - so
|
|
204
|
+
`textAlign="right"` on a `w`-less d-text aligns to the ancestor's right
|
|
205
|
+
edge, not to `x`. Reach for `anchor` on chart, axis and diagram labels.
|
|
206
|
+
|
|
182
207
|
- Layout-affecting vs not (this matters for per-frame work). Props fall in three
|
|
183
208
|
buckets, split by where they take effect:
|
|
184
209
|
- `LayoutProps` - width/height, min/max sizes, margin, padding, `position` and
|
|
@@ -197,6 +222,22 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
197
222
|
`position:absolute` at `left:0,top:0`, or just let normal flow place it) and
|
|
198
223
|
then translate it with `x`/`y`.
|
|
199
224
|
|
|
225
|
+
- Measuring layout: never re-derive an element's position with your own
|
|
226
|
+
arithmetic - the layout engine already knows, so ask it.
|
|
227
|
+
`getBoundingBox(ref)` returns the node's laid-out box
|
|
228
|
+
({x, y, width, height}, transforms composed, relative to the nearest
|
|
229
|
+
`position="relative"` ancestor, else the window);
|
|
230
|
+
`getBoundingBoxViewport(ref)` is always window-relative (the space pointer
|
|
231
|
+
`clientX`/`clientY` report in). Both are snapshot reads of the last
|
|
232
|
+
computed layout, not reactive - read them in `onLayout(fn)` (fires after
|
|
233
|
+
layout, before paint) or in an event handler. The canonical shape: let
|
|
234
|
+
flex place the boxes, measure them in onLayout, and drive DETACHED drawing
|
|
235
|
+
from the result - a d-path connector between two cards, a badge pinned to
|
|
236
|
+
a bar. Two rules keep it one pass: write the measurement only to props
|
|
237
|
+
that do not affect layout (a `d` string, a d-* position, a transform), and
|
|
238
|
+
call `flush()` after the write so it lands in this frame's display list.
|
|
239
|
+
See examples/on-layout-connect.tsx.
|
|
240
|
+
|
|
200
241
|
- Text `lineHeight` is a MULTIPLIER of fontSize (1.3-1.6 is typical), not
|
|
201
242
|
pixels. A CSS-reflex value like 22 makes each line box 22x the font size:
|
|
202
243
|
the text becomes blank space and the parent balloons.
|
|
@@ -218,6 +259,23 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
218
259
|
inline atom flowing with the words as one unbreakable box on the baseline;
|
|
219
260
|
give it margins for spacing, since JSX trims the whitespace around it.
|
|
220
261
|
|
|
262
|
+
- App-side line breaking, for text a `<text>` box cannot express: text
|
|
263
|
+
poured into a shape, parting around an obstacle that is not in the flow,
|
|
264
|
+
continued across columns, fitted to a box by size, or placed glyph by
|
|
265
|
+
glyph. `prepareText(text, font)` shapes the words once (through the
|
|
266
|
+
shared word cache) into `units` carrying advance, ink width, ascent and
|
|
267
|
+
descent; `layoutNextLine(prepared, cursor, width)` is the greedy breaker,
|
|
268
|
+
returning one line and the `cursor` for the next, at whatever width you
|
|
269
|
+
hand it per call (a chord of a circle, a slot beside an obstacle, the next
|
|
270
|
+
column). Draw each line as a `<d-text>` of exactly its text with the same
|
|
271
|
+
font. Re-breaking every frame is arithmetic plus cache hits, so a moving
|
|
272
|
+
or breathing shape re-flows at frame rate; only the first shaping cost
|
|
273
|
+
anything. `carets: true` adds each unit's per-grapheme x positions - the
|
|
274
|
+
ONLY kerned per-glyph source; per-character `measureText` drops kerning
|
|
275
|
+
and visibly drifts on pairs like AV/TA. Styled runs come from `runs` (draw
|
|
276
|
+
such a line with a `<span>` per run). See examples/text-flow.tsx and
|
|
277
|
+
examples/text-glyphs.tsx.
|
|
278
|
+
|
|
221
279
|
- Events: there is NO `onClick`/`onPress`. A "button" is a `<view>`/`<rect>`
|
|
222
280
|
with `onPointerDown`. Handlers: onPointerDown/Up/Move/Enter/Leave, onWheel,
|
|
223
281
|
onKeyDown/Up, onTextInput, onFocus/onBlur. Text entry: focus a node with an
|
|
@@ -236,7 +294,12 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
236
294
|
- Gesture recognizers, shared by every package: `createPan` (single-pointer
|
|
237
295
|
drag, axis-aware slop, per-event dx/dy) and `createTransform` (merged
|
|
238
296
|
pan + pinch + rotate over the whole pointer set, Flutter-Scale style: streams
|
|
239
|
-
`{ dx, dy, scale, rotation, x, y, pointers }` once per FRAME
|
|
297
|
+
`{ dx, dy, scale, rotation, x, y, pointers }` once per FRAME. Frames, both
|
|
298
|
+
recognizers: slop and span gates are finger travel and measure in window
|
|
299
|
+
pixels; dx/dy are measured in the handler node's PARENT frame (the frame
|
|
300
|
+
its own x/y live in) so they apply 1:1 to the node or to content inside
|
|
301
|
+
it under any scaled ancestor (a designSize fit); the transform's focal
|
|
302
|
+
x/y is in the node's LOCAL frame, the zoom-about anchor. Positions
|
|
240
303
|
update per event, but the cross-pointer measure waits for the
|
|
241
304
|
`pointerFrame` batch terminator, when every pointer is the same age; one
|
|
242
305
|
finger degrades to a plain pan, and `pointers` is how a consumer gives one-
|
|
@@ -259,13 +322,14 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
259
322
|
inside JSX, a `createMemo`, or an effect's compute phase) reads it
|
|
260
323
|
untracked: it silently freezes at the initial value.
|
|
261
324
|
Writing a signal or store from inside an owned scope (a component body, a
|
|
262
|
-
`createMemo`, an effect's compute phase) throws
|
|
263
|
-
`REACTIVE_WRITE_IN_OWNED_SCOPE` in dev; a loader
|
|
264
|
-
body
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
325
|
+
`createMemo`, an effect's compute phase, a `ref` callback) throws
|
|
326
|
+
`REACTIVE_WRITE_IN_OWNED_SCOPE` in dev; a loader kicked off in the
|
|
327
|
+
component body or a ref that reports progress through a signal is the
|
|
328
|
+
classic reflex that hits this. `untrack` does NOT exempt a write (the
|
|
329
|
+
guard is owner-based, and untrack only stops tracking). Move the write
|
|
330
|
+
into an event handler, an effect's apply phase, or `onSettled`; opt in
|
|
331
|
+
narrowly with `createSignal(v, { ownedWrite: true })` for a signal that
|
|
332
|
+
genuinely is internal state.
|
|
269
333
|
Signal writes flush on a microtask: a handler that sets a signal and
|
|
270
334
|
immediately reads it back gets the OLD value. Read it in an effect, or
|
|
271
335
|
call `flush()` (from @solidjs/signals) to force it through.
|
|
@@ -286,9 +350,19 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
286
350
|
session in seconds) and it is what keeps per-frame work cheap.
|
|
287
351
|
- Animation is target-shaped first: declare `transition` on the element and
|
|
288
352
|
write targets, and the runtime animates natively with no per-frame JS.
|
|
353
|
+
Enter and exit are per-property entries on that same spec: `from` is the
|
|
354
|
+
value the property animates in from at first attach, `exit` the value it
|
|
355
|
+
animates to on removal (the node stays painted until it settles, then
|
|
356
|
+
frees - no AnimatePresence equivalent needed). `stagger` (ms) goes on an
|
|
357
|
+
ANCESTOR, never on the animating elements: it delays each descendant's
|
|
358
|
+
enter/exit by `index * stagger`, and cascades nothing unless the
|
|
359
|
+
descendants declare `from`/`exit`. An enter plays once per mount; to
|
|
360
|
+
replay it, remount the subtree (`<Show when={epoch()} keyed>` around it,
|
|
361
|
+
then bump `epoch`). See examples/stagger.tsx.
|
|
289
362
|
Reach for per-frame work only for genuinely procedural motion:
|
|
290
363
|
`onFrame((tick, frame, rate) => {})` is the native hook (runtime-paced,
|
|
291
|
-
returns a cleanup, auto-cleaned inside
|
|
364
|
+
returns a cleanup, auto-cleaned inside an owned scope; from an effect's
|
|
365
|
+
apply, return it so the loop runs only while the condition holds); `rate` is the
|
|
292
366
|
display's nominal refresh rate in Hz, which a fixed-timestep loop needs
|
|
293
367
|
(see @solidrt/cli agents/debugging.md on stepping); `requestAnimationFrame`
|
|
294
368
|
exists as a web-standard one-shot but is not the preferred driver. A JS
|
|
@@ -298,10 +372,11 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
298
372
|
Window state: onResize, onLayout, onWindowFocus, onWindowBlur exist, but
|
|
299
373
|
prefer the reactive reads (`env`/`capabilities` above, or the accessors
|
|
300
374
|
`windowSize()`, `safeArea()`, `displayScale()`, `windowFocused()`,
|
|
301
|
-
`keyboardHeight()`, `pointerLocked()`) for reading
|
|
302
|
-
|
|
303
|
-
(
|
|
304
|
-
reporting motion
|
|
375
|
+
`keyboardHeight()`, `pointerLocked()`) for reading window state;
|
|
376
|
+
onLayout's real job is the measure-then-draw pattern above. For mouse
|
|
377
|
+
look, `lockPointer(true)` enters relative mouse mode (cursor hidden and
|
|
378
|
+
confined, positions freeze) and pointer events keep reporting motion
|
|
379
|
+
through `movementX`/`movementY`.
|
|
305
380
|
|
|
306
381
|
- `createPortal` cannot mount during the app's initial render (it throws
|
|
307
382
|
"no mount target"): gate portal content behind a signal that starts false
|
|
@@ -313,10 +388,13 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
313
388
|
|
|
314
389
|
- Device/GPU access via subpath imports: @solidrt/core/camera, /microphone,
|
|
315
390
|
/speech, /gpu. Image flow: `decodeImage(bytes)` ->
|
|
316
|
-
`createTexture(data,w,h)` -> `<texture src={id} />`.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
391
|
+
`createTexture(data,w,h)` -> `<texture src={id} />`. Sampling (`filter`,
|
|
392
|
+
`wrap`, `mipmap`) is fixed at creation on every create* helper and follows
|
|
393
|
+
the id everywhere; agents/performance.md rule 2 says when to set which.
|
|
394
|
+
Pixels are premultiplied alpha from decode onward (the GPU contract);
|
|
395
|
+
`decodeImage(bytes, { alpha: "straight" })` keeps the file's color under
|
|
396
|
+
transparent pixels for CPU work, and `encodeImage` converts back to
|
|
397
|
+
straight for the file.
|
|
320
398
|
|
|
321
399
|
## Minimal app, core primitives only (verified to render)
|
|
322
400
|
|
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
|
|
@@ -38,17 +41,32 @@ no matter how complex the effect, which is why the performance model
|
|
|
38
41
|
|
|
39
42
|
## Web reflexes and what replaces them
|
|
40
43
|
|
|
44
|
+
- `color-mix()`, `oklch()`, `lab()` -> not in the color grammar (hex,
|
|
45
|
+
`rgb()`, `hsl()`, `hwb()`, named colors only; the prop throws `Invalid
|
|
46
|
+
color` otherwise). Mix in JS with `mixColors(a, b, t)` from @solidrt/core
|
|
47
|
+
(oklab, so a ramp between two colors stays perceptually even) and pass
|
|
48
|
+
the result; `withAlpha(color, a)` gives any color at an opacity (do not
|
|
49
|
+
rebuild `rgba()` strings by hand from a hex); `brightness(color)` picks
|
|
50
|
+
readable text over any fill.
|
|
41
51
|
- gradient background -> a gradient `color` on a `d-rect` (gradients are
|
|
42
52
|
paint values, usable anywhere a color is)
|
|
43
|
-
- `filter: blur/grayscale/hue-rotate
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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)
|
|
52
70
|
- CSS `transition` -> the `transition` prop: declare it on the element and
|
|
53
71
|
keep writing targets; the runtime animates natively (performance rule 1)
|
|
54
72
|
- `@keyframes` -> a `transition` prop when the motion is target-shaped;
|
|
@@ -56,6 +74,12 @@ no matter how complex the effect, which is why the performance model
|
|
|
56
74
|
uniform when the animation is continuous and visual
|
|
57
75
|
- `<canvas>` 2D -> `d-*` primitives (rebuild one `d-path` string per frame
|
|
58
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
|
|
59
83
|
- `<canvas>` WebGL, three.js -> `createPipelineTexture`, or `@solidrt/3d`
|
|
60
84
|
- video background, animated hero, particle field -> a shader texture; this
|
|
61
85
|
is the case the runtime is built for
|
package/agents/performance.md
CHANGED
|
@@ -60,9 +60,21 @@ Rules, in order of leverage:
|
|
|
60
60
|
the flip into its projection) or it draws upside down. Sampling is a
|
|
61
61
|
create-time option on every texture: `{ filter: "nearest" }` for
|
|
62
62
|
hard-pixel upscaling (render a small target, display it big - the
|
|
63
|
-
retro/pixel-art path)
|
|
64
|
-
shaders
|
|
65
|
-
|
|
63
|
+
retro/pixel-art path), `{ wrap: "repeat" }` to tile outside 0..1 in
|
|
64
|
+
shaders, and `{ mipmap: true }` wherever a shader samples the texture
|
|
65
|
+
smaller than it is (a 3d surface receding into the distance, a target
|
|
66
|
+
read at a fraction of its size): without a mip chain minification skips
|
|
67
|
+
texels and aliases. The chain is regenerated by the runtime after every
|
|
68
|
+
upload and every target render, nothing to schedule, but it is one GPU
|
|
69
|
+
pass each time, so a per-frame texture pays it per frame. Add
|
|
70
|
+
`anisotropy: 8` (any level; the device clamps) beside `mipmap: true` on a
|
|
71
|
+
tiled surface seen at a grazing angle - a road or floor receding to the
|
|
72
|
+
horizon - where trilinear alone smears the far half into the mip its
|
|
73
|
+
long axis chose; it does nothing without the chain. The defaults
|
|
74
|
+
are linear, clamp, no mips and anisotropy 1; the choice is fixed at creation (a change
|
|
75
|
+
means a new id) and applies both on screen and to shaders sampling the
|
|
76
|
+
texture, except that the `<texture>` display draw samples the full-size
|
|
77
|
+
level only, so mips never help a texture merely shown smaller.
|
|
66
78
|
3. Reduce setProperty calls wherever possible: one path string rebuilt per
|
|
67
79
|
frame beats N elements with N animated positions; a shader beats the path
|
|
68
80
|
string. get_stats' setPropsPerFrame is the counter to watch. Compiled JSX
|
|
@@ -76,8 +88,11 @@ Rules, in order of leverage:
|
|
|
76
88
|
springs need no pump at all - that is rule 1, and the runtime requests
|
|
77
89
|
frames only while tracks run. For genuinely procedural per-frame motion,
|
|
78
90
|
use a self-rechaining one-shot requestAnimationFrame that stops
|
|
79
|
-
re-requesting when its work list empties.
|
|
80
|
-
|
|
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.
|
|
81
96
|
5. repaintBoundary works like Flutter's: transforms and opacity on the
|
|
82
97
|
boundary node itself (or any ancestor) are hoisted out of the cache and
|
|
83
98
|
applied at composite time, so animating x/y/scale/rotate/opacity of a
|
|
@@ -102,7 +117,15 @@ Rules, in order of leverage:
|
|
|
102
117
|
node - the hoisting described above.
|
|
103
118
|
6. "snapshot" boundaries pay first-frame texture allocation + raster:
|
|
104
119
|
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.
|
|
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.
|
|
106
129
|
7. Shading pixels the app already drew is a different mechanism from rule 2's
|
|
107
130
|
generated textures, and both forms are a `shader` prop taking a linked
|
|
108
131
|
program from compileShader/linkProgram (@solidrt/core/gpu), not a
|
|
@@ -140,6 +163,13 @@ Rules, in order of leverage:
|
|
|
140
163
|
platform the app targets. Use it when the app must call a native
|
|
141
164
|
library that already exists and already ships for those targets; for
|
|
142
165
|
speed, everything above comes first.
|
|
166
|
+
10. Text costs shaping, once per word per style, through a shared word cache;
|
|
167
|
+
line breaking is arithmetic over the shaped words. So re-breaking text
|
|
168
|
+
every frame with prepareText + layoutNextLine (a shape that breathes, an
|
|
169
|
+
obstacle that moves, an editor on every keystroke) is cheap - the lines'
|
|
170
|
+
d-texts hit the same cache - while changing a text's font, size or
|
|
171
|
+
weight re-shapes it. Animate text with transforms and paint (rule 5), or
|
|
172
|
+
by re-breaking; not by resizing it per frame.
|
|
143
173
|
|
|
144
174
|
## Isolates: heavy work off the JS thread
|
|
145
175
|
|
|
@@ -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
|
|
|
@@ -49,7 +52,10 @@ to detach from.
|
|
|
49
52
|
|
|
50
53
|
## Geometry
|
|
51
54
|
|
|
52
|
-
Detached geometry is paint-space pixels and never affects layout.
|
|
55
|
+
Detached geometry is paint-space pixels and never affects layout. The
|
|
56
|
+
reverse holds too: a laid-out tile holding only detached geometry must not
|
|
57
|
+
be allowed to flex-shrink (wrap the row, or give it `flexShrink={0}`),
|
|
58
|
+
since the box shrinks and the geometry does not.
|
|
53
59
|
|
|
54
60
|
{{ decl packages/core/src/types.d.ts PositionProps }}
|
|
55
61
|
|
|
@@ -62,15 +68,19 @@ ancestor, so a `d-rect` with only `x`/`y` still has something to draw:
|
|
|
62
68
|
|
|
63
69
|
{{ decl packages/core/src/types.d.ts OvalGeometryProps }}
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
`d-text` is boxed or anchored. Boxed, it wraps at `w` or at the inherited
|
|
72
|
+
box, and `textAlign` aligns lines inside that width. Anchored, `x` is a point
|
|
73
|
+
on the line (SVG's `text-anchor`) and the text does not wrap unless given a
|
|
74
|
+
`w`; that is the form for labels. Its reported bounds are the laid-out
|
|
75
|
+
paragraph, with `w`/`h` overriding a side each:
|
|
67
76
|
|
|
68
77
|
{{ decl packages/core/src/types.d.ts TextGeometryProps }}
|
|
69
78
|
|
|
70
79
|
`d-path` takes only a position, since its size is whatever its `d` string
|
|
71
80
|
draws. `d-line` has no width and height either: its geometry is its two
|
|
72
|
-
endpoints (or, as a polyline, its `points`),
|
|
73
|
-
primitive to reach for when the geometry moves.
|
|
81
|
+
endpoints (or, as a polyline, its `points`), offset as a whole by `x`/`y`,
|
|
82
|
+
which is what makes it the primitive to reach for when the geometry moves.
|
|
83
|
+
Both report their bounds
|
|
74
84
|
(getBoundingBox, the tree, a capture) as that geometry plus the stroke, not
|
|
75
85
|
the inherited box:
|
|
76
86
|
|
|
@@ -27,12 +27,13 @@ stroke rather than a box.
|
|
|
27
27
|
|
|
28
28
|
## Dashing
|
|
29
29
|
|
|
30
|
-
A stroke's dash pattern, on
|
|
30
|
+
A stroke's dash pattern, on every stroked primitive:
|
|
31
31
|
|
|
32
32
|
{{ decl packages/core/src/types.d.ts DashProps }}
|
|
33
33
|
|
|
34
34
|
The pattern is walked along the geometry itself: through a polyline's
|
|
35
|
-
vertices
|
|
35
|
+
vertices, along a path's curves (restarting at each subpath), and around a
|
|
36
|
+
rect's or oval's inset outline (inside the box, like the solid stroke).
|
|
36
37
|
`dashOffset` slides it - write it every frame for marching ants, or
|
|
37
38
|
transition it for a one-shot slide. A dashed stroke keeps its caps on every
|
|
38
39
|
dash, and a stroke-and-fill path dashes only the stroke.
|
|
@@ -47,10 +48,16 @@ the length).
|
|
|
47
48
|
|
|
48
49
|
{{ decl packages/core/src/types.d.ts RectProps }}
|
|
49
50
|
|
|
51
|
+
A dashed rect (a selection marquee, a drop zone) is the dash props on the
|
|
52
|
+
rect itself; see Dashing above. The pattern starts on the top edge after
|
|
53
|
+
the top-left corner and runs clockwise.
|
|
54
|
+
|
|
50
55
|
## oval
|
|
51
56
|
|
|
52
57
|
{{ decl packages/core/src/types.d.ts OvalProps }}
|
|
53
58
|
|
|
59
|
+
Dashes like a rect; the pattern starts at 3 o'clock and runs clockwise.
|
|
60
|
+
|
|
54
61
|
## line
|
|
55
62
|
|
|
56
63
|
{{ decl packages/core/src/types.d.ts LineProps }}
|
|
@@ -84,7 +91,7 @@ for every pixel source in the engine.
|
|
|
84
91
|
## Logo
|
|
85
92
|
|
|
86
93
|
The SolidRT brand mark, the same seven-segment puzzle the scaffold's welcome
|
|
87
|
-
screen and the
|
|
94
|
+
screen and the player draw, as a component: a square `view` of `size`
|
|
88
95
|
pixels with the segments as gradient-filled `d-path`s.
|
|
89
96
|
|
|
90
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/docs/reference/text.md
CHANGED
|
@@ -44,3 +44,37 @@ Inline only: its children are text and other spans, and it has no layout box,
|
|
|
44
44
|
which is why it is the one element with no detached form. Pointer handlers on
|
|
45
45
|
a span fire for the boxes its text occupies on each line it spans, and bubble
|
|
46
46
|
to the enclosing spans and the text.
|
|
47
|
+
|
|
48
|
+
## Line breaking in app code
|
|
49
|
+
|
|
50
|
+
A `<text>` wraps in a box. For anything else - text poured into a shape,
|
|
51
|
+
parting around an obstacle that is not in the flow, continued across columns,
|
|
52
|
+
fitted to a box by size, or placed glyph by glyph - the app does the breaking
|
|
53
|
+
itself over words the engine shaped once:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
let prepared = prepareText(article, { fontSize: 16, lineHeight: 1.4 })
|
|
57
|
+
let cursor = 0
|
|
58
|
+
for (let band of bands) { // any widths, in any order
|
|
59
|
+
let line = layoutNextLine(prepared, cursor, band.w)
|
|
60
|
+
if (!line) break
|
|
61
|
+
lines.push({ ...band, w: line.width, text: prepared.text.slice(line.start, line.end) })
|
|
62
|
+
cursor = line.cursor // the next box continues here
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`prepareText` returns the paragraph's wrap units (a word and its trailing
|
|
67
|
+
whitespace) with their advance, ink width, ascent and descent. `layoutNextLine`
|
|
68
|
+
is a greedy breaker over those numbers: it fills one line to the width you hand
|
|
69
|
+
it and returns the cursor for the next, so a circle is a stack of chords, a
|
|
70
|
+
column is the same loop with a new box, and a headline that must fit is a loop
|
|
71
|
+
over sizes. Each line is drawn as a `<d-text>` of exactly its own text in the
|
|
72
|
+
same font.
|
|
73
|
+
|
|
74
|
+
The split is what makes it usable: shaping happened once, through the shared
|
|
75
|
+
word cache the drawn lines hit again, so re-breaking three paragraphs every
|
|
76
|
+
frame as a shape moves costs arithmetic, not layout. `carets: true` adds each
|
|
77
|
+
unit's per-grapheme x positions from that same shaping - the kerned source for
|
|
78
|
+
per-glyph placement and animation, which measuring characters one at a time
|
|
79
|
+
cannot give. `runs` restyles ranges, with a unit crossing a run boundary coming
|
|
80
|
+
back as glued pieces that always land on one line.
|
package/examples/README.md
CHANGED
|
@@ -13,9 +13,14 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
13
13
|
- `detached-positioning.tsx` - the `d-` prefix: x/y placement, no reflow, detached-only children.
|
|
14
14
|
- `text-paint-styling.tsx` - the uniform `color` prop; `drawStyle="stroke"` vs fill.
|
|
15
15
|
|
|
16
|
+
## Text
|
|
17
|
+
- `text-flow.tsx` - app-side line breaking: `prepareText` shapes a paragraph's words once, `layoutNextLine` breaks one line per call at any width. Poured band by band into a circle (each band the chord at its height), then the cursor continues in a column beside it; every line is a `d-text` of its own text. Re-breaks fully on resize for arithmetic plus word-cache hits.
|
|
18
|
+
- `text-glyphs.tsx` - per-glyph positions: `prepareText(text, { carets: true })` reports each unit's kerned grapheme x positions from the shaping that is drawn. One `d-text` per glyph placed from carets sits exactly on the whole headline; the same glyphs placed by per-character `measureText` drift on every kerning pair.
|
|
19
|
+
|
|
16
20
|
## Frame and lifecycle
|
|
17
21
|
- `frame-animation.tsx` - `onFrame` driving a transform animation each frame.
|
|
18
22
|
- `on-layout-connect.tsx` - `onLayout` + `getBoundingBox` connecting laid-out boxes with a `d-path`.
|
|
23
|
+
- `stagger.tsx` - enter/exit/stagger with no per-frame JS: `from` and `exit` per property on each row's own `transition`, `stagger` on the ANCESTOR cascading them in occurrence order, and the replay idiom - a keyed `<Show>` remounts the subtree when its `when` value changes, so bumping an epoch replays the whole cascade. Exiting rows stay painted until they settle.
|
|
19
24
|
|
|
20
25
|
## Pointer input
|
|
21
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.
|
|
@@ -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"
|
package/examples/line-points.tsx
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
// knowing its length.
|
|
20
20
|
// 5. A laid-out <line points>: the points are content (like a path's `d`), so
|
|
21
21
|
// the box measures from their extent and takes part in the row.
|
|
22
|
+
// 6. Dashed boxes: the same props on d-rect and d-oval dash the inset
|
|
23
|
+
// outline the solid stroke draws, so a marquee or a drop zone is one
|
|
24
|
+
// element and its dashes never leave the box.
|
|
22
25
|
// The two-endpoint form (x1..y2, on d-line only) is unchanged; while `points`
|
|
23
26
|
// is set it takes precedence over the endpoints.
|
|
24
27
|
import { render, onFrame, createSignal } from "@solidrt/core"
|
|
@@ -115,6 +118,24 @@ function App() {
|
|
|
115
118
|
</view>
|
|
116
119
|
</view>
|
|
117
120
|
|
|
121
|
+
<text fontSize={16} color="#8b949e">
|
|
122
|
+
dashed boxes: a rounded marquee, a stroke-and-fill rect, an oval drawing on
|
|
123
|
+
</text>
|
|
124
|
+
<view flexDirection="row" flexWrap="wrap" gap={20}>
|
|
125
|
+
<view width={140} height={120}>
|
|
126
|
+
<d-rect radius={8} color="#151b28" />
|
|
127
|
+
<d-rect x={20} y={20} w={100} h={80} radius={12} drawStyle="stroke" onLength={8} offLength={6} dashOffset={-ants()} color="#e3b341" strokeWidth={3} />
|
|
128
|
+
</view>
|
|
129
|
+
<view width={140} height={120}>
|
|
130
|
+
<d-rect radius={8} color="#151b28" />
|
|
131
|
+
<d-rect x={20} y={20} w={100} h={80} drawStyle="stroke-and-fill" onLength={10} offLength={6} color="#f8514966" strokeWidth={6} strokeCap="round" />
|
|
132
|
+
</view>
|
|
133
|
+
<view width={140} height={120}>
|
|
134
|
+
<d-rect radius={8} color="#151b28" />
|
|
135
|
+
<d-oval x={20} y={20} w={100} h={80} drawStyle="stroke" pathLength={1} onLength={drawn()} offLength={1} color="#3fb950" strokeWidth={4} strokeCap="round" />
|
|
136
|
+
</view>
|
|
137
|
+
</view>
|
|
138
|
+
|
|
118
139
|
<text fontSize={16} color="#8b949e">
|
|
119
140
|
partial draw: pathLength=1 makes the pattern fractional, 77% of the curve and a triangle drawing on
|
|
120
141
|
</text>
|