@solidrt/core 0.0.57 → 0.0.59
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 +42 -7
- package/docs/index.md +23 -0
- package/examples/README.md +1 -0
- package/examples/gpu-draw-list.tsx +7 -7
- package/examples/gpu-instancing.tsx +15 -11
- package/examples/gpu-particles.tsx +8 -4
- package/examples/gpu-pipeline.tsx +8 -4
- package/examples/gpu-shared-params.tsx +6 -6
- package/examples/gpu-sprites.tsx +11 -7
- package/examples/layout-slide.tsx +78 -0
- package/examples/stagger.tsx +35 -19
- package/package.json +5 -5
- package/src/environment.ts +34 -4
- package/src/gpu.ts +25 -35
- package/src/index.ts +4 -4
- package/src/input-chord.ts +44 -0
- package/src/input-keyboard.ts +10 -21
- package/src/input-map.ts +2 -1
- package/src/input-pointer.ts +153 -60
- package/src/renderer.ts +17 -2
- package/src/runtime-modules.d.ts +11 -4
- package/src/transform.ts +4 -1
- package/src/types.d.ts +70 -24
- package/src/video.ts +1 -1
- package/src/window.ts +146 -10
package/AGENTS.md
CHANGED
|
@@ -111,12 +111,18 @@ 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.8); 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
|
|
118
|
-
latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works
|
|
119
|
-
|
|
118
|
+
latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works
|
|
119
|
+
deletes the installed copies and leaves the lockfile alone:
|
|
120
|
+
`rm -rf node_modules/solid-js node_modules/@solidjs && bun install`
|
|
121
|
+
(delete any nested `node_modules/@solidjs` too, or the old copy keeps
|
|
122
|
+
resolving). Removing the directories is what forces re-resolution. Do not
|
|
123
|
+
delete `bun.lock` to force it: that re-resolves every unrelated dependency
|
|
124
|
+
as well, and a package that floats elsewhere fails the typecheck in a way
|
|
125
|
+
that reads exactly like Solid fallout.
|
|
120
126
|
|
|
121
127
|
## Element model (the parts that are easy to get wrong)
|
|
122
128
|
|
|
@@ -361,7 +367,17 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
361
367
|
`<View2d pointer>` take a feed and spread it on their built-in leaf) and
|
|
362
368
|
bind its gestures `drag` (one pointer), `pan` (two), `pinch`, `twist`,
|
|
363
369
|
`wheel` and `mouseDelta` (raw motion while pointerLocked()). One
|
|
364
|
-
merged recognizer serves them, so they arbitrate in the arena as one.
|
|
370
|
+
merged recognizer serves them, so they arbitrate in the arena as one.
|
|
371
|
+
Each gesture takes a chord in the key-spec grammar: `pointer.drag("Ctrl")`
|
|
372
|
+
fires only when the event that opens the gesture carries Ctrl, and then
|
|
373
|
+
instead of the bare `drag` (the most specific bound chord wins, as a
|
|
374
|
+
key spec does), held for the whole gesture - so plain drag on `rotate`
|
|
375
|
+
and `drag("Ctrl")` on `pan` is one binding each, the every-viewport
|
|
376
|
+
default. A bracketed spec may end in its button (`drag("Right")`,
|
|
377
|
+
`drag("Shift+Middle")`; bare means Left), a discriminator rather than
|
|
378
|
+
a modifier: a right drag feeds only Right specs. Touch carries no
|
|
379
|
+
modifiers or buttons: a chord is the desktop path, the bare binding
|
|
380
|
+
stays for fingers. A
|
|
365
381
|
detached d-* leaf has no layout box: give the feed `{ layout }` or it
|
|
366
382
|
throws at the first press. `invert(source)` and `scale(source, k)` are
|
|
367
383
|
the two processors: keys and sticks move the CAMERA where a drag moves
|
|
@@ -445,13 +461,32 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
|
|
|
445
461
|
write targets, and the runtime animates natively with no per-frame JS.
|
|
446
462
|
Enter and exit are per-property entries on that same spec: `from` is the
|
|
447
463
|
value the property animates in from at first attach, `exit` the value it
|
|
448
|
-
animates to on removal
|
|
449
|
-
|
|
464
|
+
animates to on removal. An exit reaches the whole unmounted subtree: every
|
|
465
|
+
`exit` declared under the node that unmounts plays, and the node stays
|
|
466
|
+
painted until the last of them settles, then frees - a panel's contents
|
|
467
|
+
leave with `exit` beside their `from`, no closing signal, held mount or
|
|
468
|
+
settle cue (no AnimatePresence equivalent needed). One rule in both
|
|
469
|
+
trees: a node animating out is a ghost, painted and nothing else -
|
|
470
|
+
hit-test invisible here, and in the spatial arena (a @solidrt/2d sprite,
|
|
471
|
+
a @solidrt/3d node, whose `destroy` verbs carry the same `exit`)
|
|
472
|
+
invisible to every pick, raycast, overlap and sweep - while an entering
|
|
473
|
+
node is live from its first frame, whatever `from` it passes through.
|
|
474
|
+
Either endpoint takes
|
|
475
|
+
`{ value, curve?, duration?, bounce?, delay? }` to own its direction's
|
|
476
|
+
motion (an ease-out enter, an ease-in exit). `stagger` (ms) goes on an
|
|
450
477
|
ANCESTOR, never on the animating elements: it delays each descendant's
|
|
451
478
|
enter/exit by `index * stagger`, and cascades nothing unless the
|
|
452
479
|
descendants declare `from`/`exit`. An enter plays once per mount; to
|
|
453
480
|
replay it, remount the subtree (`<Show when={epoch()} keyed>` around it,
|
|
454
|
-
then bump `epoch`). See examples/stagger.tsx.
|
|
481
|
+
then bump `epoch`). See examples/stagger.tsx. The companion to the exit
|
|
482
|
+
pop-out is `layout` on the same spec: an element declaring it slides
|
|
483
|
+
from where it was painted to the box a layout gives it (a row removed
|
|
484
|
+
above it, one inserted, a `<For>` reorder) instead of jumping, retargets
|
|
485
|
+
if the layout changes again mid-slide, and is hit-tested where it is
|
|
486
|
+
drawn. It is parent-relative, so declare it on every level that should
|
|
487
|
+
glide; `layout: true` borrows the `all` motion, and `all` alone never
|
|
488
|
+
slides. Position only today, a size change snaps. See
|
|
489
|
+
examples/layout-slide.tsx.
|
|
455
490
|
Per-frame work is for genuinely procedural motion, and a game or a
|
|
456
491
|
simulation is that in full: its natural shape is ONE `onFrame` doing one
|
|
457
492
|
sim step and N property (or sprite) writes, which is not a smell but the
|
package/docs/index.md
CHANGED
|
@@ -146,6 +146,29 @@ let camera = createCamera()
|
|
|
146
146
|
The same pattern covers `@solidrt/core/microphone`, `/sound`,
|
|
147
147
|
`/speech-recognition`, `/text-input`, `/image`, `/color`, and `/gpu`.
|
|
148
148
|
|
|
149
|
+
## Lifecycle
|
|
150
|
+
|
|
151
|
+
The runtime reports the platform's lifecycle facts and holds the platform
|
|
152
|
+
open while the app acts on them; what to do with them is the app's policy.
|
|
153
|
+
`onBack` is the user's back intent (prevent it for in-app navigation, or let
|
|
154
|
+
it fall through to the platform's default: `background()` on Android, where
|
|
155
|
+
the system keeps the app behind the others, `exit()` elsewhere). `onSuspend` fires when the app is being
|
|
156
|
+
suspended and may be killed without notice (switching away on Android or
|
|
157
|
+
iOS; never on desktop): persist session state there, async is fine, the
|
|
158
|
+
runtime waits for the returned promise up to its deadline. `onQuit` fires
|
|
159
|
+
when this app instance is ending (`exit()`, the window closing, the Android
|
|
160
|
+
activity finishing) and waits the same way, inside the platform's close
|
|
161
|
+
budget. `env.launch` says how this process came to run: "restored" when the
|
|
162
|
+
system recreated the app from a session it ended on its own, "fresh" for a
|
|
163
|
+
launch the user started. Whether to load what `onSuspend` saved is the app's
|
|
164
|
+
call. `env.visibility` is only whether anything is on screen, for pausing
|
|
165
|
+
animation and audio, not the persistence moment.
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
onSuspend(() => file("session.json").write(JSON.stringify(state)))
|
|
169
|
+
if (env.launch === "restored") state = JSON.parse(await file("session.json").text())
|
|
170
|
+
```
|
|
171
|
+
|
|
149
172
|
## Reference
|
|
150
173
|
|
|
151
174
|
The [reference](/core/reference/) covers the API by subject: the element
|
package/examples/README.md
CHANGED
|
@@ -21,6 +21,7 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
21
21
|
- `frame-animation.tsx` - `onFrame` driving a transform animation each frame.
|
|
22
22
|
- `on-layout-connect.tsx` - `onLayout` + `getBoundingBox` connecting laid-out boxes with a `d-path`.
|
|
23
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.
|
|
24
|
+
- `layout-slide.tsx` - the `layout` transition, the companion to exit: rows declaring it slide from the box they had to the box a layout gives them instead of jumping - tap a row to remove it (its exit plays where it was while the rows below slide up), the header to shuffle the order (a `<For>` reorder), the footer to add one back. No per-frame JS, no hand-animated `y`.
|
|
24
25
|
|
|
25
26
|
## Pointer input
|
|
26
27
|
- `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.
|
|
@@ -60,22 +60,22 @@ let FRAGMENT_PULSE = glsl`
|
|
|
60
60
|
function App() {
|
|
61
61
|
let triangle = createBuffer(new Float32Array([0, 0.6, -0.5, -0.4, 0.5, -0.4]), { label: "tri" })
|
|
62
62
|
let vs = compileShader("vertex", VERTEX, { header: true })
|
|
63
|
-
let attrs = [{ name: "aPos", format: "
|
|
63
|
+
let attrs = [{ name: "aPos", format: "float32x2" as const }]
|
|
64
64
|
let warm = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_WARM, { header: true })), {
|
|
65
|
-
attributes: attrs,
|
|
65
|
+
buffers: [{ attributes: attrs }],
|
|
66
66
|
depth: true,
|
|
67
67
|
})
|
|
68
68
|
let cool = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_COOL, { header: true })), {
|
|
69
|
-
attributes: attrs,
|
|
69
|
+
buffers: [{ attributes: attrs }],
|
|
70
70
|
depth: true,
|
|
71
71
|
})
|
|
72
72
|
let pulse = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_PULSE, { header: true })), {
|
|
73
|
-
attributes: attrs,
|
|
73
|
+
buffers: [{ attributes: attrs }],
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
let target = createDrawTarget(512, 512, null, { depth: true, clearColor: [0.04, 0.04, 0.08, 1], label: "orbits" })
|
|
77
|
-
let warmDraw = addDraw(target, warm, { uAngle: 0 }, {
|
|
78
|
-
let coolDraw = addDraw(target, cool, { uAngle: Math.PI }, {
|
|
77
|
+
let warmDraw = addDraw(target, warm, { uAngle: 0 }, { buffers: [triangle] })
|
|
78
|
+
let coolDraw = addDraw(target, cool, { uAngle: Math.PI }, { buffers: [triangle] })
|
|
79
79
|
|
|
80
80
|
// A third entry blinks in and out every second: add/remove are ordinary
|
|
81
81
|
// per-frame-affordable writes, and a removed DrawId simply retires. Its
|
|
@@ -91,7 +91,7 @@ function App() {
|
|
|
91
91
|
setDrawParams(target, coolDraw, { uAngle: t + Math.PI })
|
|
92
92
|
let wantPulse = Math.floor(t) % 2 === 0
|
|
93
93
|
if (wantPulse && pulseDraw === null) {
|
|
94
|
-
pulseDraw = addDraw(target, pulse, { uAngle: t * 0.3, uPhase: t * 5 }, {
|
|
94
|
+
pulseDraw = addDraw(target, pulse, { uAngle: t * 0.3, uPhase: t * 5 }, { buffers: [triangle], before: warmDraw })
|
|
95
95
|
} else if (!wantPulse && pulseDraw !== null) {
|
|
96
96
|
removeDraw(target, pulseDraw)
|
|
97
97
|
pulseDraw = null
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Instanced drawing with per-instance attributes: one 3-vertex triangle in
|
|
2
2
|
// the vertex buffer, drawn hundreds of times, each copy reading its own
|
|
3
|
-
// record from an instance buffer (
|
|
4
|
-
//
|
|
3
|
+
// record from an instance buffer (a `stepMode: "instance"` layout on the
|
|
4
|
+
// pipeline, vertex divisor 1 underneath). Here each
|
|
5
5
|
// record is one petal of a phyllotaxis spiral - its angle, radius, size, and
|
|
6
6
|
// tint, computed once in JS - so the vertex stage just reads state instead
|
|
7
7
|
// of re-deriving it from gl_InstanceID every frame, and the geometry buffer
|
|
@@ -45,7 +45,7 @@ let FRAGMENT = glsl`
|
|
|
45
45
|
}
|
|
46
46
|
`
|
|
47
47
|
|
|
48
|
-
// One record per petal (matching
|
|
48
|
+
// One record per petal (matching the instance layout: 6 floats): radius grows
|
|
49
49
|
// with sqrt(index), the angle steps by the golden angle (~2.39996 rad) - the
|
|
50
50
|
// sunflower layout - petals shrink toward the rim, and the tint walks a
|
|
51
51
|
// cosine palette.
|
|
@@ -70,15 +70,19 @@ function App() {
|
|
|
70
70
|
let instanceId = createBuffer(petalRecords(), { label: "petal-records" })
|
|
71
71
|
let id = createPipelineTexture(VERTEX, FRAGMENT, 512, 512, { uTime: 0 }, {
|
|
72
72
|
label: "petals",
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
buffers: [
|
|
74
|
+
{ attributes: [{ name: "aPos", format: "float32x2" }], buffer: bufferId },
|
|
75
|
+
{
|
|
76
|
+
stepMode: "instance",
|
|
77
|
+
attributes: [
|
|
78
|
+
{ name: "iAngle", format: "float32" },
|
|
79
|
+
{ name: "iRadius", format: "float32" },
|
|
80
|
+
{ name: "iScale", format: "float32" },
|
|
81
|
+
{ name: "iTint", format: "float32x3" },
|
|
82
|
+
],
|
|
83
|
+
buffer: instanceId,
|
|
84
|
+
},
|
|
80
85
|
],
|
|
81
|
-
instanceBuffer: instanceId,
|
|
82
86
|
instanceCount: 1,
|
|
83
87
|
clearColor: [0.03, 0.03, 0.06, 1],
|
|
84
88
|
})
|
|
@@ -74,11 +74,15 @@ function App() {
|
|
|
74
74
|
let bufferId = createBuffer(particles(1500), { label: "particle-verts" })
|
|
75
75
|
let id = createPipelineTexture(VERTEX, FRAGMENT, 512, 512, { uTime: 0, uTintA: [1.0, 0.45, 0.15], uTintB: [0.25, 0.5, 1.0] }, {
|
|
76
76
|
label: "particles",
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
|
|
77
|
+
buffers: [
|
|
78
|
+
{
|
|
79
|
+
attributes: [
|
|
80
|
+
{ name: "aPos", format: "float32x3" },
|
|
81
|
+
{ name: "aSeed", format: "float32" },
|
|
82
|
+
],
|
|
83
|
+
buffer: bufferId,
|
|
84
|
+
},
|
|
80
85
|
],
|
|
81
|
-
buffer: bufferId,
|
|
82
86
|
topology: "points",
|
|
83
87
|
blend: "add",
|
|
84
88
|
clearColor: [0.02, 0.02, 0.05, 1],
|
|
@@ -87,11 +87,15 @@ function App() {
|
|
|
87
87
|
let indexId = createBuffer(cubeIndices(), { label: "cube-indices" })
|
|
88
88
|
let id = createPipelineTexture(VERTEX, FRAGMENT, 1024, 1024, { uTime: 0 }, {
|
|
89
89
|
label: "cube",
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
90
|
+
buffers: [
|
|
91
|
+
{
|
|
92
|
+
attributes: [
|
|
93
|
+
{ name: "aPos", format: "float32x3" },
|
|
94
|
+
{ name: "aColor", format: "float32x3" },
|
|
95
|
+
],
|
|
96
|
+
buffer: bufferId,
|
|
97
|
+
},
|
|
93
98
|
],
|
|
94
|
-
buffer: bufferId,
|
|
95
99
|
indexBuffer: indexId,
|
|
96
100
|
indexFormat: "uint16",
|
|
97
101
|
depth: true,
|
|
@@ -65,12 +65,12 @@ let FRAGMENT_MAP = glsl`
|
|
|
65
65
|
function App() {
|
|
66
66
|
let quad = createBuffer(new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]), { label: "quad" })
|
|
67
67
|
let vs = compileShader("vertex", VERTEX, { header: true })
|
|
68
|
-
let attrs = [{ name: "aPos", format: "
|
|
68
|
+
let attrs = [{ name: "aPos", format: "float32x2" as const }]
|
|
69
69
|
let tint = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_TINT, { header: true })), {
|
|
70
|
-
attributes: attrs,
|
|
70
|
+
buffers: [{ attributes: attrs }],
|
|
71
71
|
})
|
|
72
72
|
let mapped = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_MAP, { header: true })), {
|
|
73
|
-
attributes: attrs,
|
|
73
|
+
buffers: [{ attributes: attrs }],
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
// 2x2 patterns, nearest + repeat, so gl_FragCoord tiling shows hard cells.
|
|
@@ -99,12 +99,12 @@ function App() {
|
|
|
99
99
|
if (i === 1 || i === 5) {
|
|
100
100
|
// Patterned entries read the shared uMap; the one at i === 5 brings
|
|
101
101
|
// its own binding and keeps its stripes through every shared swap.
|
|
102
|
-
addDraw(target, mapped, { uCenter }, i === 5 ? {
|
|
102
|
+
addDraw(target, mapped, { uCenter }, i === 5 ? { buffers: [quad], textures: { uMap: stripes } } : { buffers: [quad] })
|
|
103
103
|
} else if (i === 3) {
|
|
104
104
|
// The override quad: its own uTint beats the shared color cycle.
|
|
105
|
-
addDraw(target, tint, { uCenter, uTint: [1, 0.62, 0.1, 1] }, {
|
|
105
|
+
addDraw(target, tint, { uCenter, uTint: [1, 0.62, 0.1, 1] }, { buffers: [quad] })
|
|
106
106
|
} else {
|
|
107
|
-
addDraw(target, tint, { uCenter }, {
|
|
107
|
+
addDraw(target, tint, { uCenter }, { buffers: [quad] })
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
package/examples/gpu-sprites.tsx
CHANGED
|
@@ -60,14 +60,18 @@ function App() {
|
|
|
60
60
|
label: "sprites",
|
|
61
61
|
topology: "triangle-strip",
|
|
62
62
|
vertexCount: 4,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
buffers: [
|
|
64
|
+
{ attributes: [{ name: "aPos", format: "float32x2" }], buffer: quad },
|
|
65
|
+
{
|
|
66
|
+
stepMode: "instance",
|
|
67
|
+
attributes: [
|
|
68
|
+
{ name: "iCenter", format: "float32x2" },
|
|
69
|
+
{ name: "iSize", format: "float32" },
|
|
70
|
+
{ name: "iTint", format: "float32x3" },
|
|
71
|
+
],
|
|
72
|
+
buffer: records,
|
|
73
|
+
},
|
|
69
74
|
],
|
|
70
|
-
instanceBuffer: records,
|
|
71
75
|
instanceCount: MAX_SPRITES,
|
|
72
76
|
clearColor: [0.03, 0.03, 0.06, 1],
|
|
73
77
|
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The `layout` transition, the companion to `exit`: a row declaring it
|
|
2
|
+
// slides from the box it had to the box the next layout gives it, so the
|
|
3
|
+
// list never jumps when a row leaves, arrives or moves. Nothing here
|
|
4
|
+
// animates `y` by hand and there is no per-frame JS: the rows keep their
|
|
5
|
+
// flex layout, and the runtime slides each one from where it was painted.
|
|
6
|
+
// Tap a row to remove it - its exit plays where it was (the pop-out) while
|
|
7
|
+
// the rows below slide up past it. Tap the header to shuffle: a `<For>`
|
|
8
|
+
// reorder moves the surviving nodes, so every displaced row slides. Tap
|
|
9
|
+
// the footer to add a row back: the rows below its slot slide apart and
|
|
10
|
+
// the new row enters through its `from`. A reflow that lands mid-slide
|
|
11
|
+
// retargets, and a spring keeps its momentum. `layout` is parent-relative
|
|
12
|
+
// (declare it on every level that should glide) and position only: a size
|
|
13
|
+
// change snaps.
|
|
14
|
+
import { render, createSignal, For } from "@solidrt/core"
|
|
15
|
+
import type { Transition } from "@solidrt/core"
|
|
16
|
+
|
|
17
|
+
const ALL_ROWS = ["Signals", "Effects", "Memos", "Stores", "Boundaries", "Resources"]
|
|
18
|
+
// Rows the list starts with; the rest come back through the footer.
|
|
19
|
+
const INITIAL_ROWS = 4
|
|
20
|
+
// Every reflow slides on this spring.
|
|
21
|
+
const SLIDE = { duration: 400, bounce: 0.15 } satisfies Transition
|
|
22
|
+
// Enter from the left, leave to the right, each direction on its own curve.
|
|
23
|
+
const ARRIVE = {
|
|
24
|
+
duration: 300,
|
|
25
|
+
curve: "ease-out",
|
|
26
|
+
from: -80,
|
|
27
|
+
exit: { value: 80, curve: "ease-in", duration: 200 },
|
|
28
|
+
} satisfies Transition
|
|
29
|
+
const FADE = {
|
|
30
|
+
duration: 300,
|
|
31
|
+
curve: "ease-out",
|
|
32
|
+
from: 0,
|
|
33
|
+
exit: { value: 0, curve: "ease-in", duration: 200 },
|
|
34
|
+
} satisfies Transition
|
|
35
|
+
|
|
36
|
+
function shuffled(items: string[]): string[] {
|
|
37
|
+
let pool = items.slice()
|
|
38
|
+
let out: string[] = []
|
|
39
|
+
while (pool.length > 0) out.push(...pool.splice(Math.floor(Math.random() * pool.length), 1))
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function App() {
|
|
44
|
+
let [rows, setRows] = createSignal(ALL_ROWS.slice(0, INITIAL_ROWS))
|
|
45
|
+
let addRow = () => {
|
|
46
|
+
let next = ALL_ROWS.find((label) => !rows().includes(label))
|
|
47
|
+
if (next) setRows([...rows(), next])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<window padding={40} gap={12}>
|
|
52
|
+
<view height={40} justifyContent="center" onPointerDown={() => setRows(shuffled(rows()))}>
|
|
53
|
+
<text color="#8899aa" fontSize={14}>Tap here to shuffle, a row to remove it</text>
|
|
54
|
+
</view>
|
|
55
|
+
<For each={rows()}>
|
|
56
|
+
{(label) => (
|
|
57
|
+
<view
|
|
58
|
+
x={0}
|
|
59
|
+
opacity={1}
|
|
60
|
+
transition={{ layout: SLIDE, x: ARRIVE, opacity: FADE }}
|
|
61
|
+
height={48}
|
|
62
|
+
justifyContent="center"
|
|
63
|
+
paddingLeft={16}
|
|
64
|
+
onPointerDown={() => setRows(rows().filter((row) => row !== label))}
|
|
65
|
+
>
|
|
66
|
+
<d-rect color="#2a3a55" radius={10} />
|
|
67
|
+
<text color="#e8eef6" fontSize={18}>{label}</text>
|
|
68
|
+
</view>
|
|
69
|
+
)}
|
|
70
|
+
</For>
|
|
71
|
+
<view height={40} justifyContent="center" transition={{ layout: SLIDE }} onPointerDown={addRow}>
|
|
72
|
+
<text color="#8899aa" fontSize={14}>Tap here to add a row back</text>
|
|
73
|
+
</view>
|
|
74
|
+
</window>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
render(() => <App />)
|
package/examples/stagger.tsx
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
// Enter, exit and stagger are declared on elements; the runtime plays them
|
|
2
2
|
// with no per-frame JS. `from` is the value a property animates in from at
|
|
3
|
-
// first attach, `exit` the value it animates to on removal
|
|
4
|
-
//
|
|
5
|
-
// the
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
3
|
+
// first attach, `exit` the value it animates to on removal. Both are
|
|
4
|
+
// per-property entries on the element's own transition spec. An exit
|
|
5
|
+
// reaches the whole removed subtree: every `exit` declared under the node
|
|
6
|
+
// that unmounts plays, and that node stays painted until the last of them
|
|
7
|
+
// settles, then frees. It leaves the layout at once, so a fresh panel
|
|
8
|
+
// mounted mid-exit takes its place while the old one fades over it. The
|
|
9
|
+
// rows below never unmount one by one - the panel around them does - and
|
|
10
|
+
// they still leave the way they arrived. Each direction owns its motion:
|
|
11
|
+
// `from`/`exit` take a bare value (the entry's curve, duration and delay)
|
|
12
|
+
// or `{ value, curve?, duration?, bounce?, delay? }`, so the fade below
|
|
13
|
+
// eases out on the way in and eases in, shorter, on the way out.
|
|
14
|
+
// `stagger` (ms) goes on an ANCESTOR, never on the animating elements: every
|
|
15
|
+
// descendant enter or exit that begins in the same frame gets index * stagger
|
|
16
|
+
// of extra delay, in tree order. It cascades nothing on its own - the
|
|
17
|
+
// descendants must declare from/exit.
|
|
10
18
|
//
|
|
11
19
|
// An enter plays once per mount. To replay it, remount the subtree: a keyed
|
|
12
20
|
// <Show> re-creates its child whenever `when` changes value, so bumping an
|
|
13
|
-
// epoch replays the whole cascade. Tap anywhere: odd taps unmount the
|
|
21
|
+
// epoch replays the whole cascade. Tap anywhere: odd taps unmount the panel
|
|
14
22
|
// (staggered exits), even taps mount a fresh one (staggered enters).
|
|
15
23
|
import { render, createSignal, Show } from "@solidrt/core"
|
|
16
24
|
import type { Transition } from "@solidrt/core"
|
|
@@ -18,25 +26,33 @@ import type { Transition } from "@solidrt/core"
|
|
|
18
26
|
const ROWS = ["Signals", "Effects", "Memos", "Stores", "Boundaries"]
|
|
19
27
|
// Extra delay per row before its enter or exit starts.
|
|
20
28
|
const STAGGER_MS = 70
|
|
21
|
-
// Slide in from the left, out to the right; fade both ways
|
|
29
|
+
// Slide in from the left, out to the right; fade both ways, the exit on
|
|
30
|
+
// its own curve and clock.
|
|
22
31
|
const SLIDE = { duration: 500, bounce: 0.2, from: -80, exit: 80 } satisfies Transition
|
|
23
|
-
const FADE = {
|
|
32
|
+
const FADE = {
|
|
33
|
+
duration: 350,
|
|
34
|
+
curve: "ease-out",
|
|
35
|
+
from: 0,
|
|
36
|
+
exit: { value: 0, curve: "ease-in", duration: 200 },
|
|
37
|
+
} satisfies Transition
|
|
24
38
|
|
|
25
39
|
function App() {
|
|
26
40
|
let [epoch, setEpoch] = createSignal(1)
|
|
27
41
|
|
|
28
42
|
return (
|
|
29
|
-
<window padding={40} gap={12} onPointerDown={() => setEpoch((e) => e + 1)}
|
|
43
|
+
<window padding={40} gap={12} onPointerDown={() => setEpoch((e) => e + 1)}>
|
|
30
44
|
<text color="#8899aa" fontSize={14}>Tap to replay</text>
|
|
31
45
|
<Show when={epoch() % 2 === 1 ? epoch() : 0} keyed>
|
|
32
|
-
{(_epoch) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
{(_epoch) => (
|
|
47
|
+
<view gap={12} transition={{ stagger: STAGGER_MS }}>
|
|
48
|
+
{ROWS.map((label) => (
|
|
49
|
+
<view x={0} opacity={1} transition={{ x: SLIDE, opacity: FADE }} height={48} justifyContent="center" paddingLeft={16}>
|
|
50
|
+
<d-rect color="#2a3a55" radius={10} />
|
|
51
|
+
<text color="#e8eef6" fontSize={18}>{label}</text>
|
|
52
|
+
</view>
|
|
53
|
+
))}
|
|
54
|
+
</view>
|
|
55
|
+
)}
|
|
40
56
|
</Show>
|
|
41
57
|
</window>
|
|
42
58
|
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.59",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
"AGENTS.md"
|
|
32
32
|
],
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@solidrt/flux-types": "0.0.
|
|
34
|
+
"@solidrt/flux-types": "0.0.59"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
38
|
-
"@solidjs/universal": "2.0.0-rc.
|
|
39
|
-
"solid-js": "2.0.0-rc.
|
|
37
|
+
"@solidjs/signals": "2.0.0-rc.8",
|
|
38
|
+
"@solidjs/universal": "2.0.0-rc.8",
|
|
39
|
+
"solid-js": "2.0.0-rc.8"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/environment.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type SystemTheme = "dark" | "light" | "unknown"
|
|
|
37
37
|
|
|
38
38
|
export type Visibility = "visible" | "hidden"
|
|
39
39
|
|
|
40
|
+
export type Launch = "fresh" | "restored"
|
|
41
|
+
|
|
40
42
|
export type Orientation = "portrait" | "portraitFlipped" | "landscape" | "landscapeFlipped" | "unknown"
|
|
41
43
|
|
|
42
44
|
let devicesAccessor: (() => InputDevices | undefined) | undefined
|
|
@@ -65,6 +67,22 @@ function ensureSystemThemeState() {
|
|
|
65
67
|
})
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// Fixed for the process, so a plain value rather than a signal: the sticky
|
|
71
|
+
// `launch` event replays synchronously on subscribe, and the runner emits it
|
|
72
|
+
// before any app code runs.
|
|
73
|
+
let launchValue: Launch | undefined
|
|
74
|
+
|
|
75
|
+
function ensureLaunchState() {
|
|
76
|
+
if (launchValue) return
|
|
77
|
+
runWithOwner(null, () => {
|
|
78
|
+
on("launch", (e: { state?: Launch }) => {
|
|
79
|
+
launchValue = e.state === "restored" ? "restored" : "fresh"
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
// A runtime that reports no launch fact (desktop today) launches fresh.
|
|
83
|
+
launchValue ??= "fresh"
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
let visibilityAccessor: (() => Visibility) | undefined
|
|
69
87
|
|
|
70
88
|
function ensureVisibilityState() {
|
|
@@ -218,15 +236,27 @@ export let env = {
|
|
|
218
236
|
* `visibilityState` vocabulary without the `document` machinery - react
|
|
219
237
|
* to it in a tracked scope (JSX, memo, effect).
|
|
220
238
|
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* running but no frames are produced.
|
|
239
|
+
* A rendering and UI fact (nothing is on screen: stop animating, pause
|
|
240
|
+
* the audio), not the persistence moment - that is `onSuspend`, which the
|
|
241
|
+
* runtime waits for. While hidden no frames are produced.
|
|
225
242
|
*/
|
|
226
243
|
get visibility(): Visibility {
|
|
227
244
|
ensureVisibilityState()
|
|
228
245
|
return visibilityAccessor!()
|
|
229
246
|
},
|
|
247
|
+
/**
|
|
248
|
+
* How this process came to run. "restored": the system recreated the app
|
|
249
|
+
* from a session it ended on its own (a suspended app reclaimed in the
|
|
250
|
+
* background; Android's saved instance state). "fresh": launched anew, a
|
|
251
|
+
* first start or one after `exit()` or a close ended the previous
|
|
252
|
+
* instance, and always on desktop. Reported, not interpreted: whether to
|
|
253
|
+
* load what `onSuspend` saved is the app's decision. Fixed for the
|
|
254
|
+
* process, so it needs no tracked scope.
|
|
255
|
+
*/
|
|
256
|
+
get launch(): Launch {
|
|
257
|
+
ensureLaunchState()
|
|
258
|
+
return launchValue!
|
|
259
|
+
},
|
|
230
260
|
/** Orientation of the display the window is on. */
|
|
231
261
|
get orientation(): Orientation {
|
|
232
262
|
ensureOrientationState()
|