@solidrt/core 0.0.14 → 0.0.16
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/examples/README.md +27 -0
- package/examples/background-rect.tsx +15 -0
- package/examples/detached-positioning.tsx +21 -0
- package/examples/frame-animation.tsx +26 -0
- package/examples/gpu-shader.tsx +36 -0
- package/examples/image.tsx +27 -0
- package/examples/on-layout-connect.tsx +39 -0
- package/examples/portal.tsx +38 -0
- package/examples/text-paint-styling.tsx +18 -0
- package/examples/view-layout.tsx +20 -0
- package/examples/window-root.tsx +14 -0
- package/examples/window-signals.tsx +29 -0
- package/package.json +2 -1
- package/src/gpu.ts +7 -6
- package/src/image.ts +51 -5
- package/src/index.ts +3 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @solidrt/core examples
|
|
2
|
+
|
|
3
|
+
Single-concept SolidRT patterns. Each file is a complete, runnable app (ends in
|
|
4
|
+
`render(() => <App />)`) demonstrating exactly one thing - copy one and adapt it.
|
|
5
|
+
For the SolidJS 2.0 reactivity/control-flow model see `solid-js/CHEATSHEET.md`;
|
|
6
|
+
for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
7
|
+
|
|
8
|
+
## Host elements and layout
|
|
9
|
+
- `window-root.tsx` - the minimal app; the root must be `<window>`.
|
|
10
|
+
- `view-layout.tsx` - `<view>` as a flex container; containers do not paint.
|
|
11
|
+
- `background-rect.tsx` - a `d-rect` filling its parent as a background.
|
|
12
|
+
- `detached-positioning.tsx` - the `d-` prefix: x/y placement, no reflow, detached-only children.
|
|
13
|
+
- `text-paint-styling.tsx` - the uniform `color` prop; `drawStyle="stroke"` vs fill.
|
|
14
|
+
|
|
15
|
+
## Frame and lifecycle
|
|
16
|
+
- `frame-animation.tsx` - `onFrame` driving a transform animation each frame.
|
|
17
|
+
- `on-layout-connect.tsx` - `onLayout` + `getBoundingBox` connecting laid-out boxes with a `d-path`.
|
|
18
|
+
|
|
19
|
+
## Window state
|
|
20
|
+
- `window-signals.tsx` - reactive `windowSize()` / `safeArea()` accessors (prefer over `onResize`).
|
|
21
|
+
|
|
22
|
+
## Overlays
|
|
23
|
+
- `portal.tsx` - `createPortal` relocating content to the window root to escape clipping.
|
|
24
|
+
|
|
25
|
+
## Images and GPU
|
|
26
|
+
- `image.tsx` - `createImage` (async value: fetch + decode + upload) read inside a `<Loading>` boundary and shown with `<texture>`.
|
|
27
|
+
- `gpu-shader.tsx` - a GLSL fragment shader rendered to a texture, animated via `setShaderParams`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Containers do not paint, so a background is just a draw primitive placed behind
|
|
2
|
+
// the content. A d- primitive with no x/y/w/h fills its parent, which is exactly
|
|
3
|
+
// what you want for a background. Put it FIRST so siblings paint on top of it.
|
|
4
|
+
import { render } from "@solidrt/core"
|
|
5
|
+
|
|
6
|
+
function App() {
|
|
7
|
+
return (
|
|
8
|
+
<window alignItems="center" justifyContent="center">
|
|
9
|
+
<d-rect color="#1a3380" />
|
|
10
|
+
<text fontSize={28} color="#ffffff">Content on a full-bleed background</text>
|
|
11
|
+
</window>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
render(() => <App />)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The d- prefix means "detached from layout": the layout engine ignores it and
|
|
2
|
+
// you place it yourself with x/y. Because a detached node does not participate in
|
|
3
|
+
// layout, moving it (animating x/y) does not reflow its siblings - which makes
|
|
4
|
+
// d- elements the right choice for overlays, badges, and anything that moves
|
|
5
|
+
// independently. The rule: a detached node can only contain other detached
|
|
6
|
+
// nodes. Everything under a d- element must itself be a d- element (here d-rect +
|
|
7
|
+
// d-text) - nesting a plain <view> or <text> inside a d- element is an error.
|
|
8
|
+
import { render } from "@solidrt/core"
|
|
9
|
+
|
|
10
|
+
function App() {
|
|
11
|
+
return (
|
|
12
|
+
<window>
|
|
13
|
+
<d-view x={40} y={60}>
|
|
14
|
+
<d-rect w={160} h={64} radius={12} color="#3366b3" />
|
|
15
|
+
<d-text x={16} y={20} fontSize={18} color="#ffffff">Badge</d-text>
|
|
16
|
+
</d-view>
|
|
17
|
+
</window>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render(() => <App />)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// onFrame(fn) is the per-frame animation hook. tick is runtime-paced time in ms
|
|
2
|
+
// (smooth even when frame times jitter), frame is the present count, rate is the
|
|
3
|
+
// refresh rate in Hz. Drive a signal from the tick and read it in JSX - the graph
|
|
4
|
+
// repaints each frame. A pending onFrame is a standing request for the next
|
|
5
|
+
// frame, so the loop runs only while something is animating and stops when the
|
|
6
|
+
// callback is cleaned up (automatic within a reactive scope).
|
|
7
|
+
//
|
|
8
|
+
// Animate a transform (rotate / scale / x / y on a <view>), not a layout prop:
|
|
9
|
+
// transforms are applied at paint time, while animating width/margin/etc would
|
|
10
|
+
// re-run layout every frame.
|
|
11
|
+
import { render, onFrame, createSignal } from "@solidrt/core"
|
|
12
|
+
|
|
13
|
+
function App() {
|
|
14
|
+
let [angle, setAngle] = createSignal(0)
|
|
15
|
+
onFrame((tick) => setAngle(tick / 1000)) // radians; ~1 turn every 6.3s
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<window alignItems="center" justifyContent="center">
|
|
19
|
+
<view width={120} height={120} rotate={angle()}>
|
|
20
|
+
<rect width={120} height={120} radius={16} color="#3366b3" />
|
|
21
|
+
</view>
|
|
22
|
+
</window>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render(() => <App />)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// createShader compiles a GLSL ES 3.00 fragment shader and renders it into a
|
|
2
|
+
// texture, returning a texture id you display with <texture src={id}>. The
|
|
3
|
+
// fragment body may reference vUV (0..1, top-left origin), iResolution, iTime, and
|
|
4
|
+
// any `uniform float` it declares; there is no #version line - the runtime injects
|
|
5
|
+
// the preamble. The texture is freed automatically when the reactive owner is
|
|
6
|
+
// disposed.
|
|
7
|
+
//
|
|
8
|
+
// iResolution is filled in for you, but iTime is NOT - drive it (and any other
|
|
9
|
+
// uniform) yourself with setShaderParams from onFrame to animate. The shader's
|
|
10
|
+
// size is baked in at creation.
|
|
11
|
+
import { render, onFrame } from "@solidrt/core"
|
|
12
|
+
import { createShader, setShaderParams } from "@solidrt/core/gpu"
|
|
13
|
+
|
|
14
|
+
let FRAGMENT = `
|
|
15
|
+
void main() {
|
|
16
|
+
vec2 uv = vUV;
|
|
17
|
+
float t = iTime * 2.0;
|
|
18
|
+
float a = 0.5 + 0.5 * sin(uv.x * 10.0 + t);
|
|
19
|
+
float b = 0.5 + 0.5 * sin(uv.y * 10.0 - t * 1.3);
|
|
20
|
+
float c = 0.5 + 0.5 * sin((uv.x + uv.y) * 8.0 + t * 0.7);
|
|
21
|
+
fragColor = vec4(a, b, c, 1.0);
|
|
22
|
+
}
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
function App() {
|
|
26
|
+
let id = createShader(FRAGMENT, 512, 512, { iTime: 0 })
|
|
27
|
+
onFrame((tick) => setShaderParams(id, { iTime: tick / 1000 }))
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<window alignItems="center" justifyContent="center">
|
|
31
|
+
<texture src={id} width={400} height={400} />
|
|
32
|
+
</window>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render(() => <App />)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// createImage loads an image as a SolidJS 2.0 async value and returns an accessor
|
|
2
|
+
// for its GPU texture id. It handles fetch, decode, GPU upload, and cleanup for
|
|
3
|
+
// you. A string source is fetched; a Uint8Array is decoded directly. The texture
|
|
4
|
+
// carries its own pixel size, so <texture> needs no width/height to show it at
|
|
5
|
+
// natural size - pass them only to scale it.
|
|
6
|
+
//
|
|
7
|
+
// Reading img() suspends until ready, so read it inside a <Loading> boundary
|
|
8
|
+
// (this is the 2.0 async mechanic, not a manual undefined-signal + <Show>); a
|
|
9
|
+
// load failure surfaces to <Errored>. Pass an accessor (createImage(() => src()))
|
|
10
|
+
// to make the source reactive - the image reloads and the old texture is freed.
|
|
11
|
+
// For manual control, decodeImage + createTexture (from @solidrt/core/gpu) are
|
|
12
|
+
// the primitives underneath.
|
|
13
|
+
import { render, createImage, Loading } from "@solidrt/core"
|
|
14
|
+
|
|
15
|
+
function App() {
|
|
16
|
+
let img = createImage("https://picsum.photos/seed/solidrt/400/300")
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<window alignItems="center" justifyContent="center">
|
|
20
|
+
<Loading fallback={<text color="#888">loading...</text>}>
|
|
21
|
+
<texture src={img()} />
|
|
22
|
+
</Loading>
|
|
23
|
+
</window>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
render(() => <App />)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// onLayout(fn) fires after layout is computed but before paint - the one point
|
|
2
|
+
// where measured geometry is available via getBoundingBox (a window-relative
|
|
3
|
+
// snapshot, valid only here or in an event handler). Its canonical use is to
|
|
4
|
+
// connect or annotate laid-out elements with detached drawing: let the layout
|
|
5
|
+
// engine place the boxes, read their boxes, then draw a d-path between them.
|
|
6
|
+
//
|
|
7
|
+
// Two rules make it safe:
|
|
8
|
+
// 1. Write the result to something that does NOT affect layout - a d-path's `d`,
|
|
9
|
+
// a detached node's position. Writing a layout-affecting prop here forces an
|
|
10
|
+
// extra layout pass every frame.
|
|
11
|
+
// 2. Call flush() after the write so it lands before the display list is built
|
|
12
|
+
// (onLayout runs after the frame's normal flush).
|
|
13
|
+
import { render, onLayout, getBoundingBox, createSignal, flush } from "@solidrt/core"
|
|
14
|
+
|
|
15
|
+
function App() {
|
|
16
|
+
let boxA!: { id: number }
|
|
17
|
+
let boxB!: { id: number }
|
|
18
|
+
let [d, setD] = createSignal("")
|
|
19
|
+
|
|
20
|
+
onLayout(() => {
|
|
21
|
+
let a = getBoundingBox(boxA)
|
|
22
|
+
let b = getBoundingBox(boxB)
|
|
23
|
+
if (!a || !b) return
|
|
24
|
+
let ax = a.x + a.width / 2, ay = a.y + a.height / 2
|
|
25
|
+
let bx = b.x + b.width / 2, by = b.y + b.height / 2
|
|
26
|
+
setD(`M ${ax} ${ay} L ${bx} ${by}`)
|
|
27
|
+
flush()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<window flexDirection="row" justifyContent="space-between" alignItems="center" padding={48}>
|
|
32
|
+
<rect ref={n => (boxA = n)} width={80} height={80} radius={8} color="#3366b3" />
|
|
33
|
+
<rect ref={n => (boxB = n)} width={80} height={80} radius={8} color="#6699e6" />
|
|
34
|
+
<d-path d={d()} color="#e0245e" drawStyle="stroke" strokeWidth={3} />
|
|
35
|
+
</window>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render(() => <App />)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// createPortal relocates an already-built node to the window root (or a given
|
|
2
|
+
// mount), so content declared deep in the tree can escape the layout, clipping,
|
|
3
|
+
// and stacking of its surroundings - the primitive behind overlays like modals,
|
|
4
|
+
// menus, and tooltips. It moves one concrete node and removes it again when the
|
|
5
|
+
// surrounding reactive scope disposes.
|
|
6
|
+
//
|
|
7
|
+
// Key points:
|
|
8
|
+
// - A JSX element's runtime value IS the built node, so pass the JSX straight to
|
|
9
|
+
// createPortal. It returns void, so `return createPortal(...)` renders nothing
|
|
10
|
+
// in place while the content lives at the mount.
|
|
11
|
+
// - The default mount is the window's flex root, so the portaled node must be
|
|
12
|
+
// position="absolute" - otherwise it takes flow space and displaces content.
|
|
13
|
+
// - Pass a second argument (a node captured from a ref) to mount elsewhere.
|
|
14
|
+
import { render, createPortal } from "@solidrt/core"
|
|
15
|
+
|
|
16
|
+
// Declared inside the clipped card below, but drawn at the window root, on top of
|
|
17
|
+
// everything and outside the card's overflow clip.
|
|
18
|
+
function Banner() {
|
|
19
|
+
return createPortal(
|
|
20
|
+
<view position="absolute" top={0} left={0} right={0} padding={12}>
|
|
21
|
+
<d-rect color="#e0245e" />
|
|
22
|
+
<text color="#ffffff">Portaled banner - escaped the card</text>
|
|
23
|
+
</view>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function App() {
|
|
28
|
+
return (
|
|
29
|
+
<window padding={40}>
|
|
30
|
+
<view flex={1} overflow="hidden" alignItems="center" justifyContent="center">
|
|
31
|
+
<rect width={140} height={140} radius={8} color="#3366b3" />
|
|
32
|
+
<Banner />
|
|
33
|
+
</view>
|
|
34
|
+
</window>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
render(() => <App />)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Painting is uniform across primitives: the `color` prop sets the paint (a CSS
|
|
2
|
+
// color string or a gradient). There is no fill / stroke / background prop. To
|
|
3
|
+
// outline instead of fill, set drawStyle="stroke" and strokeWidth; "stroke-and-
|
|
4
|
+
// fill" does both. The same color prop styles <text>, alongside fontSize /
|
|
5
|
+
// fontWeight / fontFamily.
|
|
6
|
+
import { render } from "@solidrt/core"
|
|
7
|
+
|
|
8
|
+
function App() {
|
|
9
|
+
return (
|
|
10
|
+
<window flexDirection="column" gap={16} padding={24} alignItems="center" justifyContent="center">
|
|
11
|
+
<text fontSize={28} fontWeight={700} color="#1a3380">Filled text</text>
|
|
12
|
+
<rect width={160} height={64} radius={12} color="#3366b3" />
|
|
13
|
+
<oval width={120} height={64} drawStyle="stroke" strokeWidth={4} color="#1a3380" />
|
|
14
|
+
</window>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
render(() => <App />)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// <view> is the layout container: a flexbox box positioned by the layout engine.
|
|
2
|
+
// It does NOT paint anything itself - it has no color. To make a box visible you
|
|
3
|
+
// place a draw primitive inside it (here an attached <rect>, which is both laid
|
|
4
|
+
// out and painted). Flex props (flexDirection, gap, padding, alignItems) work as
|
|
5
|
+
// in CSS.
|
|
6
|
+
import { render } from "@solidrt/core"
|
|
7
|
+
|
|
8
|
+
function App() {
|
|
9
|
+
return (
|
|
10
|
+
<window>
|
|
11
|
+
<view flex={1} flexDirection="column" gap={12} padding={24} justifyContent="center">
|
|
12
|
+
<rect height={48} radius={8} color="#3366b3" />
|
|
13
|
+
<rect height={48} radius={8} color="#6699e6" />
|
|
14
|
+
<rect height={48} radius={8} color="#99c2f0" />
|
|
15
|
+
</view>
|
|
16
|
+
</window>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
render(() => <App />)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The minimal valid app. render() is called once at the top level and the root
|
|
2
|
+
// element MUST be <window> - anything else throws. <window> takes layout props
|
|
3
|
+
// (it is a flex container), so children can be centered here directly.
|
|
4
|
+
import { render } from "@solidrt/core"
|
|
5
|
+
|
|
6
|
+
function App() {
|
|
7
|
+
return (
|
|
8
|
+
<window title="Hello" alignItems="center" justifyContent="center">
|
|
9
|
+
<text fontSize={24} color="#222">Hello SolidRT</text>
|
|
10
|
+
</window>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
render(() => <App />)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Window state is exposed as reactive accessors - read them in JSX and the UI
|
|
2
|
+
// updates when the window changes. Prefer these over the onResize event callback:
|
|
3
|
+
// they are reactive, sticky (the first read sees the current value), and live for
|
|
4
|
+
// the whole app.
|
|
5
|
+
//
|
|
6
|
+
// windowSize() -> { width, height }
|
|
7
|
+
// safeArea() -> { top, left, right, bottom } inset distances from each edge
|
|
8
|
+
// displayScale() -> device pixel ratio
|
|
9
|
+
//
|
|
10
|
+
// safeArea is mostly an interactivity boundary: the platform may intercept
|
|
11
|
+
// touches/gestures inside the insets (system bars, notch, home indicator), so do
|
|
12
|
+
// not place interactive content there. For plain drawing it is usually fine to
|
|
13
|
+
// extend into the left/right insets (full-bleed backgrounds); top and bottom are
|
|
14
|
+
// what matter, since content there - text especially - can be covered. So pad the
|
|
15
|
+
// top/bottom by the inset and let the background fill the whole window.
|
|
16
|
+
import { render, windowSize, safeArea } from "@solidrt/core"
|
|
17
|
+
|
|
18
|
+
function App() {
|
|
19
|
+
return (
|
|
20
|
+
<window>
|
|
21
|
+
<d-rect color="#101418" />
|
|
22
|
+
<view flex={1} flexDirection="column" gap={8} paddingTop={safeArea().top} paddingBottom={safeArea().bottom}>
|
|
23
|
+
<text fontSize={18} color="#e6e6e6">{windowSize().width} x {windowSize().height}</text>
|
|
24
|
+
</view>
|
|
25
|
+
</window>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render(() => <App />)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"src/",
|
|
22
|
+
"examples/",
|
|
22
23
|
"jsx-runtime.d.ts",
|
|
23
24
|
"AGENTS.md"
|
|
24
25
|
],
|
package/src/gpu.ts
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
import { getOwner, onCleanup } from "@solidjs/signals"
|
|
7
7
|
import * as gpu from "flux:gpu"
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
9
|
+
// Imperative companions to the reactive create* helpers, re-exported so callers
|
|
10
|
+
// that depend on @solidrt/core -- like @solidrt/components -- need not import flux
|
|
11
|
+
// directly: destroyTexture for the manual-cleanup path (textures made outside a
|
|
12
|
+
// reactive scope, e.g. after an await, are not auto-freed), setShaderParams to
|
|
13
|
+
// drive a shader's uniforms over time, and uploadTexture to push new pixels into
|
|
14
|
+
// a mutable texture.
|
|
15
|
+
export { destroyTexture, setShaderParams, uploadTexture } from "flux:gpu"
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Uploads raw RGBA8 pixels to an immutable GPU texture and returns its id (use
|
package/src/image.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
// CPU image codec
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// CPU image codec plus the reactive load-and-upload convenience. decodeImage is
|
|
2
|
+
// the raw primitive (no GPU involved); createImage is the owner-aware layer on
|
|
3
|
+
// top that fetches/decodes/uploads for you and swaps the texture when the source
|
|
4
|
+
// changes - the same relationship createTexture/createShader have to flux:gpu.
|
|
5
|
+
|
|
6
|
+
import { createMemo, onCleanup, NotReadyError } from "@solidjs/signals"
|
|
7
|
+
import { createTexture, destroyTexture } from "./gpu"
|
|
5
8
|
|
|
6
9
|
export type DecodedImage = {
|
|
7
10
|
data: Uint8Array
|
|
@@ -12,8 +15,51 @@ export type DecodedImage = {
|
|
|
12
15
|
/**
|
|
13
16
|
* Decodes encoded image bytes (PNG, JPEG, and the other formats the runtime's
|
|
14
17
|
* image decoder supports) into raw, tightly-packed RGBA8 pixels plus the
|
|
15
|
-
* decoded dimensions. Feed the result straight into `createTexture`.
|
|
18
|
+
* decoded dimensions. Feed the result straight into `createTexture`. Use this
|
|
19
|
+
* when you want manual control; for the common case reach for `createImage`.
|
|
16
20
|
*/
|
|
17
21
|
export function decodeImage(bytes: Uint8Array): DecodedImage {
|
|
18
22
|
return image.decodeImage(bytes)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ImageSource = string | Uint8Array
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Loads an image as an async computation and returns a reactive accessor for its
|
|
29
|
+
* GPU texture id. This is a SolidJS 2.0 async value: reading it suspends until
|
|
30
|
+
* the image is ready, so read it inside a `<Loading>` boundary (a load failure
|
|
31
|
+
* surfaces to `<Errored>`). A string source is fetched; a Uint8Array is decoded
|
|
32
|
+
* directly. Pass an accessor instead of a value to make the source reactive -
|
|
33
|
+
* the image reloads and the old texture is freed whenever it changes; the
|
|
34
|
+
* current texture is freed when the owner is disposed. Display it with
|
|
35
|
+
* `<texture src={id()} />`; the texture carries its own pixel size, so no
|
|
36
|
+
* width/height is needed unless you want to scale it.
|
|
37
|
+
*/
|
|
38
|
+
export function createImage(src: ImageSource | (() => ImageSource)): () => number {
|
|
39
|
+
let getSrc = typeof src === "function" ? src : () => src
|
|
40
|
+
let generation = 0
|
|
41
|
+
|
|
42
|
+
return createMemo<number>(async () => {
|
|
43
|
+
let source = getSrc()
|
|
44
|
+
let mine = ++generation
|
|
45
|
+
|
|
46
|
+
// Register cleanup synchronously, before the await: an onCleanup added after
|
|
47
|
+
// an await is orphaned because the reactive owner is not restored across it.
|
|
48
|
+
// The holder is filled in once the texture exists.
|
|
49
|
+
let holder = { id: -1 }
|
|
50
|
+
onCleanup(() => {
|
|
51
|
+
if (holder.id >= 0) destroyTexture(holder.id)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
let bytes = typeof source === "string" ? await (await fetch(source)).bytes() : source
|
|
55
|
+
|
|
56
|
+
// If the source changed while we were loading, this run is superseded. Skip
|
|
57
|
+
// the GPU upload and stay pending: a texture created here would leak, since
|
|
58
|
+
// superseded async runs are not otherwise cleaned up. The newer run wins.
|
|
59
|
+
if (mine !== generation) throw new NotReadyError()
|
|
60
|
+
|
|
61
|
+
let { data, width, height } = decodeImage(bytes)
|
|
62
|
+
holder.id = createTexture(data, width, height)
|
|
63
|
+
return holder.id
|
|
64
|
+
})
|
|
19
65
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,8 +6,8 @@ export type { Gradient, GradientStop } from "./color"
|
|
|
6
6
|
export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur } from "./window"
|
|
7
7
|
export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
|
|
8
8
|
export { createTexture } from "./gpu"
|
|
9
|
-
export { decodeImage } from "./image"
|
|
10
|
-
export type { DecodedImage } from "./image"
|
|
9
|
+
export { createImage, decodeImage } from "./image"
|
|
10
|
+
export type { DecodedImage, ImageSource } from "./image"
|
|
11
11
|
export type {
|
|
12
12
|
LayoutProps,
|
|
13
13
|
TransformProps,
|
|
@@ -52,6 +52,7 @@ export {
|
|
|
52
52
|
mapArray,
|
|
53
53
|
repeat,
|
|
54
54
|
untrack,
|
|
55
|
+
flush,
|
|
55
56
|
onCleanup,
|
|
56
57
|
onSettled,
|
|
57
58
|
} from "@solidjs/signals"
|