@solidrt/core 0.0.50 → 0.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +102 -21
- package/README.md +1 -1
- package/agents/painting.md +61 -0
- package/agents/performance.md +216 -0
- package/docs/index.md +154 -0
- package/docs/reference/detached.md +85 -0
- package/docs/reference/drawing.md +95 -0
- package/docs/reference/elements.md +56 -0
- package/docs/reference/gpu.md +204 -0
- package/docs/reference/index.md +50 -0
- package/docs/reference/input.md +58 -0
- package/docs/reference/layout.md +44 -0
- package/docs/reference/shaders.md +46 -0
- package/docs/reference/text.md +46 -0
- package/docs/reference/transforms.md +35 -0
- package/docs/reference/types.md +34 -0
- package/examples/README.md +7 -5
- package/examples/{sound.tsx → audio.tsx} +1 -1
- package/examples/gpu-pipeline.tsx +2 -2
- package/examples/gpu-sprites.tsx +102 -0
- package/examples/line-points.tsx +145 -0
- package/examples/parse-svg.tsx +6 -6
- package/examples/responsive-grid.tsx +1 -1
- package/examples/scroll.tsx +2 -2
- package/examples/snapshot-texture.tsx +72 -0
- package/examples/{view-viewbox.tsx → view-design-size.tsx} +33 -15
- package/jsx-runtime.d.ts +15 -14
- package/package.json +11 -9
- package/src/{sound.ts → audio.ts} +68 -15
- package/src/color.ts +17 -18
- package/src/core.ts +40 -3
- package/src/data.ts +99 -0
- package/src/gpu.ts +88 -34
- package/src/index.ts +9 -3
- package/src/logo.tsx +92 -0
- package/src/renderer.ts +219 -53
- package/src/runtime-modules.d.ts +7 -2
- package/src/scroll.ts +51 -15
- package/src/svg.ts +1 -1
- package/src/text-input.ts +297 -61
- package/src/types.d.ts +291 -31
- package/src/window.ts +110 -14
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
// `
|
|
2
|
-
// author the whole scene once, in your own made-up design units, and the
|
|
3
|
-
// scales that space to fit its box. It is SVG's viewBox generalized
|
|
4
|
-
//
|
|
1
|
+
// `designSize` on a <view> is the fixed-aspect answer to "many screen sizes":
|
|
2
|
+
// you author the whole scene once, in your own made-up design units, and the
|
|
3
|
+
// view scales that space to fit its box. It is SVG's viewBox generalized off
|
|
4
|
+
// the graphics format and onto a layout element - the design size is the only
|
|
5
|
+
// irreducible half of it, since a transform already does what min-x/min-y did.
|
|
5
6
|
//
|
|
6
7
|
// Four facts, each demonstrated below:
|
|
7
|
-
// 1.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// 1. The view sizes like a REPLACED element (think <img>): its intrinsic size
|
|
9
|
+
// is the design size, one sized axis derives the other from the design
|
|
10
|
+
// aspect, and layout props override it as usual - here `flex={1}` takes
|
|
11
|
+
// the whole window. It never refuses to shrink: a design has no size it
|
|
12
|
+
// cannot scale below.
|
|
10
13
|
// 2. It fits UNIFORMLY and centers - one scale for both axes, SVG's default
|
|
11
14
|
// preserveAspectRatio. Content never stretches; the leftover on the loose
|
|
12
15
|
// axis is letterbox, showing the window background through it.
|
|
13
|
-
// 3. Children live in DESIGN space.
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
16
|
+
// 3. Children live in DESIGN space, laid-out ones included. x/y, w/h,
|
|
17
|
+
// fontSize, stroke widths, flex, percentages, text wrapping - everything
|
|
18
|
+
// resolves against the design size, not the box. The box a child inherits
|
|
19
|
+
// IS the design size, so a bare `d-rect` fills the design space, detached
|
|
20
|
+
// text wraps at its width, and a flex row is laid out at the design width
|
|
21
|
+
// whatever the window - nothing reflows on resize, the fit does the work.
|
|
17
22
|
// 4. Pointer coordinates arrive in design space too. localX/localY on the
|
|
18
|
-
//
|
|
23
|
+
// design-size view (and on anything under it) read in design units, so no
|
|
19
24
|
// scale factor is threaded through the app's hit math.
|
|
20
25
|
//
|
|
21
26
|
// The payoff: no `windowSizeClass` branching, no per-breakpoint sizes, no
|
|
@@ -44,14 +49,15 @@ function App() {
|
|
|
44
49
|
<window>
|
|
45
50
|
<d-rect color="#0b0f17" />
|
|
46
51
|
|
|
47
|
-
{/* flex={1}
|
|
52
|
+
{/* flex={1} overrides the intrinsic design size: the box is the whole
|
|
53
|
+
window, and the fit maps the design into it (fact 1). */}
|
|
48
54
|
<view
|
|
49
55
|
flex={1}
|
|
50
|
-
|
|
56
|
+
designSize={[DESIGN_W, DESIGN_H]}
|
|
51
57
|
onPointerMove={(e) => setAt({ x: e.localX, y: e.localY })}
|
|
52
58
|
onPointerLeave={() => setAt(null)}
|
|
53
59
|
>
|
|
54
|
-
{/* No w/h, so it fills the box it inherits - which under a
|
|
60
|
+
{/* No w/h, so it fills the box it inherits - which under a design size is
|
|
55
61
|
the design space (fact 3). Its edges are the letterbox edges. */}
|
|
56
62
|
<d-rect color="#151b28" />
|
|
57
63
|
|
|
@@ -78,6 +84,18 @@ function App() {
|
|
|
78
84
|
<Show when={at()}>
|
|
79
85
|
{(a) => <d-oval x={a().x - 8} y={a().y - 8} w={16} h={16} color="#f85149" />}
|
|
80
86
|
</Show>
|
|
87
|
+
|
|
88
|
+
{/* A laid-out row under the design size (fact 3): positioned and sized in
|
|
89
|
+
design units, the bar flexing against the design width. Resize the
|
|
90
|
+
window: it scales with the scene instead of reflowing. */}
|
|
91
|
+
<view position="absolute" left={40} right={40} top={364} flexDirection="row" alignItems="center" gap={12}>
|
|
92
|
+
<text fontSize={16} color="#8b949e">
|
|
93
|
+
flex row, laid out in design units
|
|
94
|
+
</text>
|
|
95
|
+
<view flex={1} height={10}>
|
|
96
|
+
<d-rect radius={5} color="#1f6feb" />
|
|
97
|
+
</view>
|
|
98
|
+
</view>
|
|
81
99
|
</view>
|
|
82
100
|
</window>
|
|
83
101
|
)
|
package/jsx-runtime.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
OvalGeometryProps,
|
|
16
16
|
TextGeometryProps,
|
|
17
17
|
LineGeometryProps,
|
|
18
|
+
TransitionProps,
|
|
18
19
|
Element as CoreElement,
|
|
19
20
|
ElementChildrenAttribute as CoreElementChildrenAttribute
|
|
20
21
|
} from "./src/types"
|
|
@@ -41,20 +42,20 @@ export namespace JSX {
|
|
|
41
42
|
// layout box; d-* forms compose the paint-space geometry props instead.
|
|
42
43
|
interface IntrinsicElements {
|
|
43
44
|
window: WindowProps & ElementRef
|
|
44
|
-
view: ViewProps & ElementRef
|
|
45
|
-
text: TextProps & LayoutProps & ElementRef
|
|
46
|
-
rect: RectProps & LayoutProps & ElementRef
|
|
47
|
-
oval: OvalProps & LayoutProps & ElementRef
|
|
48
|
-
line: LineProps & LayoutProps & ElementRef
|
|
49
|
-
path: PathProps & LayoutProps & ElementRef
|
|
50
|
-
texture: TextureProps & LayoutProps & ElementRef
|
|
51
|
-
"d-view": ViewOwnProps & ElementRef
|
|
52
|
-
"d-rect": RectProps & GeometryProps & ElementRef
|
|
53
|
-
"d-oval": OvalProps & OvalGeometryProps & ElementRef
|
|
54
|
-
"d-line": LineProps & LineGeometryProps & ElementRef
|
|
55
|
-
"d-path": PathProps & PositionProps & ElementRef
|
|
56
|
-
"d-texture": TextureProps & GeometryProps & ElementRef
|
|
57
|
-
"d-text": TextProps & TextGeometryProps & ElementRef
|
|
45
|
+
view: ViewProps & TransitionProps & ElementRef
|
|
46
|
+
text: TextProps & LayoutProps & TransitionProps & ElementRef
|
|
47
|
+
rect: RectProps & LayoutProps & TransitionProps & ElementRef
|
|
48
|
+
oval: OvalProps & LayoutProps & TransitionProps & ElementRef
|
|
49
|
+
line: LineProps & LayoutProps & TransitionProps & ElementRef
|
|
50
|
+
path: PathProps & LayoutProps & TransitionProps & ElementRef
|
|
51
|
+
texture: TextureProps & LayoutProps & TransitionProps & ElementRef
|
|
52
|
+
"d-view": ViewOwnProps & TransitionProps & ElementRef
|
|
53
|
+
"d-rect": RectProps & GeometryProps & TransitionProps & ElementRef
|
|
54
|
+
"d-oval": OvalProps & OvalGeometryProps & TransitionProps & ElementRef
|
|
55
|
+
"d-line": LineProps & LineGeometryProps & TransitionProps & ElementRef
|
|
56
|
+
"d-path": PathProps & PositionProps & TransitionProps & ElementRef
|
|
57
|
+
"d-texture": TextureProps & GeometryProps & TransitionProps & ElementRef
|
|
58
|
+
"d-text": TextProps & TextGeometryProps & TransitionProps & ElementRef
|
|
58
59
|
// A styled run inside <text>/<d-text>; never has a layout box, so there is
|
|
59
60
|
// no d- form.
|
|
60
61
|
span: SpanProps & ElementRef
|
package/package.json
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.52",
|
|
4
4
|
"license": "MIT",
|
|
5
|
+
"funding": "https://github.com/sponsors/wellawaretech",
|
|
5
6
|
"author": "Antoine van Wel",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"main": "src/index.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": "./src/index.ts",
|
|
10
11
|
"./camera": "./src/camera.ts",
|
|
12
|
+
"./color": "./src/color.ts",
|
|
13
|
+
"./data": "./src/data.ts",
|
|
11
14
|
"./gpu": "./src/gpu.ts",
|
|
12
15
|
"./image": "./src/image.ts",
|
|
16
|
+
"./audio": "./src/audio.ts",
|
|
13
17
|
"./microphone": "./src/microphone.ts",
|
|
14
|
-
"./sound": "./src/sound.ts",
|
|
15
18
|
"./speech-recognition": "./src/speech-recognition.ts",
|
|
16
19
|
"./text-input": "./src/text-input.ts",
|
|
17
20
|
"./jsx-runtime": "./jsx-runtime.d.ts",
|
|
@@ -21,17 +24,16 @@
|
|
|
21
24
|
"src/",
|
|
22
25
|
"examples/",
|
|
23
26
|
"jsx-runtime.d.ts",
|
|
27
|
+
"agents/",
|
|
28
|
+
"docs/",
|
|
24
29
|
"AGENTS.md"
|
|
25
30
|
],
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"colord": "^2.9.3"
|
|
28
|
-
},
|
|
29
31
|
"devDependencies": {
|
|
30
|
-
"@solidrt/flux-types": "0.0.
|
|
32
|
+
"@solidrt/flux-types": "0.0.52"
|
|
31
33
|
},
|
|
32
34
|
"peerDependencies": {
|
|
33
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
34
|
-
"@solidjs/universal": "2.0.0-rc.
|
|
35
|
-
"solid-js": "2.0.0-rc.
|
|
35
|
+
"@solidjs/signals": "2.0.0-rc.1",
|
|
36
|
+
"@solidjs/universal": "2.0.0-rc.1",
|
|
37
|
+
"solid-js": "2.0.0-rc.1"
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -27,6 +27,15 @@ export type SoundOptions = {
|
|
|
27
27
|
* Omitted means unspatialized.
|
|
28
28
|
*/
|
|
29
29
|
pan?: number
|
|
30
|
+
/**
|
|
31
|
+
* Playback rate: 1.0 plays as loaded, higher is faster and higher-pitched
|
|
32
|
+
* (clamped to [0.01, 100]). Defaults to 1.0.
|
|
33
|
+
*/
|
|
34
|
+
rate?: number
|
|
35
|
+
/** Fade each play() in from silence over this many milliseconds. */
|
|
36
|
+
fadeInMs?: number
|
|
37
|
+
/** Bus name for every voice of this sound (see flux:audio `stop({ bus })`). */
|
|
38
|
+
bus?: string
|
|
30
39
|
/**
|
|
31
40
|
* Let play() stack overlapping voices instead of restarting. Defaults to
|
|
32
41
|
* true: rapid triggers overlap. Set false for a single-voice sound where each
|
|
@@ -35,6 +44,21 @@ export type SoundOptions = {
|
|
|
35
44
|
overlap?: boolean
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
/** Options for the live setters: ramp to the value instead of jumping. */
|
|
48
|
+
export type SoundRampOptions = {
|
|
49
|
+
/**
|
|
50
|
+
* Reach the new value over this many milliseconds, engine-smoothed (immune
|
|
51
|
+
* to frame hitches). Omitted (or 0) sets immediately.
|
|
52
|
+
*/
|
|
53
|
+
rampMs?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Options for {@link Sound.stop}. */
|
|
57
|
+
export type SoundStopOptions = {
|
|
58
|
+
/** Fade to silence over this many milliseconds before stopping. */
|
|
59
|
+
fadeOutMs?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
/** Options for a PCM sound: `SoundOptions` plus the channel count. */
|
|
39
63
|
export type PcmSoundOptions = SoundOptions & {
|
|
40
64
|
/** Channel count, interleaved samples when 2. Defaults to 1 (mono). */
|
|
@@ -49,18 +73,26 @@ export type SoundStreamOptions = {
|
|
|
49
73
|
gain?: number
|
|
50
74
|
/** Stereo position in [-1, 1] (see {@link SoundOptions.pan}). */
|
|
51
75
|
pan?: number
|
|
76
|
+
/** Playback rate (see {@link SoundOptions.rate}). Defaults to 1.0. */
|
|
77
|
+
rate?: number
|
|
78
|
+
/** Fade each play() in from silence over this many milliseconds. */
|
|
79
|
+
fadeInMs?: number
|
|
80
|
+
/** Bus name for the stream's voice (see flux:audio `stop({ bus })`). */
|
|
81
|
+
bus?: string
|
|
52
82
|
}
|
|
53
83
|
|
|
54
84
|
/** A decoded sound with reactive lifecycle. */
|
|
55
85
|
export type Sound = {
|
|
56
86
|
/** Start the clip. Overlaps or restarts per the `overlap` option. */
|
|
57
87
|
play(): void
|
|
58
|
-
/** Stop every voice started from this sound. */
|
|
59
|
-
stop(): void
|
|
88
|
+
/** Stop every voice started from this sound, fading first if asked. */
|
|
89
|
+
stop(options?: SoundStopOptions): void
|
|
60
90
|
/** Set the volume of every live voice, and of voices started later. */
|
|
61
|
-
setGain(gain: number): void
|
|
91
|
+
setGain(gain: number, options?: SoundRampOptions): void
|
|
62
92
|
/** Set the stereo position of every live voice, and of voices started later. */
|
|
63
|
-
setPan(pan: number): void
|
|
93
|
+
setPan(pan: number, options?: SoundRampOptions): void
|
|
94
|
+
/** Set the playback rate of every live voice, and of voices started later. */
|
|
95
|
+
setRate(rate: number, options?: SoundRampOptions): void
|
|
64
96
|
/** True after play() until stop() (does not track natural completion). */
|
|
65
97
|
playing(): boolean
|
|
66
98
|
/** Set if loading failed. */
|
|
@@ -69,12 +101,12 @@ export type Sound = {
|
|
|
69
101
|
|
|
70
102
|
// Shared reactive wrapper: owns the loaded clip, tracks live voices, and
|
|
71
103
|
// disposes both on cleanup. `loader` runs once (may throw -> error signal).
|
|
72
|
-
// Gain and
|
|
73
|
-
// the sound, not back at the initial options.
|
|
104
|
+
// Gain, pan and rate are remembered so later voices start where the setters
|
|
105
|
+
// left the sound, not back at the initial options.
|
|
74
106
|
function reactiveSound(
|
|
75
107
|
loader: () => Clip,
|
|
76
108
|
overlap: boolean,
|
|
77
|
-
initial: { loop?: boolean; gain?: number; pan?: number },
|
|
109
|
+
initial: { loop?: boolean; gain?: number; pan?: number; rate?: number; fadeInMs?: number; bus?: string },
|
|
78
110
|
): Sound {
|
|
79
111
|
let [error, setError] = createSignal<Error | undefined>(undefined, { ownedWrite: true })
|
|
80
112
|
let [playing, setPlaying] = createSignal(false, { ownedWrite: true })
|
|
@@ -84,6 +116,9 @@ function reactiveSound(
|
|
|
84
116
|
let loop = initial.loop
|
|
85
117
|
let gain = initial.gain
|
|
86
118
|
let pan = initial.pan
|
|
119
|
+
let rate = initial.rate
|
|
120
|
+
let fadeInMs = initial.fadeInMs
|
|
121
|
+
let bus = initial.bus
|
|
87
122
|
try {
|
|
88
123
|
clip = loader()
|
|
89
124
|
} catch (e) {
|
|
@@ -96,8 +131,8 @@ function reactiveSound(
|
|
|
96
131
|
voices = voices.filter((v) => !v.ended())
|
|
97
132
|
}
|
|
98
133
|
|
|
99
|
-
let stopAll = () => {
|
|
100
|
-
for (let v of voices) v.stop()
|
|
134
|
+
let stopAll = (options?: SoundStopOptions) => {
|
|
135
|
+
for (let v of voices) v.stop(options)
|
|
101
136
|
voices = []
|
|
102
137
|
setPlaying(false)
|
|
103
138
|
}
|
|
@@ -115,19 +150,24 @@ function reactiveSound(
|
|
|
115
150
|
if (!clip) return
|
|
116
151
|
if (overlap) prune()
|
|
117
152
|
else stopAll()
|
|
118
|
-
voices.push(clip.play({ loop, gain, pan }))
|
|
153
|
+
voices.push(clip.play({ loop, gain, pan, rate, fadeInMs, bus }))
|
|
119
154
|
setPlaying(true)
|
|
120
155
|
},
|
|
121
156
|
stop: stopAll,
|
|
122
|
-
setGain(value) {
|
|
157
|
+
setGain(value, options) {
|
|
123
158
|
gain = value
|
|
124
159
|
prune()
|
|
125
|
-
for (let v of voices) v.setGain(value)
|
|
160
|
+
for (let v of voices) v.setGain(value, options)
|
|
126
161
|
},
|
|
127
|
-
setPan(value) {
|
|
162
|
+
setPan(value, options) {
|
|
128
163
|
pan = value
|
|
129
164
|
prune()
|
|
130
|
-
for (let v of voices) v.setPan(value)
|
|
165
|
+
for (let v of voices) v.setPan(value, options)
|
|
166
|
+
},
|
|
167
|
+
setRate(value, options) {
|
|
168
|
+
rate = value
|
|
169
|
+
prune()
|
|
170
|
+
for (let v of voices) v.setRate(value, options)
|
|
131
171
|
},
|
|
132
172
|
playing,
|
|
133
173
|
error,
|
|
@@ -144,6 +184,9 @@ export function createSound(source: Uint8Array, options: SoundOptions = {}): Sou
|
|
|
144
184
|
loop: options.loop,
|
|
145
185
|
gain: options.gain,
|
|
146
186
|
pan: options.pan,
|
|
187
|
+
rate: options.rate,
|
|
188
|
+
fadeInMs: options.fadeInMs,
|
|
189
|
+
bus: options.bus,
|
|
147
190
|
})
|
|
148
191
|
}
|
|
149
192
|
|
|
@@ -165,6 +208,9 @@ export function createPcmSound(
|
|
|
165
208
|
loop: options.loop,
|
|
166
209
|
gain: options.gain,
|
|
167
210
|
pan: options.pan,
|
|
211
|
+
rate: options.rate,
|
|
212
|
+
fadeInMs: options.fadeInMs,
|
|
213
|
+
bus: options.bus,
|
|
168
214
|
})
|
|
169
215
|
}
|
|
170
216
|
|
|
@@ -178,5 +224,12 @@ export function createPcmSound(
|
|
|
178
224
|
*/
|
|
179
225
|
export function createSoundStream(source: string | FluxFile, options: SoundStreamOptions = {}): Sound {
|
|
180
226
|
let src = typeof source === "string" ? file(source) : source
|
|
181
|
-
return reactiveSound(() => stream(src), false, {
|
|
227
|
+
return reactiveSound(() => stream(src), false, {
|
|
228
|
+
loop: options.loop,
|
|
229
|
+
gain: options.gain,
|
|
230
|
+
pan: options.pan,
|
|
231
|
+
rate: options.rate,
|
|
232
|
+
fadeInMs: options.fadeInMs,
|
|
233
|
+
bus: options.bus,
|
|
234
|
+
})
|
|
182
235
|
}
|
package/src/color.ts
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import * as tree from "flux:rendertree"
|
|
2
|
+
|
|
3
|
+
// Color parsing and mixing live in the runtime (alloy's color module), one
|
|
4
|
+
// owner for the CSS grammar and the perceptual math; these re-exports keep
|
|
5
|
+
// the core API surface. The renderer no longer parses at all: color strings
|
|
6
|
+
// cross the FFI raw and the runtime decodes them.
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Parses a CSS color string (named, hex, `rgb()`, `hsl()`, ...) into a packed
|
|
8
|
-
* `0xRRGGBBAA` u32: red in the high byte, alpha in the low byte.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* `0xRRGGBBAA` u32: red in the high byte, alpha in the low byte. This is the
|
|
11
|
+
* wire format the runtime expects for the `color` property. Throws on a
|
|
12
|
+
* string that is not a valid CSS color, so a typo fails on the line that
|
|
13
|
+
* wrote it instead of silently painting black.
|
|
12
14
|
*/
|
|
13
15
|
export function parseColor(color: string): number {
|
|
14
|
-
|
|
15
|
-
if (!c.isValid()) throw new Error(`Invalid color "${color}"`)
|
|
16
|
-
let { r, g, b, a } = c.toRgb()
|
|
17
|
-
return (((r & 0xFF) << 24) | ((g & 0xFF) << 16) | ((b & 0xFF) << 8) | ((a * 255) & 0xFF)) >>> 0
|
|
16
|
+
return tree.parseColor(color)
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
|
-
* Mixes two CSS colors in
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* Mixes two CSS colors in oklab; `t` is the fraction of `b` (0 = pure `a`,
|
|
21
|
+
* 1 = pure `b`). Returns a hex string. Use it to derive semantic tones
|
|
22
|
+
* (muted text, subtle borders) instead of alpha overlays, so the resulting
|
|
23
|
+
* color does not depend on what is drawn beneath it.
|
|
25
24
|
*/
|
|
26
25
|
export function mixColors(a: string, b: string, t: number): string {
|
|
27
|
-
return
|
|
26
|
+
return tree.mixColors(a, b, t)
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
/**
|
|
@@ -33,7 +32,7 @@ export function mixColors(a: string, b: string, t: number): string {
|
|
|
33
32
|
* e.g. whether a label sits light-on-dark (see typeWeight in components).
|
|
34
33
|
*/
|
|
35
34
|
export function brightness(color: string): number {
|
|
36
|
-
return
|
|
35
|
+
return tree.brightness(color)
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
// A color stop: `offset` is 0..1 along the gradient, `color` any CSS color string.
|
package/src/core.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as tree from "flux:rendertree"
|
|
2
|
+
import type { TextureId } from "flux:gpu"
|
|
2
3
|
import { createSignal, onCleanup } from "@solidjs/signals"
|
|
3
4
|
import { on } from "srt:events"
|
|
4
5
|
|
|
@@ -310,7 +311,8 @@ export interface BoundingBox {
|
|
|
310
311
|
* reactive: call it inside `onLayout` (or an event handler) to get values for
|
|
311
312
|
* the current frame. Transforms anywhere in the chain (including the node's
|
|
312
313
|
* own) compose fully; the box is the axis-aligned bounds of the transformed
|
|
313
|
-
* quad.
|
|
314
|
+
* quad. A detached node reports its painted box: its own w/h (or the
|
|
315
|
+
* inherited box), or for `d-line` and `d-path` the geometry plus its stroke.
|
|
314
316
|
*/
|
|
315
317
|
export function getBoundingBox(node: { id: number }): BoundingBox | null {
|
|
316
318
|
return tree.getBoundingBox(node.id)
|
|
@@ -324,6 +326,22 @@ export function getBoundingBoxViewport(node: { id: number }): BoundingBox | null
|
|
|
324
326
|
return tree.getBoundingBoxViewport(node.id)
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
/**
|
|
330
|
+
* The texture id of a snapshot boundary's retained rasterization
|
|
331
|
+
* (`repaintBoundary="snapshot"`): the subtree's pixels as a live texture any
|
|
332
|
+
* GPU consumer - a `<texture>`, a shader or draw target binding, a 3d
|
|
333
|
+
* material - samples by id. Allocated on the first call and stable for the
|
|
334
|
+
* node's lifetime; the runtime re-points it at the current pixels after every
|
|
335
|
+
* rasterization, and only when the subtree changes (a static panel on an
|
|
336
|
+
* animated consumer costs no repaint). Empty until the first paint. Owned by
|
|
337
|
+
* the boundary: `destroyTexture` throws on it, and unmounting the boundary
|
|
338
|
+
* releases it. Throws if the node is not a snapshot boundary. Not a readback:
|
|
339
|
+
* for pixels on the CPU use `captureSnapshot`.
|
|
340
|
+
*/
|
|
341
|
+
export function snapshotTexture(node: { id: number }): TextureId {
|
|
342
|
+
return tree.snapshotTexture(node.id)
|
|
343
|
+
}
|
|
344
|
+
|
|
327
345
|
/**
|
|
328
346
|
* Measures the rendered size of `text` in layout pixels under the given font
|
|
329
347
|
* options (family, size, weight, style, maxLines), without adding it to the
|
|
@@ -363,6 +381,21 @@ export type TextLine = {
|
|
|
363
381
|
cursor: number
|
|
364
382
|
}
|
|
365
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Ink width of the wrap unit starting at unit `index`: its advance through
|
|
386
|
+
* every glued piece that follows, plus the last piece's ink. What must fit on
|
|
387
|
+
* the line for the unit to go on it.
|
|
388
|
+
*/
|
|
389
|
+
export function unitInk(units: tree.TextUnit[], index: number): number {
|
|
390
|
+
let ink = units[index]!.width
|
|
391
|
+
let advance = 0
|
|
392
|
+
for (let j = index + 1; j < units.length && units[j]!.glue; j++) {
|
|
393
|
+
advance += units[j - 1]!.advance
|
|
394
|
+
ink = advance + units[j]!.width
|
|
395
|
+
}
|
|
396
|
+
return ink
|
|
397
|
+
}
|
|
398
|
+
|
|
366
399
|
/**
|
|
367
400
|
* The next line of `prepared` from unit `cursor` that fits `width`, or null
|
|
368
401
|
* when the text is used up. Greedy: units go on the line while the pen plus
|
|
@@ -370,7 +403,9 @@ export type TextLine = {
|
|
|
370
403
|
* wider than `width` on its own goes on the line whole and overflows. Draw a
|
|
371
404
|
* line as `<d-text x y w={line.width + 1}>{prepared.text.slice(line.start, line.end)}</d-text>`
|
|
372
405
|
* with the same font options; its words are already shaped, so that is
|
|
373
|
-
* cheap.
|
|
406
|
+
* cheap. With `runs`, a unit crossing a run boundary is several glued units
|
|
407
|
+
* that always land on one line; draw such a line with a <span> per run.
|
|
408
|
+
* Floats, balancing and ellipsis are <text> features, not this.
|
|
374
409
|
*/
|
|
375
410
|
export function layoutNextLine(prepared: tree.PreparedText, cursor: number, width: number): TextLine | null {
|
|
376
411
|
let units = prepared.units
|
|
@@ -381,7 +416,9 @@ export function layoutNextLine(prepared: tree.PreparedText, cursor: number, widt
|
|
|
381
416
|
let i = cursor
|
|
382
417
|
while (i < units.length) {
|
|
383
418
|
let unit = units[i]!
|
|
384
|
-
|
|
419
|
+
// Glued pieces stay with the unit they continue: the whole wrap unit
|
|
420
|
+
// (this piece through the last glued one) must fit for any of it to go on.
|
|
421
|
+
if (i > cursor && !unit.glue && pen + unitInk(units, i) > width) break
|
|
385
422
|
pen += unit.advance
|
|
386
423
|
if (unit.ascent > ascent) ascent = unit.ascent
|
|
387
424
|
if (unit.descent > descent) descent = unit.descent
|
package/src/data.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Reactive SQLite. A query is a reactive value: it re-runs when any table it
|
|
2
|
+
// reads is written on this connection, and every consumer updates through
|
|
3
|
+
// normal Solid reactivity. SQLite itself reports both sides of the dependency
|
|
4
|
+
// graph - the statement's read-set comes from `stmt.tables()` (SQLite's
|
|
5
|
+
// authorizer) and writes come from `db.onWrite` (SQLite's update hook, so
|
|
6
|
+
// trigger and cascade writes are included). No SQL parsing, no manual
|
|
7
|
+
// declarations, no wrapper: queries take the plain `flux:sqlite` Database.
|
|
8
|
+
//
|
|
9
|
+
// The contract, in one sentence: a query re-runs when any table it reads is
|
|
10
|
+
// written on this connection. Granularity is per table; writes from another
|
|
11
|
+
// connection or process are not seen; WITHOUT ROWID tables do not report.
|
|
12
|
+
//
|
|
13
|
+
// The imperative primitives live in the `flux:sqlite` module and are
|
|
14
|
+
// framework-neutral. This module is only the thin Solid binding on top (a
|
|
15
|
+
// version signal per table, an async memo per query).
|
|
16
|
+
|
|
17
|
+
import { createMemo, createSignal } from "@solidjs/signals"
|
|
18
|
+
import type { Signal, SourceAccessor } from "@solidjs/signals"
|
|
19
|
+
import type { Database, Row, SqlParam, Statement } from "flux:sqlite"
|
|
20
|
+
|
|
21
|
+
export { Database } from "flux:sqlite"
|
|
22
|
+
export type { OpenMode, Row, RunResult, SqlParam, SqlValue, Statement } from "flux:sqlite"
|
|
23
|
+
|
|
24
|
+
/** Bind parameters: a plain array, or an accessor for reactive params. */
|
|
25
|
+
export type Params = SqlParam[] | (() => SqlParam[])
|
|
26
|
+
|
|
27
|
+
// Per-database dependency tracking, created lazily on the first createQuery:
|
|
28
|
+
// one version signal per table, bumped from the connection's write events.
|
|
29
|
+
// The function reads (and lazily creates) the version signal of one table.
|
|
30
|
+
// Keyed by the plain Database so there is no wrapper type to hand around.
|
|
31
|
+
let trackers = new WeakMap<Database, (table: string) => void>()
|
|
32
|
+
|
|
33
|
+
function trackerFor(db: Database): (table: string) => void {
|
|
34
|
+
let track = trackers.get(db)
|
|
35
|
+
if (track) return track
|
|
36
|
+
|
|
37
|
+
let versions = new Map<string, Signal<number>>()
|
|
38
|
+
// A write bumps only existing signals: a signal that was never read has no
|
|
39
|
+
// subscribers, so there is nothing to invalidate. The subscription lives
|
|
40
|
+
// for the connection's life; close() ends it.
|
|
41
|
+
db.onWrite((tables) => {
|
|
42
|
+
for (let table of tables) {
|
|
43
|
+
let version = versions.get(table)
|
|
44
|
+
if (version) version[1]((v) => v + 1)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
track = (table) => {
|
|
48
|
+
let version = versions.get(table)
|
|
49
|
+
if (!version) {
|
|
50
|
+
version = createSignal(0)
|
|
51
|
+
versions.set(table, version)
|
|
52
|
+
}
|
|
53
|
+
version[0]()
|
|
54
|
+
}
|
|
55
|
+
trackers.set(db, track)
|
|
56
|
+
return track
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A reactive query: an accessor over the matching rows that re-runs when any
|
|
61
|
+
* table the statement reads is written on this connection. Reads surface as
|
|
62
|
+
* pending until the first result lands - wrap in `<Loading>` or default with
|
|
63
|
+
* `?? []`. Pass params as an accessor to also re-run when they change.
|
|
64
|
+
*/
|
|
65
|
+
export function createQuery(db: Database, sql: string, params?: Params): SourceAccessor<Row[]> {
|
|
66
|
+
return statementQuery(db, sql, params, (stmt, p) => stmt.all(p))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Like {@link createQuery}, but for single-row reads: resolves to the first
|
|
71
|
+
* matching row, or `undefined` when there is none.
|
|
72
|
+
*/
|
|
73
|
+
export function createQueryRow(
|
|
74
|
+
db: Database,
|
|
75
|
+
sql: string,
|
|
76
|
+
params?: Params,
|
|
77
|
+
): SourceAccessor<Row | undefined> {
|
|
78
|
+
return statementQuery(db, sql, params, (stmt, p) => stmt.get(p))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function statementQuery<T>(
|
|
82
|
+
db: Database,
|
|
83
|
+
sql: string,
|
|
84
|
+
params: Params | undefined,
|
|
85
|
+
run: (stmt: Statement, params?: SqlParam[]) => Promise<T>,
|
|
86
|
+
): SourceAccessor<T> {
|
|
87
|
+
let track = trackerFor(db)
|
|
88
|
+
let stmt = db.query(sql)
|
|
89
|
+
// The read-set arrives async (one authorizer round-trip on the connection
|
|
90
|
+
// thread); computed once, it never changes for a given statement. Reads of
|
|
91
|
+
// the query below surface as pending until it lands, then the query runs -
|
|
92
|
+
// so the first execution already subscribes to its tables and no write can
|
|
93
|
+
// slip between subscription and execution.
|
|
94
|
+
let tables = createMemo(() => stmt.tables())
|
|
95
|
+
return createMemo(() => {
|
|
96
|
+
for (let table of tables()) track(table)
|
|
97
|
+
return run(stmt, typeof params === "function" ? params() : params)
|
|
98
|
+
})
|
|
99
|
+
}
|