@solidrt/core 0.0.11 → 0.0.13
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 +8 -4
- package/jsx-runtime.d.ts +3 -0
- package/package.json +5 -3
- package/src/camera.ts +83 -40
- package/src/color.ts +58 -0
- package/src/core.ts +24 -18
- package/src/gpu.ts +44 -30
- package/src/image.ts +19 -0
- package/src/index.ts +8 -4
- package/src/microphone.ts +43 -25
- package/src/renderer.ts +25 -12
- package/src/speech-recognition.ts +149 -0
- package/src/text-input.ts +207 -0
- package/src/types.d.ts +27 -57
- package/src/window.ts +88 -8
- package/src/camera-view.tsx +0 -55
- package/src/speech.ts +0 -67
package/AGENTS.md
CHANGED
|
@@ -68,10 +68,14 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
|
68
68
|
onKeyDown/Up, onTextInput, onFocus/onBlur. Text entry: focus a node with an
|
|
69
69
|
`onTextInput` handler (setFocus activates the on-screen keyboard).
|
|
70
70
|
|
|
71
|
-
- Reactivity is
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
- Reactivity is SolidJS 2.0 (`@solidjs/signals`), NOT Solid 1.x. `createSignal`
|
|
72
|
+
is as you expect, but `createEffect` takes the 2.0 two-function shape: a
|
|
73
|
+
TRACKED compute that reads signals and returns a value, then an UNTRACKED
|
|
74
|
+
effect that receives it - `createEffect(() => count(), (c) => ...)`. The 1.x
|
|
75
|
+
single-callback form `createEffect(() => { ...count()... })` does NOT track
|
|
76
|
+
here. Per-frame work: `onFrame((tick, frame) => {})` (returns a cleanup;
|
|
77
|
+
auto-cleaned inside a reactive scope) or standard `requestAnimationFrame`.
|
|
78
|
+
Also onResize, onLayout, onWindowFocus, onWindowBlur.
|
|
75
79
|
|
|
76
80
|
- Device/GPU access via subpath imports: @solidrt/core/camera, /microphone,
|
|
77
81
|
/speech, /gpu. Image flow: `decodeImage(bytes)` -> `createTexture(data,w,h)`
|
package/jsx-runtime.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
RectProps,
|
|
4
4
|
OvalProps,
|
|
5
5
|
PathProps,
|
|
6
|
+
SvgProps,
|
|
6
7
|
ViewProps,
|
|
7
8
|
TextProps,
|
|
8
9
|
TextureProps,
|
|
@@ -32,12 +33,14 @@ export namespace JSX {
|
|
|
32
33
|
rect: RectProps & LayoutProps
|
|
33
34
|
oval: OvalProps & LayoutProps
|
|
34
35
|
path: PathProps & LayoutProps
|
|
36
|
+
svg: SvgProps & LayoutProps
|
|
35
37
|
texture: TextureProps & LayoutProps
|
|
36
38
|
audio: AudioProps
|
|
37
39
|
"d-view": ViewProps
|
|
38
40
|
"d-rect": RectProps
|
|
39
41
|
"d-oval": OvalProps
|
|
40
42
|
"d-path": PathProps
|
|
43
|
+
"d-svg": SvgProps
|
|
41
44
|
"d-texture": TextureProps
|
|
42
45
|
"d-text": TextProps
|
|
43
46
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
".": "./src/index.ts",
|
|
10
10
|
"./camera": "./src/camera.ts",
|
|
11
11
|
"./gpu": "./src/gpu.ts",
|
|
12
|
+
"./image": "./src/image.ts",
|
|
12
13
|
"./microphone": "./src/microphone.ts",
|
|
13
|
-
"./speech": "./src/speech.ts",
|
|
14
|
+
"./speech-recognition": "./src/speech-recognition.ts",
|
|
15
|
+
"./text-input": "./src/text-input.ts",
|
|
14
16
|
"./jsx-runtime": "./jsx-runtime.d.ts",
|
|
15
17
|
"./jsx-runtime-dev": "./jsx-runtime.d.ts"
|
|
16
18
|
},
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
"colord": "^2.9.3"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
26
|
-
"@solidrt/flux-types": "0.0.
|
|
28
|
+
"@solidrt/flux-types": "0.0.13"
|
|
27
29
|
},
|
|
28
30
|
"peerDependencies": {
|
|
29
31
|
"@solidjs/signals": "2.0.0-beta.14",
|
package/src/camera.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
// Camera capture. A camera streams into a texture id,
|
|
2
|
-
// <texture src={cam.texture} />. Opening the camera IS
|
|
3
|
-
// (SDL semantics):
|
|
4
|
-
// rejects if the user denies access.
|
|
1
|
+
// Camera capture, reactive (SolidJS) layer. A camera streams into a texture id,
|
|
2
|
+
// so a viewfinder is just <texture src={cam.texture} />. Opening the camera IS
|
|
3
|
+
// the permission request (SDL semantics): it resolves once the stream is
|
|
4
|
+
// configured and rejects if the user denies access.
|
|
5
|
+
//
|
|
6
|
+
// The imperative primitive lives in the `flux:camera` module; import { open,
|
|
7
|
+
// listCameras, scanImage } from "flux:camera" for non-reactive use.
|
|
5
8
|
|
|
9
|
+
import { createSignal, onCleanup } from "@solidjs/signals"
|
|
10
|
+
import { listCameras, open } from "flux:camera"
|
|
6
11
|
import { on } from "srt:events"
|
|
7
12
|
|
|
8
13
|
export type CameraFacing = "front" | "back" | "unknown"
|
|
@@ -21,58 +26,96 @@ export type BarcodeResult = {
|
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export type CameraOptions = {
|
|
24
|
-
/** Explicit device id from listCameras(); takes precedence over facing. */
|
|
29
|
+
/** Explicit device id from flux:camera listCameras(); takes precedence over facing. */
|
|
25
30
|
camera?: number
|
|
26
31
|
/** Pick the first camera with this facing (falls back to the first camera). */
|
|
27
32
|
facing?: "front" | "back"
|
|
28
33
|
/** Size hint; the device picks the closest supported mode. */
|
|
29
34
|
width?: number
|
|
30
35
|
height?: number
|
|
31
|
-
/** Decode these barcode formats from the stream (delivered via
|
|
36
|
+
/** Decode these barcode formats from the stream (delivered via the barcode signal). */
|
|
32
37
|
scan?: BarcodeFormat[]
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
/** Texture id updated every frame while open; render with <texture src={...}>. */
|
|
37
|
-
texture: number
|
|
38
|
-
/** Actual stream size (may differ from the requested hint). */
|
|
39
|
-
width: number
|
|
40
|
-
height: number
|
|
41
|
-
/** Receive decoded barcodes (requires the scan option; replaces any previous callback). */
|
|
42
|
-
onBarcode(callback: (result: BarcodeResult) => void): void
|
|
43
|
-
/** Release the device. The texture keeps showing the last frame. */
|
|
44
|
-
close(): void
|
|
45
|
-
}
|
|
40
|
+
let devicesAccessor: (() => CameraInfo[]) | undefined
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Current camera list as a reactive accessor: re-enumerates on hotplug. Also
|
|
44
|
+
* initializes the camera subsystem (required before hotplug events fire).
|
|
45
|
+
* App-lifetime: there is one camera subsystem, so no cleanup is needed.
|
|
46
|
+
*
|
|
47
|
+
* Coverage caveat (SDL 3.4.8): only Android delivers both add and remove. On
|
|
48
|
+
* Linux you get add events but not remove (removal is broken upstream); on
|
|
49
|
+
* macOS/Windows there is no camera hotplug at all.
|
|
50
|
+
*/
|
|
51
|
+
export function cameraDevices(): CameraInfo[] {
|
|
52
|
+
if (!devicesAccessor) {
|
|
53
|
+
let [devices, setDevices] = createSignal<CameraInfo[]>(listCameras())
|
|
54
|
+
on("cameraDeviceChange", () => setDevices(listCameras()))
|
|
55
|
+
devicesAccessor = devices
|
|
56
|
+
}
|
|
57
|
+
return devicesAccessor()
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Low-level fallback: subscribe to camera hotplug events with a callback. Prefer
|
|
62
|
+
* `cameraDevices()` unless the reactive accessor does not fit. Re-enumerate with
|
|
63
|
+
* `listCameras()` (from flux:camera) inside the callback to get the new set.
|
|
64
|
+
* Returns an unsubscribe function.
|
|
65
|
+
*/
|
|
57
66
|
export function onDeviceChange(callback: (event: { added: boolean }) => void): () => void {
|
|
58
67
|
return on("cameraDeviceChange", callback)
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
/** A live camera as reactive accessors. */
|
|
71
|
+
export type CameraStream = {
|
|
72
|
+
/** Texture id once the stream is up, undefined while opening; render with <texture src={...}>. */
|
|
73
|
+
texture(): number | undefined
|
|
74
|
+
/** Actual stream size, undefined while opening. */
|
|
75
|
+
width(): number | undefined
|
|
76
|
+
height(): number | undefined
|
|
77
|
+
/** The most recently decoded barcode (requires the scan option). */
|
|
78
|
+
barcode(): BarcodeResult | undefined
|
|
79
|
+
/** Set if opening failed (e.g. permission denied). */
|
|
80
|
+
error(): Error | undefined
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Opens a camera and exposes it as reactive signals: read texture() in JSX and
|
|
85
|
+
* it appears once the stream is configured. Closes automatically when the
|
|
86
|
+
* reactive owner is disposed (e.g. the component unmounts). For imperative use,
|
|
87
|
+
* call open() from "flux:camera" directly.
|
|
88
|
+
*/
|
|
89
|
+
export function createCamera(options: CameraOptions = {}): CameraStream {
|
|
90
|
+
let [texture, setTexture] = createSignal<number | undefined>(undefined)
|
|
91
|
+
let [width, setWidth] = createSignal<number | undefined>(undefined)
|
|
92
|
+
let [height, setHeight] = createSignal<number | undefined>(undefined)
|
|
93
|
+
let [barcode, setBarcode] = createSignal<BarcodeResult | undefined>(undefined)
|
|
94
|
+
let [error, setError] = createSignal<Error | undefined>(undefined)
|
|
95
|
+
let session: Awaited<ReturnType<typeof open>> | undefined
|
|
96
|
+
let disposed = false
|
|
97
|
+
|
|
98
|
+
open(options)
|
|
99
|
+
.then((cam) => {
|
|
100
|
+
if (disposed) {
|
|
101
|
+
cam.close()
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
session = cam
|
|
105
|
+
if (options.scan) cam.onBarcode((result) => setBarcode(result))
|
|
106
|
+
setTexture(cam.texture)
|
|
107
|
+
setWidth(cam.width)
|
|
108
|
+
setHeight(cam.height)
|
|
109
|
+
})
|
|
110
|
+
.catch((e) => setError(e instanceof Error ? e : new Error(String(e))))
|
|
111
|
+
|
|
112
|
+
onCleanup(() => {
|
|
113
|
+
disposed = true
|
|
114
|
+
if (session) {
|
|
115
|
+
session.close()
|
|
116
|
+
session = undefined
|
|
117
|
+
}
|
|
118
|
+
})
|
|
77
119
|
|
|
78
|
-
|
|
120
|
+
return { texture, width, height, barcode, error }
|
|
121
|
+
}
|
package/src/color.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { colord, extend } from "colord"
|
|
2
|
+
import namesPlugin from "colord/plugins/names"
|
|
3
|
+
extend([namesPlugin])
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parses a CSS color string (named, hex, `rgb()`, `hsl()`, ...) into a packed
|
|
7
|
+
* `0xRRGGBBAA` u32: red in the high byte, alpha in the low byte. Alpha is scaled
|
|
8
|
+
* from colord's 0..1 to 0..255. This is the wire format the runtime expects for
|
|
9
|
+
* the `color` property.
|
|
10
|
+
*/
|
|
11
|
+
export function parseColor(color: string): number {
|
|
12
|
+
let { r, g, b, a } = colord(color).toRgb()
|
|
13
|
+
return (((r & 0xFF) << 24) | ((g & 0xFF) << 16) | ((b & 0xFF) << 8) | ((a * 255) & 0xFF)) >>> 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A color stop: `offset` is 0..1 along the gradient, `color` any CSS color string.
|
|
17
|
+
export type GradientStop = { offset: number; color: string }
|
|
18
|
+
|
|
19
|
+
type Stop = { offset: number; color: number }
|
|
20
|
+
|
|
21
|
+
// A gradient fill value, produced by createLinearGradient / createRadialGradient
|
|
22
|
+
// and passed to a paint `color` prop. Coordinates are relative (0..1 of the
|
|
23
|
+
// element's box), so one gradient can be reused on elements of any size. Branded
|
|
24
|
+
// so the renderer can tell it from a solid color string. The object crosses to
|
|
25
|
+
// the runtime as-is and is decoded by key (see properties/paint.rs).
|
|
26
|
+
export type Gradient =
|
|
27
|
+
| { readonly __gradient: "linear"; x0: number; y0: number; x1: number; y1: number; stops: Stop[] }
|
|
28
|
+
| { readonly __gradient: "radial"; cx: number; cy: number; r: number; circle: boolean; stops: Stop[] }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A linear gradient between two points, each given in 0..1 of the element's box
|
|
32
|
+
* ((0,0) top-left, (1,1) bottom-right). Stops are clamped at the ends.
|
|
33
|
+
*/
|
|
34
|
+
export function createLinearGradient(
|
|
35
|
+
x0: number, y0: number, x1: number, y1: number, stops: GradientStop[],
|
|
36
|
+
): Gradient {
|
|
37
|
+
return { __gradient: "linear", x0, y0, x1, y1, stops: parseStops(stops) }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A radial gradient centered at `(cx, cy)` (0..1 of the box) with radius `r`
|
|
42
|
+
* (0..1). Defaults to an ellipse that follows the box's aspect ratio; pass
|
|
43
|
+
* `{ shape: "circle" }` to keep a true circle (radius is then a fraction of the
|
|
44
|
+
* shorter side).
|
|
45
|
+
*/
|
|
46
|
+
export function createRadialGradient(
|
|
47
|
+
cx: number, cy: number, r: number, stops: GradientStop[], opts?: { shape?: "ellipse" | "circle" },
|
|
48
|
+
): Gradient {
|
|
49
|
+
return { __gradient: "radial", cx, cy, r, circle: opts?.shape === "circle", stops: parseStops(stops) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isGradient(value: unknown): value is Gradient {
|
|
53
|
+
return typeof value === "object" && value !== null && "__gradient" in value
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseStops(stops: GradientStop[]): Stop[] {
|
|
57
|
+
return stops.map((s) => ({ offset: s.offset, color: parseColor(s.color) }))
|
|
58
|
+
}
|
package/src/core.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import namesPlugin from "colord/plugins/names"
|
|
3
|
-
import type { MeasureTextOptions } from "./types"
|
|
4
|
-
extend([namesPlugin])
|
|
5
|
-
|
|
6
|
-
export function parseColorToU32(color: string): number {
|
|
7
|
-
let { r, g, b, a } = colord(color).toRgb()
|
|
8
|
-
return (((r & 0xFF) << 24) | ((g & 0xFF) << 16) | ((b & 0xFF) << 8) | ((a * 255) & 0xFF)) >>> 0
|
|
9
|
-
}
|
|
1
|
+
import * as tree from "flux:rendertree"
|
|
10
2
|
|
|
11
3
|
let handlers = new Map<number, Map<string, Function>>()
|
|
12
4
|
|
|
@@ -36,6 +28,13 @@ export function cleanupNodeHandlers(nodeId: number): void {
|
|
|
36
28
|
let focusedNodeId: number | null = null
|
|
37
29
|
let textInputActive = false
|
|
38
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Moves keyboard focus to `nodeId`, or clears it with `null`. Fires `onBlur` on
|
|
33
|
+
* the previously focused node and `onFocus` on the new one. As a side effect,
|
|
34
|
+
* the on-screen keyboard is activated when the newly focused node has an
|
|
35
|
+
* `onTextInput` handler and deactivated otherwise. No-op if the node is already
|
|
36
|
+
* focused.
|
|
37
|
+
*/
|
|
39
38
|
export function setFocus(nodeId: number | null): void {
|
|
40
39
|
if (nodeId === focusedNodeId) return
|
|
41
40
|
let oldId = focusedNodeId
|
|
@@ -49,7 +48,7 @@ export function setFocus(nodeId: number | null): void {
|
|
|
49
48
|
let wantActive = nodeId != null && getEventHandler(nodeId, "onTextInput") != null
|
|
50
49
|
if (wantActive !== textInputActive) {
|
|
51
50
|
textInputActive = wantActive
|
|
52
|
-
|
|
51
|
+
tree.setTextInputActive(wantActive)
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
54
|
|
|
@@ -64,15 +63,22 @@ export interface BoundingBox {
|
|
|
64
63
|
height: number
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Returns the node's window-relative bounding box from the most recently
|
|
68
|
+
* computed layout, or `null` if the node has no layout or has not been laid out
|
|
69
|
+
* yet. This is a snapshot read, not reactive: call it inside `onLayout` (or an
|
|
70
|
+
* event handler) to get values for the current frame. Phase 1 composes only
|
|
71
|
+
* translations; x/y are wrong when a rotate/scale sits anywhere above the node.
|
|
72
|
+
*/
|
|
72
73
|
export function getBoundingBox(node: { id: number }): BoundingBox | null {
|
|
73
|
-
return
|
|
74
|
+
return tree.getBoundingBox(node.id)
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Measures the rendered size of `text` in layout pixels under the given font
|
|
79
|
+
* options (family, size, weight, style, maxLines), without adding it to the
|
|
80
|
+
* tree. Useful for sizing or laying out around text before it is drawn.
|
|
81
|
+
*/
|
|
82
|
+
export function measureText(text: string, options?: tree.MeasureTextOptions): { width: number, height: number } {
|
|
83
|
+
return tree.measureText(text, options)
|
|
78
84
|
}
|
package/src/gpu.ts
CHANGED
|
@@ -1,34 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
1
|
+
// GPU textures and shaders, reactive (SolidJS) layer: the create* helpers free
|
|
2
|
+
// their texture automatically when the reactive owner is disposed. The imperative
|
|
3
|
+
// primitive lives in the `flux:gpu` module; import { uploadTexture,
|
|
4
|
+
// setShaderParams, destroyTexture, ... } from "flux:gpu" for non-reactive use.
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
6
|
+
import { getOwner, onCleanup } from "@solidjs/signals"
|
|
7
|
+
import * as gpu from "flux:gpu"
|
|
10
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Uploads raw RGBA8 pixels to an immutable GPU texture and returns its id (use
|
|
11
|
+
* it as `<texture src={id} />`). `data` must be exactly `width * height * 4`
|
|
12
|
+
* bytes; a mismatch throws. For pixels you intend to mutate and re-upload, use
|
|
13
|
+
* `createMutableTexture` instead. When called inside a reactive scope the
|
|
14
|
+
* texture is freed automatically once that owner is disposed; when called
|
|
15
|
+
* outside one (e.g. after an `await`, where the owner is no longer current)
|
|
16
|
+
* nothing is registered and you must call `destroyTexture` (from flux:gpu)
|
|
17
|
+
* yourself.
|
|
18
|
+
*/
|
|
11
19
|
export function createTexture(data: Uint8Array, width: number, height: number): number {
|
|
12
|
-
|
|
20
|
+
let id = gpu.createTexture(data, width, height)
|
|
21
|
+
if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
22
|
+
return id
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Creates a GPU texture you intend to update over time: seed it with `data`,
|
|
27
|
+
* then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels. `data`
|
|
28
|
+
* is RGBA8 and must hold at least `width * height * 4` bytes (it may hold several
|
|
29
|
+
* frames). Like `createTexture`, the texture is freed automatically when the
|
|
30
|
+
* reactive owner is disposed; created outside a reactive scope you must call
|
|
31
|
+
* `destroyTexture` (from flux:gpu) yourself.
|
|
32
|
+
*/
|
|
17
33
|
export function createMutableTexture(data: Uint8Array, width: number, height: number): number {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export function uploadTexture(textureId: number, offset: number = 0): void {
|
|
22
|
-
gpu.uploadTexture(textureId, offset)
|
|
34
|
+
let id = gpu.createMutableTexture(data, width, height)
|
|
35
|
+
if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
36
|
+
return id
|
|
23
37
|
}
|
|
24
38
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Compiles a GLSL ES 3.00 fragment shader and renders it into a texture,
|
|
41
|
+
* returning the texture id (usable anywhere a normal texture id is, e.g.
|
|
42
|
+
* `<texture src>`). The fragment body may reference `vUV` (0..1, top-left
|
|
43
|
+
* origin), `iResolution`, `iTime`, and any `uniform float` it declares; pass
|
|
44
|
+
* their values via `params`. `textures` binds each declared `uniform sampler2D`
|
|
45
|
+
* to an existing texture id (e.g. a camera or decoded image) so the shader can
|
|
46
|
+
* read it; those inputs are re-sampled on every `setShaderParams` call, so live
|
|
47
|
+
* sources stay current. Frees the texture and shader program when the reactive
|
|
48
|
+
* owner is disposed; create outside any reactive scope for app-lifetime shaders.
|
|
49
|
+
*/
|
|
32
50
|
export function createShader(
|
|
33
51
|
fragmentSrc: string,
|
|
34
52
|
width: number,
|
|
@@ -36,11 +54,7 @@ export function createShader(
|
|
|
36
54
|
params?: Record<string, number>,
|
|
37
55
|
textures?: Record<string, number>,
|
|
38
56
|
): number {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Re-render an existing shader texture with new param values and request a
|
|
43
|
-
// frame. Use this to animate (e.g. update iTime each frame).
|
|
44
|
-
export function setShaderParams(textureId: number, params: Record<string, number>): void {
|
|
45
|
-
gpu.setShaderParams(textureId, params)
|
|
57
|
+
let id = gpu.createShader(fragmentSrc, width, height, params, textures)
|
|
58
|
+
if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
59
|
+
return id
|
|
46
60
|
}
|
package/src/image.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// CPU image codec: decode encoded image bytes into raw RGBA8 pixels (and, in
|
|
2
|
+
// future, encode them back). Kept separate from the GPU/texture APIs because no
|
|
3
|
+
// GPU is involved; pair decodeImage with createTexture from "@solidrt/core/gpu"
|
|
4
|
+
// to upload the result.
|
|
5
|
+
|
|
6
|
+
export type DecodedImage = {
|
|
7
|
+
data: Uint8Array
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Decodes encoded image bytes (PNG, JPEG, and the other formats the runtime's
|
|
14
|
+
* image decoder supports) into raw, tightly-packed RGBA8 pixels plus the
|
|
15
|
+
* decoded dimensions. Feed the result straight into `createTexture`.
|
|
16
|
+
*/
|
|
17
|
+
export function decodeImage(bytes: Uint8Array): DecodedImage {
|
|
18
|
+
return image.decodeImage(bytes)
|
|
19
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export * from "./renderer"
|
|
2
|
-
export { setFocus, getFocusedNodeId, measureText, getBoundingBox
|
|
2
|
+
export { setFocus, getFocusedNodeId, measureText, getBoundingBox } from "./core"
|
|
3
3
|
export type { BoundingBox } from "./core"
|
|
4
|
+
export { parseColor, createLinearGradient, createRadialGradient } from "./color"
|
|
5
|
+
export type { Gradient, GradientStop } from "./color"
|
|
4
6
|
export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur } from "./window"
|
|
5
|
-
export {
|
|
6
|
-
export
|
|
7
|
+
export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
|
|
8
|
+
export { createTexture } from "./gpu"
|
|
9
|
+
export { decodeImage } from "./image"
|
|
10
|
+
export type { DecodedImage } from "./image"
|
|
7
11
|
export type {
|
|
8
12
|
LayoutProps,
|
|
9
13
|
TransformProps,
|
|
@@ -22,6 +26,6 @@ export type {
|
|
|
22
26
|
TextProps,
|
|
23
27
|
TextureProps,
|
|
24
28
|
AudioProps,
|
|
25
|
-
MeasureTextOptions,
|
|
26
29
|
Color,
|
|
27
30
|
} from "./types"
|
|
31
|
+
export type { MeasureTextOptions } from "flux:rendertree"
|
package/src/microphone.ts
CHANGED
|
@@ -1,40 +1,58 @@
|
|
|
1
|
-
// Microphone capture. A session delivers raw mono
|
|
2
|
-
// requested sample rate (the device format is converted
|
|
3
|
-
// drains whatever was captured since the last call. Captured
|
|
4
|
-
// until read, so poll read() regularly (e.g. once per frame) while
|
|
1
|
+
// Microphone capture, reactive (SolidJS) layer. A session delivers raw mono
|
|
2
|
+
// float32 samples at the requested sample rate (the device format is converted
|
|
3
|
+
// by SDL); read() drains whatever was captured since the last call. Captured
|
|
4
|
+
// audio buffers until read, so poll read() regularly (e.g. once per frame) while
|
|
5
|
+
// open.
|
|
6
|
+
//
|
|
7
|
+
// The imperative primitive lives in the `flux:microphone` module; import
|
|
8
|
+
// { open, listMicrophones } from "flux:microphone" for non-reactive use.
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
name: string
|
|
9
|
-
}
|
|
10
|
+
import { createSignal, onCleanup } from "@solidjs/signals"
|
|
11
|
+
import { open } from "flux:microphone"
|
|
10
12
|
|
|
11
13
|
export type MicrophoneOptions = {
|
|
12
|
-
/** Explicit device id from listMicrophones(); default is the system default
|
|
14
|
+
/** Explicit device id from flux:microphone listMicrophones(); default is the system default. */
|
|
13
15
|
microphone?: number
|
|
14
16
|
/** Sample rate of the delivered samples (the device rate is converted). Default 16000. */
|
|
15
17
|
sampleRate?: number
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/** A live microphone with reactive lifecycle. */
|
|
21
|
+
export type MicrophoneStream = {
|
|
22
|
+
/** Sample rate of read() samples (0 if opening failed). */
|
|
23
|
+
sampleRate(): number
|
|
24
|
+
/** Drain the mono float32 samples captured since the last read (empty if not open). */
|
|
22
25
|
read(): Float32Array
|
|
23
|
-
/**
|
|
24
|
-
|
|
26
|
+
/** Set if opening failed. */
|
|
27
|
+
error(): Error | undefined
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Opens a microphone and owns its lifecycle: closes when the reactive owner is
|
|
32
|
+
* disposed. Capture stays pull-based, so read() drains samples on demand (e.g.
|
|
33
|
+
* once per frame). For imperative use, call open() from "flux:microphone".
|
|
34
|
+
*/
|
|
35
|
+
export function createMicrophone(options: MicrophoneOptions = {}): MicrophoneStream {
|
|
36
|
+
let [error, setError] = createSignal<Error | undefined>(undefined)
|
|
37
|
+
let session: ReturnType<typeof open> | undefined
|
|
38
|
+
let rate = 0
|
|
39
|
+
try {
|
|
40
|
+
session = open(options)
|
|
41
|
+
rate = session.sampleRate
|
|
42
|
+
} catch (e) {
|
|
43
|
+
setError(e instanceof Error ? e : new Error(String(e)))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onCleanup(() => {
|
|
47
|
+
if (session) {
|
|
48
|
+
session.close()
|
|
49
|
+
session = undefined
|
|
50
|
+
}
|
|
51
|
+
})
|
|
30
52
|
|
|
31
|
-
// Async to leave room for an OS permission prompt on platforms that need one
|
|
32
|
-
// (the desktop backends open synchronously).
|
|
33
|
-
export async function openMicrophone(options: MicrophoneOptions = {}): Promise<Microphone> {
|
|
34
|
-
let opened = microphone.open(options)
|
|
35
53
|
return {
|
|
36
|
-
sampleRate:
|
|
37
|
-
read: () =>
|
|
38
|
-
|
|
54
|
+
sampleRate: () => rate,
|
|
55
|
+
read: () => (session ? session.read() : new Float32Array(0)),
|
|
56
|
+
error,
|
|
39
57
|
}
|
|
40
58
|
}
|