@solidrt/core 0.0.11 → 0.0.14
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 +27 -5
- package/jsx-runtime.d.ts +3 -0
- package/package.json +9 -6
- package/src/camera.ts +83 -40
- package/src/color.ts +58 -0
- package/src/core.ts +24 -18
- package/src/gpu.ts +51 -30
- package/src/image.ts +19 -0
- package/src/index.ts +59 -4
- package/src/microphone.ts +43 -25
- package/src/renderer.ts +82 -35
- package/src/scroll.ts +93 -0
- package/src/speech-recognition.ts +149 -0
- package/src/text-input.ts +207 -0
- package/src/types.d.ts +40 -62
- package/src/window.ts +88 -8
- package/src/camera-view.tsx +0 -55
- package/src/speech.ts +0 -67
package/AGENTS.md
CHANGED
|
@@ -29,7 +29,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
32
|
-
2.0.0-beta.
|
|
32
|
+
2.0.0-beta.15); bun resolves them from peerDependencies.
|
|
33
33
|
|
|
34
34
|
## Element model (the parts that are easy to get wrong)
|
|
35
35
|
|
|
@@ -63,15 +63,37 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
|
63
63
|
directly-positioned, often-animating elements (e.g. hundreds of balls), `d-`
|
|
64
64
|
skips the per-element layout that plain elements would incur.
|
|
65
65
|
|
|
66
|
+
- Layout-affecting vs not (this matters for per-frame work). Props fall in three
|
|
67
|
+
buckets, split by where they take effect:
|
|
68
|
+
- `LayoutProps` - width/height, min/max sizes, margin, padding, `position` and
|
|
69
|
+
its `top`/`right`/`bottom`/`left` offsets, flex*/gap/display, grid*,
|
|
70
|
+
aspectRatio, overflow. Changing ANY of these triggers a Taffy reflow of the
|
|
71
|
+
node and its subtree.
|
|
72
|
+
- `TransformProps` - x, y, scale/scaleX/scaleY, rotate/rotateX/rotateY,
|
|
73
|
+
perspective, cx/cy, scrollX/scrollY. Applied at paint/composite; NO reflow.
|
|
74
|
+
- `PaintProps` - color, drawStyle, strokeWidth, blendMode, radius. Also no
|
|
75
|
+
reflow.
|
|
76
|
+
So to MOVE or animate an element - dragging, transitions, per-frame motion -
|
|
77
|
+
translate it with the transform `x`/`y` (or scale/rotate), never by animating
|
|
78
|
+
`left`/`top`/`margin`/`width`. Common trap: `left`/`top` read like "position"
|
|
79
|
+
but they are LAYOUT offsets (for `position:absolute`), so driving them every
|
|
80
|
+
frame reflows the tree. Anchor the element once with layout (e.g.
|
|
81
|
+
`position:absolute` at `left:0,top:0`, or just let normal flow place it) and
|
|
82
|
+
then translate it with `x`/`y`.
|
|
83
|
+
|
|
66
84
|
- Events: there is NO `onClick`/`onPress`. A "button" is a `<view>`/`<rect>`
|
|
67
85
|
with `onPointerDown`. Handlers: onPointerDown/Up/Move/Enter/Leave, onWheel,
|
|
68
86
|
onKeyDown/Up, onTextInput, onFocus/onBlur. Text entry: focus a node with an
|
|
69
87
|
`onTextInput` handler (setFocus activates the on-screen keyboard).
|
|
70
88
|
|
|
71
|
-
- Reactivity is
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
- Reactivity is SolidJS 2.0 (`@solidjs/signals`), NOT Solid 1.x. `createSignal`
|
|
90
|
+
is as you expect, but `createEffect` takes the 2.0 two-function shape: a
|
|
91
|
+
TRACKED compute that reads signals and returns a value, then an UNTRACKED
|
|
92
|
+
effect that receives it - `createEffect(() => count(), (c) => ...)`. The 1.x
|
|
93
|
+
single-callback form `createEffect(() => { ...count()... })` does NOT track
|
|
94
|
+
here. Per-frame work: `onFrame((tick, frame) => {})` (returns a cleanup;
|
|
95
|
+
auto-cleaned inside a reactive scope) or standard `requestAnimationFrame`.
|
|
96
|
+
Also onResize, onLayout, onWindowFocus, onWindowBlur.
|
|
75
97
|
|
|
76
98
|
- Device/GPU access via subpath imports: @solidrt/core/camera, /microphone,
|
|
77
99
|
/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.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -9,8 +9,11 @@
|
|
|
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
|
-
"./
|
|
14
|
+
"./scroll": "./src/scroll.ts",
|
|
15
|
+
"./speech-recognition": "./src/speech-recognition.ts",
|
|
16
|
+
"./text-input": "./src/text-input.ts",
|
|
14
17
|
"./jsx-runtime": "./jsx-runtime.d.ts",
|
|
15
18
|
"./jsx-runtime-dev": "./jsx-runtime.d.ts"
|
|
16
19
|
},
|
|
@@ -23,11 +26,11 @@
|
|
|
23
26
|
"colord": "^2.9.3"
|
|
24
27
|
},
|
|
25
28
|
"devDependencies": {
|
|
26
|
-
"@solidrt/flux-types": "0.0.
|
|
29
|
+
"@solidrt/flux-types": "0.0.0"
|
|
27
30
|
},
|
|
28
31
|
"peerDependencies": {
|
|
29
|
-
"@solidjs/signals": "2.0.0-beta.
|
|
30
|
-
"@solidjs/universal": "2.0.0-beta.
|
|
31
|
-
"solid-js": "2.0.0-beta.
|
|
32
|
+
"@solidjs/signals": "2.0.0-beta.15",
|
|
33
|
+
"@solidjs/universal": "2.0.0-beta.15",
|
|
34
|
+
"solid-js": "2.0.0-beta.15"
|
|
32
35
|
}
|
|
33
36
|
}
|
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,59 @@
|
|
|
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
|
+
// The imperative destroy, surfaced here for the manual-cleanup path documented
|
|
10
|
+
// on the create* helpers (textures made outside a reactive scope, e.g. after an
|
|
11
|
+
// await, are not auto-freed). Re-exported so callers that depend on
|
|
12
|
+
// @solidrt/core -- like @solidrt/components -- can free textures without
|
|
13
|
+
// importing flux directly.
|
|
14
|
+
export { destroyTexture } from "flux:gpu"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Uploads raw RGBA8 pixels to an immutable GPU texture and returns its id (use
|
|
18
|
+
* it as `<texture src={id} />`). `data` must be exactly `width * height * 4`
|
|
19
|
+
* bytes; a mismatch throws. For pixels you intend to mutate and re-upload, use
|
|
20
|
+
* `createMutableTexture` instead. When called inside a reactive scope the
|
|
21
|
+
* texture is freed automatically once that owner is disposed; when called
|
|
22
|
+
* outside one (e.g. after an `await`, where the owner is no longer current)
|
|
23
|
+
* nothing is registered and you must call `destroyTexture` (from flux:gpu)
|
|
24
|
+
* yourself.
|
|
25
|
+
*/
|
|
11
26
|
export function createTexture(data: Uint8Array, width: number, height: number): number {
|
|
12
|
-
|
|
27
|
+
let id = gpu.createTexture(data, width, height)
|
|
28
|
+
if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
29
|
+
return id
|
|
13
30
|
}
|
|
14
31
|
|
|
15
|
-
|
|
16
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Creates a GPU texture you intend to update over time: seed it with `data`,
|
|
34
|
+
* then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels. `data`
|
|
35
|
+
* is RGBA8 and must hold at least `width * height * 4` bytes (it may hold several
|
|
36
|
+
* frames). Like `createTexture`, the texture is freed automatically when the
|
|
37
|
+
* reactive owner is disposed; created outside a reactive scope you must call
|
|
38
|
+
* `destroyTexture` (from flux:gpu) yourself.
|
|
39
|
+
*/
|
|
17
40
|
export function createMutableTexture(data: Uint8Array, width: number, height: number): number {
|
|
18
|
-
|
|
41
|
+
let id = gpu.createMutableTexture(data, width, height)
|
|
42
|
+
if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
43
|
+
return id
|
|
19
44
|
}
|
|
20
45
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Compiles a GLSL ES 3.00 fragment shader and renders it into a texture,
|
|
48
|
+
* returning the texture id (usable anywhere a normal texture id is, e.g.
|
|
49
|
+
* `<texture src>`). The fragment body may reference `vUV` (0..1, top-left
|
|
50
|
+
* origin), `iResolution`, `iTime`, and any `uniform float` it declares; pass
|
|
51
|
+
* their values via `params`. `textures` binds each declared `uniform sampler2D`
|
|
52
|
+
* to an existing texture id (e.g. a camera or decoded image) so the shader can
|
|
53
|
+
* read it; those inputs are re-sampled on every `setShaderParams` call, so live
|
|
54
|
+
* sources stay current. Frees the texture and shader program when the reactive
|
|
55
|
+
* owner is disposed; create outside any reactive scope for app-lifetime shaders.
|
|
56
|
+
*/
|
|
32
57
|
export function createShader(
|
|
33
58
|
fragmentSrc: string,
|
|
34
59
|
width: number,
|
|
@@ -36,11 +61,7 @@ export function createShader(
|
|
|
36
61
|
params?: Record<string, number>,
|
|
37
62
|
textures?: Record<string, number>,
|
|
38
63
|
): 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)
|
|
64
|
+
let id = gpu.createShader(fragmentSrc, width, height, params, textures)
|
|
65
|
+
if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
66
|
+
return id
|
|
46
67
|
}
|
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,57 @@ 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"
|
|
32
|
+
|
|
33
|
+
// --- Authoring-surface re-exports -------------------------------------------
|
|
34
|
+
// A SolidRT app is built from three substrate packages: @solidjs/signals
|
|
35
|
+
// (reactivity), solid-js (control-flow components), and @solidjs/universal (the
|
|
36
|
+
// renderer factory, surfaced via ./renderer). Forwarding the app-facing pieces
|
|
37
|
+
// here means an app imports its whole vocabulary from "@solidrt/core" instead of
|
|
38
|
+
// having to know which substrate package each symbol lives in. These are already
|
|
39
|
+
// peerDependencies, so this adds no new dependency. Curated on purpose - do not
|
|
40
|
+
// `export *` from solid-js, which would leak DOM/hydration-only helpers that are
|
|
41
|
+
// meaningless on the flux runtime.
|
|
42
|
+
|
|
43
|
+
// Reactivity (from @solidjs/signals).
|
|
44
|
+
export {
|
|
45
|
+
createSignal,
|
|
46
|
+
createMemo,
|
|
47
|
+
createEffect,
|
|
48
|
+
createRenderEffect,
|
|
49
|
+
createRoot,
|
|
50
|
+
createStore,
|
|
51
|
+
reconcile,
|
|
52
|
+
mapArray,
|
|
53
|
+
repeat,
|
|
54
|
+
untrack,
|
|
55
|
+
onCleanup,
|
|
56
|
+
onSettled,
|
|
57
|
+
} from "@solidjs/signals"
|
|
58
|
+
export type { Accessor, Setter, Signal, Store, StoreSetter } from "@solidjs/signals"
|
|
59
|
+
|
|
60
|
+
// Control flow, components, and context (from solid-js).
|
|
61
|
+
export {
|
|
62
|
+
For,
|
|
63
|
+
Show,
|
|
64
|
+
Switch,
|
|
65
|
+
Match,
|
|
66
|
+
Repeat,
|
|
67
|
+
Loading,
|
|
68
|
+
Errored,
|
|
69
|
+
Reveal,
|
|
70
|
+
lazy,
|
|
71
|
+
createUniqueId,
|
|
72
|
+
createContext,
|
|
73
|
+
useContext,
|
|
74
|
+
children,
|
|
75
|
+
} from "solid-js"
|
|
76
|
+
export type {
|
|
77
|
+
Component,
|
|
78
|
+
ParentComponent,
|
|
79
|
+
FlowComponent,
|
|
80
|
+
VoidComponent,
|
|
81
|
+
ComponentProps,
|
|
82
|
+
} from "solid-js"
|