@solidrt/core 0.0.24 → 0.0.26
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 +1 -1
- package/package.json +6 -5
- package/src/gpu.ts +7 -0
- package/src/renderer.ts +39 -20
- package/src/sound.ts +119 -0
package/AGENTS.md
CHANGED
|
@@ -84,7 +84,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
|
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
87
|
-
2.0.0-beta.
|
|
87
|
+
2.0.0-beta.17); bun resolves them from peerDependencies.
|
|
88
88
|
|
|
89
89
|
## Element model (the parts that are easy to get wrong)
|
|
90
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"./gpu": "./src/gpu.ts",
|
|
12
12
|
"./image": "./src/image.ts",
|
|
13
13
|
"./microphone": "./src/microphone.ts",
|
|
14
|
+
"./sound": "./src/sound.ts",
|
|
14
15
|
"./speech-recognition": "./src/speech-recognition.ts",
|
|
15
16
|
"./text-input": "./src/text-input.ts",
|
|
16
17
|
"./jsx-runtime": "./jsx-runtime.d.ts",
|
|
@@ -26,11 +27,11 @@
|
|
|
26
27
|
"colord": "^2.9.3"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
|
-
"@solidrt/flux-types": "0.0.
|
|
30
|
+
"@solidrt/flux-types": "0.0.26"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
|
-
"@solidjs/signals": "2.0.0-beta.
|
|
33
|
-
"@solidjs/universal": "2.0.0-beta.
|
|
34
|
-
"solid-js": "2.0.0-beta.
|
|
33
|
+
"@solidjs/signals": "2.0.0-beta.17",
|
|
34
|
+
"@solidjs/universal": "2.0.0-beta.17",
|
|
35
|
+
"solid-js": "2.0.0-beta.17"
|
|
35
36
|
}
|
|
36
37
|
}
|
package/src/gpu.ts
CHANGED
|
@@ -19,6 +19,13 @@ import * as gpu from "flux:gpu"
|
|
|
19
19
|
// `<texture params={...}>` when a `<texture>` element is already in the tree.
|
|
20
20
|
export { destroyTexture, setShaderParams, uploadTexture } from "flux:gpu"
|
|
21
21
|
|
|
22
|
+
// captureSnapshot renders a node to a texture and readTexture reads any
|
|
23
|
+
// texture's bytes back. Re-exported raw (no reactive auto-cleanup wrapper):
|
|
24
|
+
// captureSnapshot resolves asynchronously, by which point the reactive owner is
|
|
25
|
+
// no longer current, so the caller owns the returned id and frees it with
|
|
26
|
+
// destroyTexture (as with any texture created after an await).
|
|
27
|
+
export { captureSnapshot, readTexture } from "flux:gpu"
|
|
28
|
+
|
|
22
29
|
/**
|
|
23
30
|
* Uploads raw RGBA8 pixels to an immutable GPU texture and returns its id (use
|
|
24
31
|
* it as `<texture src={id} />`). `data` must be exactly `width * height * 4`
|
package/src/renderer.ts
CHANGED
|
@@ -87,6 +87,34 @@ function removeNode(parent: ProxyNode, node: ProxyNode): void {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// Applies a single prop to a node: routes events to the handler registry,
|
|
91
|
+
// parses color strings/gradients, and forwards everything else to the tree.
|
|
92
|
+
// Shared by the renderer's setProperty hook and by createElement, which since
|
|
93
|
+
// the dom-expressions "universal" template passes static props inline as a
|
|
94
|
+
// second argument rather than as separate setProp calls.
|
|
95
|
+
function applyProp<T>(node: ProxyNode, name: string, value: T): void {
|
|
96
|
+
if (!node) return
|
|
97
|
+
|
|
98
|
+
// console.debug("[srt] applyProp", node.id, name, value)
|
|
99
|
+
|
|
100
|
+
if (/^on[A-Z]/.test(name) && (value == null || typeof value === "function")) {
|
|
101
|
+
setEventHandler(node.id, name, value as Function | null | undefined)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (name === "color" && isGradient(value)) {
|
|
106
|
+
tree.setProperty(node.id, name, value)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (name === "color" && typeof value === "string") {
|
|
111
|
+
tree.setProperty(node.id, name, parseColor(value))
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
tree.setProperty(node.id, name, value)
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
export let {
|
|
91
119
|
effect,
|
|
92
120
|
memo,
|
|
@@ -101,7 +129,7 @@ export let {
|
|
|
101
129
|
applyRef,
|
|
102
130
|
ref,
|
|
103
131
|
} = createRenderer<ProxyNode>({
|
|
104
|
-
createElement: (elementType: string): ProxyNode => {
|
|
132
|
+
createElement: (elementType: string, props?: Record<string, any>): ProxyNode => {
|
|
105
133
|
let proxy = createProxyNode(elementType)
|
|
106
134
|
|
|
107
135
|
// console.debug("[srt] createElement", proxy.id, elementType)
|
|
@@ -109,6 +137,15 @@ export let {
|
|
|
109
137
|
if (elementType === "window") tree.createRoot(proxy.id)
|
|
110
138
|
else tree.createNode(proxy.id, elementType)
|
|
111
139
|
|
|
140
|
+
// The universal JSX template hands static props here as an object; children
|
|
141
|
+
// and ref arrive through their own hooks, so skip them.
|
|
142
|
+
if (props) {
|
|
143
|
+
for (let name in props) {
|
|
144
|
+
if (name === "children" || name === "ref") continue
|
|
145
|
+
applyProp(proxy, name, props[name])
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
112
149
|
return proxy
|
|
113
150
|
},
|
|
114
151
|
|
|
@@ -127,26 +164,8 @@ export let {
|
|
|
127
164
|
|
|
128
165
|
isTextNode: (node: ProxyNode): boolean => node?.elementType === "d-span",
|
|
129
166
|
setProperty: <T>(node: ProxyNode, name: string, value: T): void => {
|
|
130
|
-
if (!node) return
|
|
131
|
-
|
|
132
167
|
// console.debug("[srt] setProperty", node.id, name, value)
|
|
133
|
-
|
|
134
|
-
if (/^on[A-Z]/.test(name) && (value == null || typeof value === "function")) {
|
|
135
|
-
setEventHandler(node.id, name, value as Function | null | undefined)
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (name === "color" && isGradient(value)) {
|
|
140
|
-
tree.setProperty(node.id, name, value)
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (name === "color" && typeof value === "string") {
|
|
145
|
-
tree.setProperty(node.id, name, parseColor(value))
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
tree.setProperty(node.id, name, value)
|
|
168
|
+
applyProp(node, name, value)
|
|
150
169
|
},
|
|
151
170
|
|
|
152
171
|
insertNode: (parent: ProxyNode, node: ProxyNode, anchor?: ProxyNode): void => {
|
package/src/sound.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Sound playback, reactive (SolidJS) layer. `createSound` decodes an encoded
|
|
2
|
+
// clip (Ogg/Vorbis or WAV) once and owns its lifecycle: the decoded clip is
|
|
3
|
+
// released, and any playing voices stopped, when the reactive owner is disposed.
|
|
4
|
+
// Each play() is cheap (no re-decode). `createSoundStream` is the same but reads
|
|
5
|
+
// a large track from a file path on demand instead of decoding it into memory.
|
|
6
|
+
//
|
|
7
|
+
// The imperative primitive lives in the `flux:audio` module; import
|
|
8
|
+
// { play, load, stream } from "flux:audio" for non-reactive use.
|
|
9
|
+
|
|
10
|
+
import { createSignal, onCleanup } from "@solidjs/signals"
|
|
11
|
+
import { load, stream } from "flux:audio"
|
|
12
|
+
import { file } from "flux:fs"
|
|
13
|
+
|
|
14
|
+
type FluxFile = ReturnType<typeof file>
|
|
15
|
+
|
|
16
|
+
type LoadedSound = ReturnType<typeof load>
|
|
17
|
+
|
|
18
|
+
export type SoundOptions = {
|
|
19
|
+
/** Repeat the clip until stopped. Defaults to false. */
|
|
20
|
+
loop?: boolean
|
|
21
|
+
/** Volume scale, 1.0 leaves the clip unchanged. Defaults to 1.0. */
|
|
22
|
+
gain?: number
|
|
23
|
+
/**
|
|
24
|
+
* Let play() stack overlapping voices instead of restarting. Defaults to
|
|
25
|
+
* true: rapid triggers overlap. Set false for a single-voice sound where each
|
|
26
|
+
* play() cuts off the previous one.
|
|
27
|
+
*/
|
|
28
|
+
overlap?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Options for a streamed sound. Streams are always single-voice. */
|
|
32
|
+
export type SoundStreamOptions = {
|
|
33
|
+
/** Repeat the track until stopped. Defaults to false. */
|
|
34
|
+
loop?: boolean
|
|
35
|
+
/** Volume scale, 1.0 leaves the track unchanged. Defaults to 1.0. */
|
|
36
|
+
gain?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A decoded sound with reactive lifecycle. */
|
|
40
|
+
export type Sound = {
|
|
41
|
+
/** Start the clip. Overlaps or restarts per the `overlap` option. */
|
|
42
|
+
play(): void
|
|
43
|
+
/** Stop every voice started from this sound. */
|
|
44
|
+
stop(): void
|
|
45
|
+
/** True after play() until stop() (does not track natural completion). */
|
|
46
|
+
playing(): boolean
|
|
47
|
+
/** Set if loading failed. */
|
|
48
|
+
error(): Error | undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Shared reactive wrapper: owns the loaded handle, tracks live voices, and
|
|
52
|
+
// disposes both on cleanup. `loader` runs once (may throw -> error signal).
|
|
53
|
+
function reactiveSound(
|
|
54
|
+
loader: () => LoadedSound,
|
|
55
|
+
overlap: boolean,
|
|
56
|
+
playOptions: { loop?: boolean; gain?: number },
|
|
57
|
+
): Sound {
|
|
58
|
+
let [error, setError] = createSignal<Error | undefined>(undefined, { ownedWrite: true })
|
|
59
|
+
let [playing, setPlaying] = createSignal(false, { ownedWrite: true })
|
|
60
|
+
|
|
61
|
+
let handle: LoadedSound | undefined
|
|
62
|
+
let voices: { stop(): void }[] = []
|
|
63
|
+
try {
|
|
64
|
+
handle = loader()
|
|
65
|
+
} catch (e) {
|
|
66
|
+
setError(e instanceof Error ? e : new Error(String(e)))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let stopAll = () => {
|
|
70
|
+
for (let v of voices) v.stop()
|
|
71
|
+
voices = []
|
|
72
|
+
setPlaying(false)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onCleanup(() => {
|
|
76
|
+
stopAll()
|
|
77
|
+
if (handle) {
|
|
78
|
+
handle.unload()
|
|
79
|
+
handle = undefined
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
play() {
|
|
85
|
+
if (!handle) return
|
|
86
|
+
if (!overlap) stopAll()
|
|
87
|
+
voices.push(handle.play(playOptions))
|
|
88
|
+
setPlaying(true)
|
|
89
|
+
},
|
|
90
|
+
stop: stopAll,
|
|
91
|
+
playing,
|
|
92
|
+
error,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Decodes a sound once and owns its lifecycle: releases the clip and stops its
|
|
98
|
+
* voices when the reactive owner is disposed. play() replays without decoding.
|
|
99
|
+
* For imperative use, call load()/play() from "flux:audio".
|
|
100
|
+
*/
|
|
101
|
+
export function createSound(source: Uint8Array, options: SoundOptions = {}): Sound {
|
|
102
|
+
return reactiveSound(() => load(source), options.overlap ?? true, {
|
|
103
|
+
loop: options.loop,
|
|
104
|
+
gain: options.gain,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Streams a large track, decoding on demand instead of loading it into memory.
|
|
110
|
+
* Single-voice: each play() restarts it. Pass a path (resolved like flux:fs,
|
|
111
|
+
* relative to the process cwd) or a `file()` from flux:fs; a path is wrapped in
|
|
112
|
+
* `file()` for you, so a dev-server-proxied file streams over the proxy. Owns
|
|
113
|
+
* the stream's lifecycle: stopped and released when the reactive owner is
|
|
114
|
+
* disposed. For imperative use, call stream()/play() from "flux:audio".
|
|
115
|
+
*/
|
|
116
|
+
export function createSoundStream(source: string | FluxFile, options: SoundStreamOptions = {}): Sound {
|
|
117
|
+
let src = typeof source === "string" ? file(source) : source
|
|
118
|
+
return reactiveSound(() => stream(src), false, { loop: options.loop, gain: options.gain })
|
|
119
|
+
}
|