@solidrt/core 0.0.10 → 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 +112 -0
- package/README.md +2 -0
- package/jsx-runtime.d.ts +3 -0
- package/package.json +8 -5
- package/src/camera.ts +83 -40
- package/src/color.ts +58 -0
- package/src/core.ts +24 -18
- package/src/gpu.ts +51 -14
- package/src/image.ts +19 -0
- package/src/index.ts +7 -3
- 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 -49
- package/src/window.ts +88 -8
- package/src/camera-view.tsx +0 -55
- package/src/speech.ts +0 -67
package/AGENTS.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @solidrt/core - agent notes
|
|
2
|
+
|
|
3
|
+
Dense, self-contained facts for writing a SolidRT app.
|
|
4
|
+
Full docs live in docs/ (and the website). When this conflicts with prose docs,
|
|
5
|
+
trust this file and the types in src/types.d.ts and jsx-runtime.d.ts.
|
|
6
|
+
|
|
7
|
+
SolidRT is a custom SolidJS renderer: it paints through a Rust runtime, not the
|
|
8
|
+
DOM. There is no HTML, no CSS cascade, no `className`.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
bun add @solidrt/core # the renderer
|
|
14
|
+
bun add -d @solidrt/cli # the `srt` tool (see its AGENTS.md)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`@solidrt/components` is a separate, optional package of higher-level components
|
|
18
|
+
(see its own AGENTS.md); core primitives alone are enough to build a full app.
|
|
19
|
+
|
|
20
|
+
tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{ "compilerOptions": {
|
|
24
|
+
"jsx": "preserve",
|
|
25
|
+
"jsxImportSource": "@solidrt/core",
|
|
26
|
+
"moduleResolution": "bundler",
|
|
27
|
+
"strict": true
|
|
28
|
+
} }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
32
|
+
2.0.0-beta.14); bun resolves them from peerDependencies.
|
|
33
|
+
|
|
34
|
+
## Element model (the parts that are easy to get wrong)
|
|
35
|
+
|
|
36
|
+
- `render(() => <App />)`. The returned root MUST be a `<window>` or it throws.
|
|
37
|
+
Call render once, at the top level.
|
|
38
|
+
|
|
39
|
+
- Two kinds of element:
|
|
40
|
+
- Containers - `<window>`, `<view>`. Do layout + transform + pointer events.
|
|
41
|
+
THEY DO NOT PAINT. A `<view>` has no background/fill prop.
|
|
42
|
+
- Draw primitives - `<rect>`, `<oval>`, `<path>`, `<texture>`, `<text>`.
|
|
43
|
+
These paint. To give a view a background, render a draw primitive (e.g.
|
|
44
|
+
`<d-rect>`) as a child, behind the content.
|
|
45
|
+
|
|
46
|
+
- Paint color is the `color` prop (a CSS color string). There is NO `fill`,
|
|
47
|
+
`stroke`, or `background` prop (some older doc examples are wrong about this).
|
|
48
|
+
Outlines: `drawStyle="stroke"` (or "stroke-and-fill") plus `strokeWidth`.
|
|
49
|
+
Corner radius on draw primitives: `radius` (number or [tl, tr, br, bl]).
|
|
50
|
+
|
|
51
|
+
- Registered JSX intrinsics: `window`, `view`, `text`, `rect`, `oval`, `path`,
|
|
52
|
+
`texture`, `audio`, plus the `d-` variants `d-view`, `d-rect`, `d-oval`,
|
|
53
|
+
`d-path`, `d-texture`, `d-text`. NOTE: `<line>` has a LineProps type but is
|
|
54
|
+
NOT a registered intrinsic - it will not typecheck.
|
|
55
|
+
|
|
56
|
+
- Plain vs `d-` variant (the `d-` prefix means "detached" - detached from the
|
|
57
|
+
layout engine, Taffy): a plain element (e.g. `rect`) is `RectProps &
|
|
58
|
+
LayoutProps`, so it draws AND is laid out by Taffy. The detached variant
|
|
59
|
+
(`d-rect`) is `RectProps` only - it draws but is NOT in the layout pass; you
|
|
60
|
+
place it yourself with `x`/`y` (omit them and it fills the parent, which is
|
|
61
|
+
how backgrounds work). Reach for `d-` whenever you want explicit coordinate
|
|
62
|
+
positioning instead of layout. It is also a performance lever: for many
|
|
63
|
+
directly-positioned, often-animating elements (e.g. hundreds of balls), `d-`
|
|
64
|
+
skips the per-element layout that plain elements would incur.
|
|
65
|
+
|
|
66
|
+
- Events: there is NO `onClick`/`onPress`. A "button" is a `<view>`/`<rect>`
|
|
67
|
+
with `onPointerDown`. Handlers: onPointerDown/Up/Move/Enter/Leave, onWheel,
|
|
68
|
+
onKeyDown/Up, onTextInput, onFocus/onBlur. Text entry: focus a node with an
|
|
69
|
+
`onTextInput` handler (setFocus activates the on-screen keyboard).
|
|
70
|
+
|
|
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.
|
|
79
|
+
|
|
80
|
+
- Device/GPU access via subpath imports: @solidrt/core/camera, /microphone,
|
|
81
|
+
/speech, /gpu. Image flow: `decodeImage(bytes)` -> `createTexture(data,w,h)`
|
|
82
|
+
-> `<texture src={id} />`.
|
|
83
|
+
|
|
84
|
+
## Minimal app, core primitives only (verified to render)
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { render } from "@solidrt/core"
|
|
88
|
+
import { createSignal } from "@solidjs/signals"
|
|
89
|
+
|
|
90
|
+
function App() {
|
|
91
|
+
let [count, setCount] = createSignal(0)
|
|
92
|
+
return (
|
|
93
|
+
<window flexDirection="column" alignItems="center" justifyContent="center" gap={24}>
|
|
94
|
+
<d-rect color="#0b0f17" /> {/* window background */}
|
|
95
|
+
<text color="#1f6feb" fontSize={48} fontWeight={800}>{count()}</text>
|
|
96
|
+
<view onPointerDown={() => setCount((c) => c + 1)}
|
|
97
|
+
padding={16} alignItems="center" justifyContent="center">
|
|
98
|
+
<d-rect color="#1f6feb" radius={12} /> {/* button background, underlays the label */}
|
|
99
|
+
<text color="#ffffff" fontSize={20}>increment</text>
|
|
100
|
+
</view>
|
|
101
|
+
</window>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
render(() => <App />)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Note the two `<d-rect>` underlays: a `<view>`/`<window>` does not paint, so a
|
|
109
|
+
background is a draw-primitive child placed behind the content.
|
|
110
|
+
|
|
111
|
+
To run and verify (incl. headless), see @solidrt/cli (its AGENTS.md). For
|
|
112
|
+
higher-level components, see @solidrt/components (its AGENTS.md).
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ A low-level toolkit for creating cross-platform applications.
|
|
|
4
4
|
|
|
5
5
|
_SolidRT is in pre-alpha stage. Anything can and will be changed._
|
|
6
6
|
|
|
7
|
+
> LLM agents: see [AGENTS.md](./AGENTS.md) for a dense, self-contained quickstart.
|
|
8
|
+
|
|
7
9
|
## Getting started
|
|
8
10
|
|
|
9
11
|
Prerequisites: [bun](https://bun.sh) (only required for development; not needed to run built apps).
|
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,24 +9,27 @@
|
|
|
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
|
},
|
|
17
19
|
"files": [
|
|
18
20
|
"src/",
|
|
19
21
|
"jsx-runtime.d.ts",
|
|
20
|
-
"
|
|
22
|
+
"AGENTS.md"
|
|
21
23
|
],
|
|
22
24
|
"dependencies": {
|
|
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",
|
|
30
|
-
"@solidjs/universal": "2.0.0-beta.14"
|
|
32
|
+
"@solidjs/universal": "2.0.0-beta.14",
|
|
33
|
+
"solid-js": "2.0.0-beta.14"
|
|
31
34
|
}
|
|
32
35
|
}
|
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,23 +1,60 @@
|
|
|
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
|
-
|
|
34
|
+
let id = gpu.createMutableTexture(data, width, height)
|
|
35
|
+
if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
36
|
+
return id
|
|
19
37
|
}
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
*/
|
|
50
|
+
export function createShader(
|
|
51
|
+
fragmentSrc: string,
|
|
52
|
+
width: number,
|
|
53
|
+
height: number,
|
|
54
|
+
params?: Record<string, number>,
|
|
55
|
+
textures?: Record<string, number>,
|
|
56
|
+
): number {
|
|
57
|
+
let id = gpu.createShader(fragmentSrc, width, height, params, textures)
|
|
58
|
+
if (getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
59
|
+
return id
|
|
23
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
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"
|