@solidrt/core 0.0.51 → 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 +89 -20
- package/README.md +1 -1
- 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 +5 -3
- package/examples/gpu-pipeline.tsx +2 -2
- 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/package.json +6 -5
- package/src/core.ts +19 -1
- package/src/gpu.ts +68 -32
- package/src/index.ts +8 -2
- package/src/logo.tsx +92 -0
- package/src/renderer.ts +181 -29
- package/src/runtime-modules.d.ts +4 -0
- package/src/scroll.ts +50 -14
- package/src/svg.ts +1 -1
- package/src/text-input.ts +0 -1
- package/src/types.d.ts +121 -29
- package/src/window.ts +52 -7
package/src/gpu.ts
CHANGED
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
// `flux:gpu` module.
|
|
12
12
|
//
|
|
13
13
|
// Sampling is a per-texture property declared at creation: `filter`
|
|
14
|
-
// ("linear" default | "nearest")
|
|
15
|
-
// every create* helper. One state for every
|
|
16
|
-
// and shader sampling both follow it - so a
|
|
17
|
-
// hard pixels everywhere (the retro/pixel-art
|
|
18
|
-
// big).
|
|
14
|
+
// ("linear" default | "nearest"), `wrap` ("clamp" default | "repeat") and
|
|
15
|
+
// `mipmap` (default false) on every create* helper. One state for every
|
|
16
|
+
// consumer - `<texture>` display and shader sampling both follow it - so a
|
|
17
|
+
// nearest texture upscales with hard pixels everywhere (the retro/pixel-art
|
|
18
|
+
// path: render small, display big). `mipmap: true` keeps a mip chain the
|
|
19
|
+
// runtime regenerates after every upload or render, so shader sampling of a
|
|
20
|
+
// minified texture (3d surfaces at distance, a supersampled target) does
|
|
21
|
+
// not alias; the display draw samples the full-size level only.
|
|
19
22
|
//
|
|
20
23
|
// Combining several passes is a render-tree job, not a shader one: stack
|
|
21
24
|
// `<texture>` elements and set their `blendMode` (e.g. `blendMode="plus"` for
|
|
@@ -62,8 +65,8 @@ export type CreateOptions = { autoFree?: boolean; label?: string }
|
|
|
62
65
|
|
|
63
66
|
// Sampling options every texture-producing create* helper accepts, applied at
|
|
64
67
|
// creation as a property of the texture id (there is no set-sampler-later).
|
|
65
|
-
export type SamplerOptions = { filter?: gpu.FilterMode; wrap?: gpu.WrapMode }
|
|
66
|
-
export type { FilterMode, WrapMode } from "flux:gpu"
|
|
68
|
+
export type SamplerOptions = { filter?: gpu.FilterMode; wrap?: gpu.WrapMode; mipmap?: boolean }
|
|
69
|
+
export type { FilterMode, WrapMode, TextureBinding, TextureBindings } from "flux:gpu"
|
|
67
70
|
|
|
68
71
|
// Pixel format option for the pixel-upload creates (createTexture,
|
|
69
72
|
// createMutableTexture), fixed for the id's lifetime like the sampler state.
|
|
@@ -98,7 +101,11 @@ export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, Text
|
|
|
98
101
|
// resize in place at a stable id (so `<texture src>` and sampler bindings
|
|
99
102
|
// stay valid); because the id survives, the owner-scoped auto-free
|
|
100
103
|
// registered at creation keeps working and no re-registration is needed.
|
|
104
|
+
// depthTexture(target) names a draw target's sampleable depth (created with
|
|
105
|
+
// `depth: "texture"`): a sampler-only id bound like any texture, owned by
|
|
106
|
+
// the target - no auto-free of its own, it dies with the target's.
|
|
101
107
|
export {
|
|
108
|
+
depthTexture,
|
|
102
109
|
destroyTexture,
|
|
103
110
|
endBufferWrite,
|
|
104
111
|
resizeTexture,
|
|
@@ -112,7 +119,9 @@ export {
|
|
|
112
119
|
// updated draw range (vertexCount after its buffer gained or lost dynamic
|
|
113
120
|
// geometry, firstVertex for a different window of a shared buffer,
|
|
114
121
|
// instanceCount for an instanced population; absent keys keep their current
|
|
115
|
-
// value, like params)
|
|
122
|
+
// value, like params) and/or swapped buffers (instanceBuffer pointed at a
|
|
123
|
+
// larger buffer once a population outgrows the old one - the growth
|
|
124
|
+
// primitive; replace-only, the range is rechecked); destroyBuffer is the manual
|
|
116
125
|
// cleanup path for buffers created outside a reactive scope. renderTarget is
|
|
117
126
|
// the explicit render verb for `render: "manual"` targets - targets whose
|
|
118
127
|
// pass is state (accumulation, feedback) rather than a pure function of its
|
|
@@ -122,22 +131,22 @@ export {
|
|
|
122
131
|
// GPU-side (exact, same size): seed a loadOp "load" accumulator, snapshot a
|
|
123
132
|
// ping-pong buffer, reset state to a known image.
|
|
124
133
|
export { copyTexture, destroyBuffer, renderTarget, setDraw } from "flux:gpu"
|
|
125
|
-
export type { BlendMode, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
134
|
+
export type { BlendMode, BufferUpdate, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, InstanceAttribute, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
126
135
|
|
|
127
136
|
// The draw-list verbs, re-exported raw: entries live and die with their draw
|
|
128
137
|
// target (see createDrawTarget below), so there is no per-entry lifetime to
|
|
129
138
|
// wrap. addDraw adds an entry (appended, or inserted via opts.before) and
|
|
130
139
|
// returns its stable DrawId; removeDraw drops one; setDrawParams /
|
|
131
|
-
// setDrawTextures / setDrawRange are the per-entry forms of
|
|
132
|
-
// setTargetTextures / setDraw
|
|
133
|
-
// merge and validation semantics. The per-object hot path is setDrawParams (a
|
|
140
|
+
// setDrawTextures / setDrawRange / setDrawBuffers are the per-entry forms of
|
|
141
|
+
// setTargetParams / setTargetTextures / setDraw (its range and buffer halves),
|
|
142
|
+
// taking (target, draw, value) with identical merge and validation semantics. The per-object hot path is setDrawParams (a
|
|
134
143
|
// moved mesh = one call with its new matrix); the per-target one is
|
|
135
144
|
// setTargetParams (exported above), which on a draw target writes the SHARED
|
|
136
145
|
// params every entry reads. setDrawOrder replaces the whole
|
|
137
146
|
// list order with a full permutation of the live ids - the sorting verb
|
|
138
147
|
// (opaque front-to-back, transparent back-to-front, re-issued when the
|
|
139
148
|
// camera moves).
|
|
140
|
-
export { addDraw, removeDraw, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures } from "flux:gpu"
|
|
149
|
+
export { addDraw, removeDraw, setDrawBuffers, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures } from "flux:gpu"
|
|
141
150
|
|
|
142
151
|
// The device ceilings (max texture/target size, sampler inputs per pass,
|
|
143
152
|
// vertex attributes per pipeline), queried once at startup. Creates and binds
|
|
@@ -162,6 +171,7 @@ export {
|
|
|
162
171
|
destroyRenderPipeline,
|
|
163
172
|
destroyShader,
|
|
164
173
|
linkProgram,
|
|
174
|
+
programAttributes,
|
|
165
175
|
} from "flux:gpu"
|
|
166
176
|
|
|
167
177
|
/**
|
|
@@ -283,7 +293,7 @@ export function createShaderTexture(
|
|
|
283
293
|
width: number,
|
|
284
294
|
height: number,
|
|
285
295
|
params?: gpu.ShaderParams | null,
|
|
286
|
-
opts?: CreateOptions & SamplerOptions & { textures?:
|
|
296
|
+
opts?: CreateOptions & SamplerOptions & { textures?: gpu.TextureBindings },
|
|
287
297
|
): gpu.TextureId {
|
|
288
298
|
let id = gpu.createShaderTexture(fragmentSrc, width, height, params, opts)
|
|
289
299
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
@@ -320,6 +330,12 @@ export function createShaderTexture(
|
|
|
320
330
|
* the default `"clear"` clears to `clearColor` per render; state that must
|
|
321
331
|
* read its own pixels (decay, blur, simulation) still ping-pongs across two
|
|
322
332
|
* manual targets, and `copyTexture` seeds either shape.
|
|
333
|
+
*
|
|
334
|
+
* `samples` (2, 4 or 8) multisamples the target's storage so filled
|
|
335
|
+
* geometry gets anti-aliased edges; the texture id still names a
|
|
336
|
+
* single-sample image, so nothing downstream changes. Clamped to the device
|
|
337
|
+
* maximum, falls back to single-sample (with a warning) where the driver
|
|
338
|
+
* refuses, and throws with `loadOp: "load"`.
|
|
323
339
|
*/
|
|
324
340
|
export function createShaderTarget(
|
|
325
341
|
pipeline: gpu.RenderPipelineId,
|
|
@@ -327,12 +343,16 @@ export function createShaderTarget(
|
|
|
327
343
|
height: number,
|
|
328
344
|
params?: gpu.ShaderParams | null,
|
|
329
345
|
opts?: {
|
|
330
|
-
textures?:
|
|
346
|
+
textures?: gpu.TextureBindings
|
|
331
347
|
buffer?: gpu.BufferId
|
|
332
348
|
instanceBuffer?: gpu.BufferId
|
|
349
|
+
/** One buffer per instance slot of the pipeline (index = the
|
|
350
|
+
* attributes' `slot`); pass this OR `instanceBuffer`, not both. */
|
|
351
|
+
instanceBuffers?: gpu.BufferId[]
|
|
333
352
|
clearColor?: [number, number, number, number]
|
|
334
353
|
render?: "auto" | "manual"
|
|
335
354
|
loadOp?: "clear" | "load"
|
|
355
|
+
samples?: 1 | 2 | 4 | 8
|
|
336
356
|
} & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
|
|
337
357
|
CreateOptions &
|
|
338
358
|
SamplerOptions,
|
|
@@ -352,7 +372,10 @@ export function createShaderTarget(
|
|
|
352
372
|
* `setDrawParams` / `setDrawTextures` / `setDrawRange`. `depth: true` gives
|
|
353
373
|
* the target the depth storage all entries share (cross-entry occlusion);
|
|
354
374
|
* whether an entry tests/writes it stays pipeline state, and a depth-testing
|
|
355
|
-
* pipeline into a depthless target throws at `addDraw`.
|
|
375
|
+
* pipeline into a depthless target throws at `addDraw`. `depth: "texture"`
|
|
376
|
+
* makes that storage a sampleable depth texture with its own id,
|
|
377
|
+
* `depthTexture(target)` - the shadow-map / depth-effect input; not with
|
|
378
|
+
* `samples`.
|
|
356
379
|
*
|
|
357
380
|
* `params` seeds the target's SHARED params - values every entry reads,
|
|
358
381
|
* written once per target instead of once per entry (a camera's
|
|
@@ -379,11 +402,12 @@ export function createDrawTarget(
|
|
|
379
402
|
height: number,
|
|
380
403
|
params?: gpu.ShaderParams | null,
|
|
381
404
|
opts?: {
|
|
382
|
-
depth?: boolean
|
|
383
|
-
textures?:
|
|
405
|
+
depth?: boolean | "texture"
|
|
406
|
+
textures?: gpu.TextureBindings
|
|
384
407
|
clearColor?: [number, number, number, number]
|
|
385
408
|
render?: "auto" | "manual"
|
|
386
409
|
loadOp?: "clear" | "load"
|
|
410
|
+
samples?: 1 | 2 | 4 | 8
|
|
387
411
|
} & CreateOptions &
|
|
388
412
|
SamplerOptions,
|
|
389
413
|
): gpu.TextureId {
|
|
@@ -393,29 +417,31 @@ export function createDrawTarget(
|
|
|
393
417
|
}
|
|
394
418
|
|
|
395
419
|
/** The reactive shader description `createShaderTextureMemo` builds from.
|
|
396
|
-
* Sampling (`filter`/`wrap`) is creation-time state, so changing it rebuilds
|
|
420
|
+
* Sampling (`filter`/`wrap`/`mipmap`) is creation-time state, so changing it rebuilds
|
|
397
421
|
* at a fresh id, like a fragment-source or sampler-binding change. */
|
|
398
422
|
export type ShaderSpec = {
|
|
399
423
|
fragmentSrc: string
|
|
400
424
|
width: number
|
|
401
425
|
height: number
|
|
402
426
|
params?: gpu.ShaderParams
|
|
403
|
-
textures?:
|
|
427
|
+
textures?: gpu.TextureBindings
|
|
404
428
|
} & SamplerOptions
|
|
405
429
|
|
|
406
430
|
// Shallow name->value equality for params/textures records; treats undefined
|
|
407
431
|
// as the empty record. A param value may be a number or a flat number array
|
|
408
|
-
// (typed uniforms), so arrays compare elementwise
|
|
409
|
-
|
|
432
|
+
// (typed uniforms), so arrays compare elementwise; a texture binding may be
|
|
433
|
+
// an `{ id, filter?, wrap? }` override, compared field by field.
|
|
434
|
+
type RecordValue = number | number[] | gpu.TextureBinding
|
|
435
|
+
function sameValue(a: RecordValue | undefined, b: RecordValue | undefined): boolean {
|
|
410
436
|
if (a === b) return true
|
|
411
|
-
if (
|
|
412
|
-
|
|
437
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => v === b[i])
|
|
438
|
+
if (typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
439
|
+
return a.id === b.id && a.filter === b.filter && a.wrap === b.wrap
|
|
440
|
+
}
|
|
441
|
+
return false
|
|
413
442
|
}
|
|
414
443
|
|
|
415
|
-
function sameRecord(
|
|
416
|
-
a: Record<string, number | number[]> | undefined,
|
|
417
|
-
b: Record<string, number | number[]> | undefined,
|
|
418
|
-
): boolean {
|
|
444
|
+
function sameRecord(a: Record<string, RecordValue> | undefined, b: Record<string, RecordValue> | undefined): boolean {
|
|
419
445
|
if (a === b) return true
|
|
420
446
|
let ka = a ? Object.keys(a) : []
|
|
421
447
|
let kb = b ? Object.keys(b) : []
|
|
@@ -450,7 +476,12 @@ export function createShaderTextureMemo(
|
|
|
450
476
|
opts?: { onError?: (error: unknown) => void },
|
|
451
477
|
): () => gpu.TextureId {
|
|
452
478
|
let make = (s: ShaderSpec) =>
|
|
453
|
-
gpu.createShaderTexture(s.fragmentSrc, s.width, s.height, s.params, {
|
|
479
|
+
gpu.createShaderTexture(s.fragmentSrc, s.width, s.height, s.params, {
|
|
480
|
+
textures: s.textures,
|
|
481
|
+
filter: s.filter,
|
|
482
|
+
wrap: s.wrap,
|
|
483
|
+
mipmap: s.mipmap,
|
|
484
|
+
})
|
|
454
485
|
let current = untrack(spec)
|
|
455
486
|
let currentId = make(current)
|
|
456
487
|
let [id, setId] = createSignal(currentId)
|
|
@@ -460,7 +491,8 @@ export function createShaderTextureMemo(
|
|
|
460
491
|
next.fragmentSrc === current.fragmentSrc &&
|
|
461
492
|
sameRecord(next.textures, current.textures) &&
|
|
462
493
|
next.filter === current.filter &&
|
|
463
|
-
next.wrap === current.wrap
|
|
494
|
+
next.wrap === current.wrap &&
|
|
495
|
+
next.mipmap === current.mipmap
|
|
464
496
|
) {
|
|
465
497
|
// Program and inputs unchanged: mutate in place, the id stays stable.
|
|
466
498
|
if (next.width !== current.width || next.height !== current.height) {
|
|
@@ -545,11 +577,14 @@ export function createPipelineTexture(
|
|
|
545
577
|
height: number,
|
|
546
578
|
params?: gpu.ShaderParams | null,
|
|
547
579
|
opts?: {
|
|
548
|
-
textures?:
|
|
580
|
+
textures?: gpu.TextureBindings
|
|
549
581
|
attributes?: gpu.VertexAttribute[]
|
|
550
582
|
buffer?: gpu.BufferId
|
|
551
|
-
instanceAttributes?: gpu.
|
|
583
|
+
instanceAttributes?: gpu.InstanceAttribute[]
|
|
552
584
|
instanceBuffer?: gpu.BufferId
|
|
585
|
+
/** One buffer per instance slot (index = the attributes' `slot`);
|
|
586
|
+
* pass this OR `instanceBuffer`, not both. */
|
|
587
|
+
instanceBuffers?: gpu.BufferId[]
|
|
553
588
|
topology?: gpu.Topology
|
|
554
589
|
depth?: boolean
|
|
555
590
|
depthWrite?: boolean
|
|
@@ -558,6 +593,7 @@ export function createPipelineTexture(
|
|
|
558
593
|
clearColor?: [number, number, number, number]
|
|
559
594
|
render?: "auto" | "manual"
|
|
560
595
|
loadOp?: "clear" | "load"
|
|
596
|
+
samples?: 1 | 2 | 4 | 8
|
|
561
597
|
} & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
|
|
562
598
|
CreateOptions &
|
|
563
599
|
SamplerOptions,
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from "./renderer"
|
|
2
|
-
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, onPointerMove } from "./core"
|
|
2
|
+
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, snapshotTexture, onPointerMove } from "./core"
|
|
3
3
|
export type { BoundingBox, GlobalPointerEvent, TextLine } from "./core"
|
|
4
4
|
export { parseColor, mixColors, brightness, createLinearGradient, createRadialGradient } from "./color"
|
|
5
5
|
export type { Gradient, GradientStop } from "./color"
|
|
@@ -17,9 +17,11 @@ export type { TextureId } from "./gpu"
|
|
|
17
17
|
export { createImage, decodeImage, encodeImage } from "./image"
|
|
18
18
|
export type { DecodedImage, ImageSource } from "./image"
|
|
19
19
|
export { parseSvg, svg } from "./svg"
|
|
20
|
+
export { Logo } from "./logo"
|
|
21
|
+
export type { LogoProps } from "./logo"
|
|
20
22
|
export type { SvgDocument, SvgDraw } from "./svg"
|
|
21
23
|
export { createScroll } from "./scroll"
|
|
22
|
-
export type { Scroll, ScrollAxis, ScrollOffset, ScrollOptions } from "./scroll"
|
|
24
|
+
export type { Scroll, ScrollAxis, ScrollBehavior, ScrollOffset, ScrollOptions, ScrollToOptions } from "./scroll"
|
|
23
25
|
export { arena } from "./arena"
|
|
24
26
|
export type { ArenaOwner } from "./arena"
|
|
25
27
|
export { createPan } from "./pan"
|
|
@@ -31,6 +33,10 @@ export type {
|
|
|
31
33
|
LayoutProps,
|
|
32
34
|
TransformProps,
|
|
33
35
|
PointerProps,
|
|
36
|
+
TransitionProps,
|
|
37
|
+
Transition,
|
|
38
|
+
TransitionPropName,
|
|
39
|
+
TransitionEndEvent,
|
|
34
40
|
PointerEvent,
|
|
35
41
|
WheelEvent,
|
|
36
42
|
KeyEvent,
|
package/src/logo.tsx
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The SolidRT brand mark: seven puzzle segments authored in a 100x100 space,
|
|
2
|
+
// each filled with its own light-to-dark gradient. Static by default; the
|
|
3
|
+
// animated forms stagger a fade per segment, either once (fade in and hold)
|
|
4
|
+
// or in a loop (in, then out, repeat) like the scaffold's welcome screen.
|
|
5
|
+
import { For, Show, createSignal } from "solid-js"
|
|
6
|
+
import { createLinearGradient } from "./color"
|
|
7
|
+
import { onFrame } from "./window"
|
|
8
|
+
|
|
9
|
+
export interface LogoProps {
|
|
10
|
+
// Rendered width and height in pixels; the mark is square. Default 100.
|
|
11
|
+
size?: number
|
|
12
|
+
// "none" draws the mark at full opacity and requests no frames (default).
|
|
13
|
+
// "once" fades the segments in one after the other and then holds; "loop"
|
|
14
|
+
// fades them in, then out, and repeats.
|
|
15
|
+
animation?: "none" | "once" | "loop"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Per segment: the delay of its fade in ms (its fade out follows at the same
|
|
19
|
+
// offset after the last fade in has completed), its gradient, its path.
|
|
20
|
+
const SEGMENTS = [
|
|
21
|
+
{ base: 0, light: "#3f5494", dark: "#162b6c", d: "M50.000 50.000 L28.330 50.000 C28.330 48.810 27.695 47.711 26.665 47.116 C25.635 46.521 24.365 46.521 23.335 47.116 C22.305 47.711 21.670 48.810 21.670 50.000 L0.000 50.000 L50.000 0.000 L50.000 9.170 C48.810 9.170 47.711 9.805 47.116 10.835 C46.521 11.865 46.521 13.135 47.116 14.165 C47.711 15.195 48.810 15.830 50.000 15.830 L50.000 25.000 L50.000 34.170 C48.810 34.170 47.711 34.805 47.116 35.835 C46.521 36.865 46.521 38.135 47.116 39.165 C47.711 40.195 48.810 40.830 50.000 40.830 L50.000 50.000 Z" },
|
|
22
|
+
{ base: 90, light: "#547ebf", dark: "#2b5696", d: "M50.000 50.000 L50.000 59.170 C48.810 59.170 47.711 59.805 47.116 60.835 C46.521 61.865 46.521 63.135 47.116 64.165 C47.711 65.195 48.810 65.830 50.000 65.830 L50.000 75.000 L50.000 84.170 C48.810 84.170 47.711 84.805 47.116 85.835 C46.521 86.865 46.521 88.135 47.116 89.165 C47.711 90.195 48.810 90.830 50.000 90.830 L50.000 100.000 L0.000 50.000 L21.670 50.000 C21.670 48.810 22.305 47.711 23.335 47.116 C24.365 46.521 25.635 46.521 26.665 47.116 C27.695 47.711 28.330 48.810 28.330 50.000 L50.000 50.000 Z" },
|
|
23
|
+
{ base: 180, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 25.000 L50.000 15.830 C48.810 15.830 47.711 15.195 47.116 14.165 C46.521 13.135 46.521 11.865 47.116 10.835 C47.711 9.805 48.810 9.170 50.000 9.170 L50.000 0.000 L75.000 25.000 L65.830 25.000 C65.830 26.190 65.195 27.289 64.165 27.884 C63.135 28.479 61.865 28.479 60.835 27.884 C59.805 27.289 59.170 26.190 59.170 25.000 L50.000 25.000 Z" },
|
|
24
|
+
{ base: 270, light: "#547ebf", dark: "#2b5696", d: "M50.000 25.000 L59.170 25.000 C59.170 26.190 59.805 27.289 60.835 27.884 C61.865 28.479 63.135 28.479 64.165 27.884 C65.195 27.289 65.830 26.190 65.830 25.000 L75.000 25.000 L75.000 34.170 C73.810 34.170 72.711 34.805 72.116 35.835 C71.521 36.865 71.521 38.135 72.116 39.165 C72.711 40.195 73.810 40.830 75.000 40.830 L75.000 50.000 L65.830 50.000 C65.830 48.810 65.195 47.711 64.165 47.116 C63.135 46.521 61.865 46.521 60.835 47.116 C59.805 47.711 59.170 48.810 59.170 50.000 L50.000 50.000 L50.000 40.830 C48.810 40.830 47.711 40.195 47.116 39.165 C46.521 38.135 46.521 36.865 47.116 35.835 C47.711 34.805 48.810 34.170 50.000 34.170 L50.000 25.000 Z" },
|
|
25
|
+
{ base: 360, light: "#7ea9ea", dark: "#5681c1", d: "M50.000 50.000 L59.170 50.000 C59.170 48.810 59.805 47.711 60.835 47.116 C61.865 46.521 63.135 46.521 64.165 47.116 C65.195 47.711 65.830 48.810 65.830 50.000 L75.000 50.000 L64.855 60.145 C64.013 59.304 62.787 58.976 61.638 59.283 C60.489 59.591 59.591 60.489 59.283 61.638 C58.976 62.787 59.304 64.013 60.145 64.855 L50.000 75.000 L50.000 65.830 C48.810 65.830 47.711 65.195 47.116 64.165 C46.521 63.135 46.521 61.865 47.116 60.835 C47.711 59.805 48.810 59.170 50.000 59.170 L50.000 50.000 Z" },
|
|
26
|
+
{ base: 450, light: "#3f5494", dark: "#162b6c", d: "M75.000 50.000 L75.000 59.170 C73.810 59.170 72.711 59.805 72.116 60.835 C71.521 61.865 71.521 63.135 72.116 64.165 C72.711 65.195 73.810 65.830 75.000 65.830 L75.000 75.000 L50.000 100.000 L50.000 90.830 C48.810 90.830 47.711 90.195 47.116 89.165 C46.521 88.135 46.521 86.865 47.116 85.835 C47.711 84.805 48.810 84.170 50.000 84.170 L50.000 75.000 L60.145 64.855 C59.304 64.013 58.976 62.787 59.283 61.638 C59.591 60.489 60.489 59.591 61.638 59.283 C62.787 58.976 64.013 59.304 64.855 60.145 L75.000 50.000 Z" },
|
|
27
|
+
{ base: 540, light: "#7ea9ea", dark: "#5681c1", d: "M100.000 50.000 L75.000 75.000 L75.000 65.830 C73.810 65.830 72.711 65.195 72.116 64.165 C71.521 63.135 71.521 61.865 72.116 60.835 C72.711 59.805 73.810 59.170 75.000 59.170 L75.000 50.000 L75.000 40.830 C73.810 40.830 72.711 40.195 72.116 39.165 C71.521 38.135 71.521 36.865 72.116 35.835 C72.711 34.805 73.810 34.170 75.000 34.170 L75.000 25.000 L100.000 50.000 Z" },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const FADE = 360
|
|
31
|
+
const LAST = SEGMENTS[SEGMENTS.length - 1]!.base
|
|
32
|
+
// The whole mark is visible from IN_DONE; a loop cycle fades out after that.
|
|
33
|
+
const IN_DONE = LAST + FADE
|
|
34
|
+
const CYCLE = IN_DONE + LAST + FADE
|
|
35
|
+
|
|
36
|
+
let clamp = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x)
|
|
37
|
+
let ease = (t: number) => 1 - (1 - t) * (1 - t)
|
|
38
|
+
let byte = (x: number) => Math.round(clamp(x) * 255).toString(16).padStart(2, "0")
|
|
39
|
+
|
|
40
|
+
export function Logo(props: LogoProps) {
|
|
41
|
+
let size = () => props.size ?? 100
|
|
42
|
+
let mode = () => props.animation ?? "none"
|
|
43
|
+
|
|
44
|
+
// Elapsed animation time; only advanced while an animated mode is mounted.
|
|
45
|
+
let [clock, setClock] = createSignal(0)
|
|
46
|
+
|
|
47
|
+
// The frame loop is mounted through the <Show> below so a mode change
|
|
48
|
+
// remounts it: onFrame holds a standing frame request while registered.
|
|
49
|
+
// "once" releases its request as soon as the last segment is in.
|
|
50
|
+
let Animate = () => {
|
|
51
|
+
let start = -1
|
|
52
|
+
let stop = onFrame((tick) => {
|
|
53
|
+
if (start < 0) start = tick
|
|
54
|
+
let t = tick - start
|
|
55
|
+
if (mode() === "loop") setClock(t % CYCLE)
|
|
56
|
+
else if (t < IN_DONE) setClock(t)
|
|
57
|
+
else {
|
|
58
|
+
setClock(IN_DONE)
|
|
59
|
+
stop()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let alpha = (seg: (typeof SEGMENTS)[number]) => {
|
|
66
|
+
if (mode() === "none") return 1
|
|
67
|
+
let t = clock()
|
|
68
|
+
if (t < seg.base) return 0
|
|
69
|
+
let fadeIn = clamp((t - seg.base) / FADE)
|
|
70
|
+
if (mode() === "once") return ease(fadeIn)
|
|
71
|
+
let end = IN_DONE + seg.base + FADE
|
|
72
|
+
if (t >= end) return 0
|
|
73
|
+
return ease(Math.min(fadeIn, clamp((end - t) / FADE)))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let fill = (seg: (typeof SEGMENTS)[number]) => {
|
|
77
|
+
let a = byte(alpha(seg))
|
|
78
|
+
return createLinearGradient(0, 0, 1, 1, [
|
|
79
|
+
{ offset: 0, color: seg.light + a },
|
|
80
|
+
{ offset: 1, color: seg.dark + a },
|
|
81
|
+
])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<view width={size()} height={size()} designSize={[100, 100]}>
|
|
86
|
+
<Show when={mode() !== "none"}>
|
|
87
|
+
<Animate />
|
|
88
|
+
</Show>
|
|
89
|
+
<For each={SEGMENTS}>{(seg) => <d-path d={seg.d} color={fill(seg)} />}</For>
|
|
90
|
+
</view>
|
|
91
|
+
)
|
|
92
|
+
}
|
package/src/renderer.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createRoot, onCleanup } from "@solidjs/signals"
|
|
1
|
+
import { createRoot, onCleanup, NotReadyError } from "@solidjs/signals"
|
|
2
2
|
import { createRenderer } from "@solidjs/universal"
|
|
3
|
+
import { createErrorBoundary } from "solid-js"
|
|
3
4
|
import type { Element } from "solid-js"
|
|
4
5
|
import * as tree from "flux:rendertree"
|
|
5
|
-
import { attachWindow } from "./window"
|
|
6
|
+
import { attachWindow, setWindowRoot } from "./window"
|
|
6
7
|
import { setEventHandler, setFocusable, setTextInputHints, cleanupNode, focusedNode, setFocus } from "./core"
|
|
7
8
|
|
|
8
9
|
export { getEventHandler } from "./core"
|
|
@@ -213,20 +214,7 @@ function applyProp<T>(node: ProxyNode, name: string, value: T): void {
|
|
|
213
214
|
setTreeProperty(node, name, value)
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
effect,
|
|
218
|
-
memo,
|
|
219
|
-
createComponent,
|
|
220
|
-
createElement,
|
|
221
|
-
createTextNode,
|
|
222
|
-
insertNode,
|
|
223
|
-
insert,
|
|
224
|
-
spread,
|
|
225
|
-
setProp,
|
|
226
|
-
mergeProps,
|
|
227
|
-
applyRef,
|
|
228
|
-
ref,
|
|
229
|
-
} = createRenderer<ProxyNode>({
|
|
217
|
+
let renderer = createRenderer<ProxyNode>({
|
|
230
218
|
createElement: (elementType: string, props?: Record<string, any>): ProxyNode => {
|
|
231
219
|
let proxy = createProxyNode(elementType)
|
|
232
220
|
|
|
@@ -235,11 +223,13 @@ export let {
|
|
|
235
223
|
if (elementType === "window") tree.createRoot(proxy.id)
|
|
236
224
|
else tree.createNode(proxy.id, elementType)
|
|
237
225
|
|
|
238
|
-
// The universal JSX template hands static props here as an object
|
|
239
|
-
//
|
|
226
|
+
// The universal JSX template hands static props here as an object. The
|
|
227
|
+
// compiler routes children/ref expressions through their own hooks, so
|
|
228
|
+
// those names only appear here in degenerate literal forms (children="hi",
|
|
229
|
+
// a bare ref) - let them flow to the tree so the unknown-property warning
|
|
230
|
+
// reports them instead of dropping them silently.
|
|
240
231
|
if (props) {
|
|
241
232
|
for (let name in props) {
|
|
242
|
-
if (name === "children" || name === "ref") continue
|
|
243
233
|
applyProp(proxy, name, props[name])
|
|
244
234
|
}
|
|
245
235
|
}
|
|
@@ -309,28 +299,185 @@ export let {
|
|
|
309
299
|
},
|
|
310
300
|
})
|
|
311
301
|
|
|
312
|
-
|
|
313
|
-
|
|
302
|
+
export let { memo, createComponent, createElement, createTextNode, insertNode, spread, setProp, mergeProps, applyRef, ref } =
|
|
303
|
+
renderer
|
|
304
|
+
let { effect: rawEffect, insert: rawInsert } = renderer
|
|
305
|
+
|
|
306
|
+
// ------ Per-node error containment --------
|
|
307
|
+
//
|
|
308
|
+
// Every reactive write into the tree goes through two exports the compiled
|
|
309
|
+
// JSX calls: `effect` (an element's dynamic props) and `insert` (a child
|
|
310
|
+
// expression). An error thrown while computing either is contained right
|
|
311
|
+
// there: the props or children keep their last good value, the effect stays
|
|
312
|
+
// subscribed (the throwing read is tracked, so fixing it recomputes and the
|
|
313
|
+
// node recovers on its own), and the rest of the app keeps running. Without
|
|
314
|
+
// this one unclaimed error halts the whole reactive system. A NotReadyError
|
|
315
|
+
// is not an error but a pending async read on its way to the nearest
|
|
316
|
+
// <Loading>, so it passes through. Reported once per site until it recovers,
|
|
317
|
+
// not once per run. What escapes these two (a user createEffect that throws)
|
|
318
|
+
// still reaches render()'s root boundary.
|
|
319
|
+
const SKIP = Symbol("skip")
|
|
320
|
+
|
|
321
|
+
function guard<T>(fn: (prev?: T) => T, describe: () => string, nested: boolean, empty: T): (prev?: T) => T {
|
|
322
|
+
let last = empty
|
|
323
|
+
let failing = false
|
|
324
|
+
return (prev?: T) => {
|
|
325
|
+
try {
|
|
326
|
+
let value = fn(prev === SKIP ? undefined : prev)
|
|
327
|
+
if (failing) {
|
|
328
|
+
failing = false
|
|
329
|
+
console.warn(`Recovered: ${describe()} computes again`)
|
|
330
|
+
}
|
|
331
|
+
// A child expression resolving to a function is read by an inner
|
|
332
|
+
// effect (universal's insert); that read gets the same containment.
|
|
333
|
+
// Solid's flatten only unwraps zero-arity functions and inserts any
|
|
334
|
+
// other function as a node, so only accessors are wrapped, and the
|
|
335
|
+
// wrapper keeps arity 0.
|
|
336
|
+
if (nested && typeof value === "function" && value.length === 0) {
|
|
337
|
+
let inner = guard(value as any, describe, true, empty)
|
|
338
|
+
value = (() => inner()) as any
|
|
339
|
+
}
|
|
340
|
+
last = value
|
|
341
|
+
return value
|
|
342
|
+
} catch (e) {
|
|
343
|
+
if (e instanceof NotReadyError) throw e
|
|
344
|
+
if (!failing) {
|
|
345
|
+
failing = true
|
|
346
|
+
console.error(`Contained error: ${describe()} threw and keeps its last value until it computes again.`, e)
|
|
347
|
+
}
|
|
348
|
+
return last
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Universal's declarations trail its runtime (effect takes options, insert
|
|
354
|
+
// takes initial and options), so the wrappers carry the runtime signatures.
|
|
355
|
+
type EffectFn = <T>(fn: (prev?: T) => T, effectFn?: (value: T, prev?: T) => void, options?: unknown) => void
|
|
356
|
+
type InsertFn = (parent: ProxyNode, accessor: unknown, marker?: unknown, initial?: unknown, options?: unknown) => ProxyNode
|
|
357
|
+
let effectRaw = rawEffect as unknown as EffectFn
|
|
358
|
+
let insertRaw = rawInsert as unknown as InsertFn
|
|
359
|
+
|
|
360
|
+
export let effect: EffectFn = (fn, effectFn, options) =>
|
|
361
|
+
effectRaw<any>(
|
|
362
|
+
guard<any>(fn, () => "an element's prop expression", false, SKIP),
|
|
363
|
+
effectFn && ((value, prev) => (value === SKIP ? undefined : effectFn(value, prev === SKIP ? undefined : prev))),
|
|
364
|
+
options,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
export let insert: InsertFn = (parent, accessor, marker, initial, options) =>
|
|
368
|
+
insertRaw(
|
|
369
|
+
parent,
|
|
370
|
+
typeof accessor === "function"
|
|
371
|
+
? guard(accessor as any, () => `a child expression of <${parent.elementType}> ${getNodePath(parent.id).join("/")}`, true, undefined)
|
|
372
|
+
: accessor,
|
|
373
|
+
marker,
|
|
374
|
+
initial,
|
|
375
|
+
options,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
// The current <window> node: the app's, or the error window standing in for
|
|
379
|
+
// it. Serves as the default mount target for createPortal (single window by
|
|
380
|
+
// design, so one ambient ref).
|
|
314
381
|
let windowRoot: ProxyNode | undefined
|
|
382
|
+
let rendered = false
|
|
383
|
+
// Ids of error windows built by the root boundary, alive only while shown.
|
|
384
|
+
let errorWindows = new Set<number>()
|
|
315
385
|
|
|
316
386
|
/**
|
|
317
387
|
* Mounts a SolidRT app. Call once at the top level: `render(() => <App />)`.
|
|
318
388
|
* The element returned by `code` MUST be a `<window>` (it becomes the native
|
|
319
389
|
* window and root of the render tree); anything else throws. Runs inside a
|
|
320
390
|
* reactive root, so the whole tree is disposed together on engine reload.
|
|
391
|
+
*
|
|
392
|
+
* The whole app, window included, sits inside an error boundary: an error no
|
|
393
|
+
* <Errored> claims replaces the app's window with an error window (message,
|
|
394
|
+
* stack, a reset button) instead of halting the reactive system for good.
|
|
395
|
+
* The app's subtree stays alive behind it - the boundary keeps it and marks
|
|
396
|
+
* only the failed computations - so reset recomputes those in place and the
|
|
397
|
+
* same window node comes back.
|
|
321
398
|
*/
|
|
322
399
|
export function render(code: () => any) {
|
|
400
|
+
// Once per app: there is no unmount; teardown is engine teardown.
|
|
401
|
+
if (rendered) {
|
|
402
|
+
throw new Error("render() already called; an app has exactly one render()")
|
|
403
|
+
}
|
|
404
|
+
rendered = true
|
|
323
405
|
createRoot(() => {
|
|
324
|
-
let root =
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
406
|
+
let root = createErrorBoundary(
|
|
407
|
+
() => {
|
|
408
|
+
let win = code()
|
|
409
|
+
if (!win || win.elementType !== "window") {
|
|
410
|
+
throw new Error("render() root must be a <window> element")
|
|
411
|
+
}
|
|
412
|
+
return win
|
|
413
|
+
},
|
|
414
|
+
(error, reset) => {
|
|
415
|
+
// The boundary hands the error as an accessor.
|
|
416
|
+
let err = error()
|
|
417
|
+
console.error("Uncaught error: the app is replaced by the error window until reset or reload.", err)
|
|
418
|
+
let win = errorWindow(err, reset)
|
|
419
|
+
errorWindows.add(win.id)
|
|
420
|
+
return win
|
|
421
|
+
},
|
|
422
|
+
)
|
|
423
|
+
rawEffect(
|
|
424
|
+
() => root() as ProxyNode,
|
|
425
|
+
(win, prev) => swapRoot(win, prev),
|
|
426
|
+
)
|
|
331
427
|
})
|
|
332
428
|
}
|
|
333
429
|
|
|
430
|
+
// The boundary's value changed: the app's window on mount and after a
|
|
431
|
+
// successful reset, an error window after an error. Creating a window already
|
|
432
|
+
// made it the native root; setRoot is the way back to an existing one.
|
|
433
|
+
function swapRoot(win: ProxyNode, prev?: ProxyNode) {
|
|
434
|
+
windowRoot = win
|
|
435
|
+
if (prev === undefined) {
|
|
436
|
+
attachWindow(win.id)
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
// Creating the error window made it the native root; the app's window
|
|
440
|
+
// coming back is the case that needs the explicit way back.
|
|
441
|
+
if (!errorWindows.has(win.id)) tree.setRoot(win.id)
|
|
442
|
+
setWindowRoot(win.id)
|
|
443
|
+
// Keys route to the focused node; one inside the hidden window must not
|
|
444
|
+
// keep hearing them.
|
|
445
|
+
setFocus(null)
|
|
446
|
+
// The app's window survives behind an error window (the boundary keeps its
|
|
447
|
+
// subtree for reset); an error window replaced by anything is dead.
|
|
448
|
+
if (errorWindows.has(prev.id) || !errorWindows.has(win.id)) {
|
|
449
|
+
errorWindows.delete(prev.id)
|
|
450
|
+
destroyNode(prev)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// The error window: the in-app sibling of the runtime's startup BSOD, built
|
|
455
|
+
// from the primitives directly (no JSX in core). Static content; the reset
|
|
456
|
+
// button recomputes the failed sources, and a reload replaces everything.
|
|
457
|
+
function errorWindow(err: unknown, reset: () => void): ProxyNode {
|
|
458
|
+
let message = err instanceof Error ? err.message : String(err)
|
|
459
|
+
let stack = err instanceof Error && err.stack ? err.stack : ""
|
|
460
|
+
let text = (content: string, props: Record<string, any>) => {
|
|
461
|
+
let node = createElement("text", props)
|
|
462
|
+
insertNode(node, createTextNode(content))
|
|
463
|
+
return node
|
|
464
|
+
}
|
|
465
|
+
let win = createElement("window", { title: "Application error" })
|
|
466
|
+
insertNode(win, createElement("d-rect", { color: "#1144bb" }))
|
|
467
|
+
let column = createElement("view", { flexGrow: 1, flexDirection: "column", padding: 40, gap: 12 })
|
|
468
|
+
insertNode(column, text(":(", { color: "white", fontSize: 64, fontWeight: 700 }))
|
|
469
|
+
insertNode(column, text("Something went wrong", { color: "white", fontSize: 22 }))
|
|
470
|
+
insertNode(column, text(message, { color: "white", fontSize: 16 }))
|
|
471
|
+
if (stack) insertNode(column, text(stack, { color: "#aac2ff", fontSize: 12, fontFamily: "mono" }))
|
|
472
|
+
insertNode(column, text("Fix the error and save to reload, or reset to retry the failed computations.", { color: "#aac2ff", fontSize: 14 }))
|
|
473
|
+
let button = createElement("view", { alignSelf: "flex-start", padding: 12, onPointerDown: () => reset() })
|
|
474
|
+
insertNode(button, createElement("d-rect", { color: "white", radius: 6 }))
|
|
475
|
+
insertNode(button, text("Reset", { color: "#1144bb", fontSize: 16, fontWeight: 600 }))
|
|
476
|
+
insertNode(column, button)
|
|
477
|
+
insertNode(win, column)
|
|
478
|
+
return win
|
|
479
|
+
}
|
|
480
|
+
|
|
334
481
|
/**
|
|
335
482
|
* Relocates an already-built node out of its lexical position to `mount` (the
|
|
336
483
|
* window root by default), then removes it again when the surrounding reactive
|
|
@@ -362,6 +509,11 @@ export function createPortal(node: Element, mount?: ProxyNode): null {
|
|
|
362
509
|
throw new Error("createPortal: node must be a single built element")
|
|
363
510
|
}
|
|
364
511
|
insertNode(target, node as ProxyNode)
|
|
365
|
-
|
|
512
|
+
// A destroyed mount target has already swept the portaled node with it
|
|
513
|
+
// (destroy walks the mount tree), so detaching then would hand freed ids to
|
|
514
|
+
// the native side, which panics. Gone from the proxy map = already freed.
|
|
515
|
+
onCleanup(() => {
|
|
516
|
+
if (nodes.has((node as ProxyNode).id)) removeNode(target, node as ProxyNode)
|
|
517
|
+
})
|
|
366
518
|
return null
|
|
367
519
|
}
|
package/src/runtime-modules.d.ts
CHANGED
|
@@ -47,6 +47,10 @@ declare module "*.ogg" {
|
|
|
47
47
|
const bytes: Uint8Array
|
|
48
48
|
export default bytes
|
|
49
49
|
}
|
|
50
|
+
declare module "*.glb" {
|
|
51
|
+
const bytes: Uint8Array
|
|
52
|
+
export default bytes
|
|
53
|
+
}
|
|
50
54
|
|
|
51
55
|
// UI event bus (lattice), provided by the runtime as a builtin module.
|
|
52
56
|
// on/once return an unsubscribe function. Notable events: the routed pointer
|