@solidrt/core 0.0.44 → 0.0.45
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 +2 -1
- package/examples/gpu-draw-list.tsx +110 -0
- package/examples/gpu-instancing.tsx +47 -18
- package/examples/gpu-pipeline.tsx +44 -14
- package/examples/responsive-grid.tsx +5 -0
- package/examples/view-shader-history.tsx +85 -0
- package/examples/view-shader.tsx +81 -0
- package/examples/view-viewbox.tsx +86 -0
- package/package.json +2 -2
- package/src/gpu.ts +79 -14
- package/src/types.d.ts +61 -0
package/examples/README.md
CHANGED
|
@@ -8,6 +8,7 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
8
8
|
## Host elements and layout
|
|
9
9
|
- `window-root.tsx` - the minimal app; the root must be `<window>`.
|
|
10
10
|
- `view-layout.tsx` - `<view>` as a flex container; containers do not paint.
|
|
11
|
+
- `view-viewbox.tsx` - `viewBox` on a `<view>`: author a scene once in fixed design units and let the view uniformly scale-and-center (letterbox) that space into its box. A pure fit transform - it never sizes the element (layout still does); children live in design space (the box they inherit IS the design size, so a bare `d-rect` fills it); pointer `localX`/`localY` arrive in design units. The fixed-aspect alternative to `windowSizeClass` reflow for diagrams, slides, dashboards, game boards.
|
|
11
12
|
- `background-rect.tsx` - a `d-rect` filling its parent as a background.
|
|
12
13
|
- `detached-positioning.tsx` - the `d-` prefix: x/y placement, no reflow, detached-only children.
|
|
13
14
|
- `text-paint-styling.tsx` - the uniform `color` prop; `drawStyle="stroke"` vs fill.
|
|
@@ -27,7 +28,7 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
27
28
|
|
|
28
29
|
## Window state
|
|
29
30
|
- `window-signals.tsx` - reactive `windowSize()` / `safeArea()` accessors (prefer over `onResize`).
|
|
30
|
-
- `responsive-grid.tsx` - one app across phone/tablet/desktop: `capabilities.windowSizeClass` (Material 3 breakpoints, a reactive getter) drives the column count and `windowSize()` sizes each card; reflows on resize.
|
|
31
|
+
- `responsive-grid.tsx` - one app across phone/tablet/desktop: `capabilities.windowSizeClass` (Material 3 breakpoints, a reactive getter) drives the column count and `windowSize()` sizes each card; reflows on resize. The reflow answer; for fixed-aspect content use `view-viewbox.tsx` instead.
|
|
31
32
|
|
|
32
33
|
## Overlays
|
|
33
34
|
- `portal.tsx` - `createPortal` relocating content to the window root to escape clipping.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// A draw target: one render target holding an ordered, mutable LIST of
|
|
2
|
+
// draws - two orbiting triangles from two different programs sharing one
|
|
3
|
+
// depth buffer (they occlude each other correctly as they cross), plus a
|
|
4
|
+
// third entry added and removed on a timer. One render of the target is one
|
|
5
|
+
// GPU pass no matter how many entries it holds, and per-entry params
|
|
6
|
+
// (setDrawParams) are the per-object channel: each triangle carries its own
|
|
7
|
+
// angle uniform, the model-matrix pattern at toy scale.
|
|
8
|
+
//
|
|
9
|
+
// The depth split to notice: the TARGET owns the storage (depth: true on
|
|
10
|
+
// createDrawTarget - what makes cross-entry occlusion work), each PIPELINE
|
|
11
|
+
// owns the behavior (depth: true on createRenderPipeline - this draw tests
|
|
12
|
+
// and writes). A depth-testing pipeline added to a depthless draw target
|
|
13
|
+
// throws at addDraw.
|
|
14
|
+
import { render, onFrame } from "@solidrt/core"
|
|
15
|
+
import {
|
|
16
|
+
addDraw,
|
|
17
|
+
compileShader,
|
|
18
|
+
createBuffer,
|
|
19
|
+
createDrawTarget,
|
|
20
|
+
createRenderPipeline,
|
|
21
|
+
glsl,
|
|
22
|
+
linkProgram,
|
|
23
|
+
removeDraw,
|
|
24
|
+
setDrawParams,
|
|
25
|
+
} from "@solidrt/core/gpu"
|
|
26
|
+
import type { DrawId } from "@solidrt/core/gpu"
|
|
27
|
+
|
|
28
|
+
let VERTEX = glsl`
|
|
29
|
+
in vec2 aPos;
|
|
30
|
+
uniform float uAngle;
|
|
31
|
+
|
|
32
|
+
void main() {
|
|
33
|
+
// Orbit in x, swing through depth in z: the two entries cross each
|
|
34
|
+
// other, and the shared depth buffer decides who is in front.
|
|
35
|
+
float c = cos(uAngle), s = sin(uAngle);
|
|
36
|
+
vec2 p = aPos * 0.55 + vec2(0.55 * c, 0.0);
|
|
37
|
+
gl_Position = vec4(p, 0.5 * s, 1.0);
|
|
38
|
+
}
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
let FRAGMENT_WARM = glsl`
|
|
42
|
+
void main() {
|
|
43
|
+
fragColor = vec4(0.95, 0.45, 0.2, 1.0);
|
|
44
|
+
}
|
|
45
|
+
`
|
|
46
|
+
|
|
47
|
+
let FRAGMENT_COOL = glsl`
|
|
48
|
+
void main() {
|
|
49
|
+
fragColor = vec4(0.25, 0.55, 0.95, 1.0);
|
|
50
|
+
}
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
let FRAGMENT_PULSE = glsl`
|
|
54
|
+
uniform float uPhase;
|
|
55
|
+
void main() {
|
|
56
|
+
fragColor = vec4(vec3(0.6 + 0.4 * sin(uPhase)), 1.0);
|
|
57
|
+
}
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
function App() {
|
|
61
|
+
let triangle = createBuffer(new Float32Array([0, 0.6, -0.5, -0.4, 0.5, -0.4]), { label: "tri" })
|
|
62
|
+
let vs = compileShader("vertex", VERTEX, { header: true })
|
|
63
|
+
let attrs = [{ name: "aPos", format: "vec2" as const }]
|
|
64
|
+
let warm = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_WARM, { header: true })), {
|
|
65
|
+
attributes: attrs,
|
|
66
|
+
depth: true,
|
|
67
|
+
})
|
|
68
|
+
let cool = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_COOL, { header: true })), {
|
|
69
|
+
attributes: attrs,
|
|
70
|
+
depth: true,
|
|
71
|
+
})
|
|
72
|
+
let pulse = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_PULSE, { header: true })), {
|
|
73
|
+
attributes: attrs,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
let target = createDrawTarget(512, 512, { depth: true, clearColor: [0.04, 0.04, 0.08, 1], label: "orbits" })
|
|
77
|
+
let warmDraw = addDraw(target, warm, { uAngle: 0 }, { buffer: triangle })
|
|
78
|
+
let coolDraw = addDraw(target, cool, { uAngle: Math.PI }, { buffer: triangle })
|
|
79
|
+
|
|
80
|
+
// A third entry blinks in and out every second: add/remove are ordinary
|
|
81
|
+
// per-frame-affordable writes, and a removed DrawId simply retires. Its
|
|
82
|
+
// pipeline declares no depth, so it neither tests nor writes: list order
|
|
83
|
+
// alone places it, and inserting it BEFORE the warm triangle
|
|
84
|
+
// (before: warmDraw) keeps the flash beneath both orbits - appended
|
|
85
|
+
// instead, it would cover them. setDrawOrder is the wholesale form when a
|
|
86
|
+
// scene sorts its whole list.
|
|
87
|
+
let pulseDraw: DrawId | null = null
|
|
88
|
+
onFrame((tick) => {
|
|
89
|
+
let t = tick / 1000
|
|
90
|
+
setDrawParams(target, warmDraw, { uAngle: t })
|
|
91
|
+
setDrawParams(target, coolDraw, { uAngle: t + Math.PI })
|
|
92
|
+
let wantPulse = Math.floor(t) % 2 === 0
|
|
93
|
+
if (wantPulse && pulseDraw === null) {
|
|
94
|
+
pulseDraw = addDraw(target, pulse, { uAngle: t * 0.3, uPhase: t * 5 }, { buffer: triangle, before: warmDraw })
|
|
95
|
+
} else if (!wantPulse && pulseDraw !== null) {
|
|
96
|
+
removeDraw(target, pulseDraw)
|
|
97
|
+
pulseDraw = null
|
|
98
|
+
} else if (pulseDraw !== null) {
|
|
99
|
+
setDrawParams(target, pulseDraw, { uAngle: t * 0.3, uPhase: t * 5 })
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<window alignItems="center" justifyContent="center">
|
|
105
|
+
<texture src={target} width={420} height={420} />
|
|
106
|
+
</window>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
render(() => <App />)
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
// Instanced drawing: one 3-vertex triangle in
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Instanced drawing with per-instance attributes: one 3-vertex triangle in
|
|
2
|
+
// the vertex buffer, drawn hundreds of times, each copy reading its own
|
|
3
|
+
// record from an instance buffer (instanceAttributes on the pipeline +
|
|
4
|
+
// instanceBuffer on the target, vertex divisor 1 underneath). Here each
|
|
5
|
+
// record is one petal of a phyllotaxis spiral - its angle, radius, size, and
|
|
6
|
+
// tint, computed once in JS - so the vertex stage just reads state instead
|
|
7
|
+
// of re-deriving it from gl_InstanceID every frame, and the geometry buffer
|
|
8
|
+
// stays 6 floats no matter the population.
|
|
6
9
|
//
|
|
7
10
|
// The draw range is data: setDraw merges partial updates like params, so the
|
|
8
11
|
// per-frame write below changes only instanceCount while firstVertex and
|
|
9
|
-
// vertexCount keep their values (the whole buffer).
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
12
|
+
// vertexCount keep their values (the whole buffer). With an instance buffer
|
|
13
|
+
// bound, an omitted instanceCount would default to one instance per record
|
|
14
|
+
// (the whole population); the explicit 1 here starts the bloom closed, and
|
|
15
|
+
// the count is bounds-checked against the records either way.
|
|
13
16
|
import { render, onFrame, createSignal } from "@solidrt/core"
|
|
14
17
|
import { createBuffer, createPipelineTexture, glsl, setDraw } from "@solidrt/core/gpu"
|
|
15
18
|
|
|
@@ -17,21 +20,20 @@ let MAX_PETALS = 324
|
|
|
17
20
|
|
|
18
21
|
let VERTEX = glsl`
|
|
19
22
|
in vec2 aPos;
|
|
23
|
+
in float iAngle;
|
|
24
|
+
in float iRadius;
|
|
25
|
+
in float iScale;
|
|
26
|
+
in vec3 iTint;
|
|
20
27
|
out vec3 vTint;
|
|
21
28
|
uniform float uTime;
|
|
22
29
|
|
|
23
30
|
void main() {
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
float i = float(gl_InstanceID);
|
|
27
|
-
float angle = i * 2.39996 + uTime * 0.3;
|
|
28
|
-
float radius = 0.045 * sqrt(i);
|
|
29
|
-
// Petals shrink toward the rim and point outward along the spiral.
|
|
30
|
-
float scale = mix(0.05, 0.016, sqrt(i) / 18.0);
|
|
31
|
+
// The record places the petal; time only spins the whole flower.
|
|
32
|
+
float angle = iAngle + uTime * 0.3;
|
|
31
33
|
float c = cos(angle), s = sin(angle);
|
|
32
|
-
vec2 p =
|
|
34
|
+
vec2 p = iRadius * vec2(c, s) + mat2(c, s, -s, c) * (aPos * iScale);
|
|
33
35
|
gl_Position = vec4(p, 0.0, 1.0);
|
|
34
|
-
vTint =
|
|
36
|
+
vTint = iTint;
|
|
35
37
|
}
|
|
36
38
|
`
|
|
37
39
|
|
|
@@ -43,13 +45,40 @@ let FRAGMENT = glsl`
|
|
|
43
45
|
}
|
|
44
46
|
`
|
|
45
47
|
|
|
48
|
+
// One record per petal (matching instanceAttributes: 6 floats): radius grows
|
|
49
|
+
// with sqrt(index), the angle steps by the golden angle (~2.39996 rad) - the
|
|
50
|
+
// sunflower layout - petals shrink toward the rim, and the tint walks a
|
|
51
|
+
// cosine palette.
|
|
52
|
+
function petalRecords() {
|
|
53
|
+
let records = new Float32Array(MAX_PETALS * 6)
|
|
54
|
+
for (let i = 0; i < MAX_PETALS; i++) {
|
|
55
|
+
let at = i * 6
|
|
56
|
+
let t = Math.sqrt(i) / 18
|
|
57
|
+
records[at] = i * 2.39996
|
|
58
|
+
records[at + 1] = 0.045 * Math.sqrt(i)
|
|
59
|
+
records[at + 2] = 0.05 * (1 - t) + 0.016 * t
|
|
60
|
+
for (let k = 0; k < 3; k++) {
|
|
61
|
+
records[at + 3 + k] = 0.5 + 0.5 * Math.cos(6.2832 * (i / 96) + k * 2.1)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return records
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
function App() {
|
|
47
68
|
// The whole mesh: one triangle, reused by every instance.
|
|
48
69
|
let bufferId = createBuffer(new Float32Array([0, 1.3, -1, -0.75, 1, -0.75]), { label: "petal-tri" })
|
|
70
|
+
let instanceId = createBuffer(petalRecords(), { label: "petal-records" })
|
|
49
71
|
let id = createPipelineTexture(VERTEX, FRAGMENT, 512, 512, { uTime: 0 }, {
|
|
50
72
|
label: "petals",
|
|
51
73
|
attributes: [{ name: "aPos", format: "vec2" }],
|
|
52
74
|
buffer: bufferId,
|
|
75
|
+
instanceAttributes: [
|
|
76
|
+
{ name: "iAngle", format: "f32" },
|
|
77
|
+
{ name: "iRadius", format: "f32" },
|
|
78
|
+
{ name: "iScale", format: "f32" },
|
|
79
|
+
{ name: "iTint", format: "vec3" },
|
|
80
|
+
],
|
|
81
|
+
instanceBuffer: instanceId,
|
|
53
82
|
instanceCount: 1,
|
|
54
83
|
clearColor: [0.03, 0.03, 0.06, 1],
|
|
55
84
|
})
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
// but no vUV, and app-driven uniforms (uTime below) are the source's own
|
|
7
7
|
// declarations. Uniforms are driven exactly like createShaderTexture:
|
|
8
8
|
// declaratively via the <texture> params prop, applied at the next repaint.
|
|
9
|
-
|
|
9
|
+
// The cube draws indexed (indexBuffer + indexFormat: 24 shared vertices
|
|
10
|
+
// stitched by 36 uint16 indices instead of 36 unshared vertices) with its
|
|
11
|
+
// back faces culled (cull: "back") - a closed mesh never shows them, so
|
|
12
|
+
// rastering them is pure waste.
|
|
13
|
+
import { render, onFrame, createSignal, pct } from "@solidrt/core"
|
|
10
14
|
import { createBuffer, createPipelineTexture, glsl } from "@solidrt/core/gpu"
|
|
11
15
|
|
|
12
16
|
let VERTEX = glsl`
|
|
@@ -21,15 +25,19 @@ let VERTEX = glsl`
|
|
|
21
25
|
mat3 rotY = mat3(cy, 0.0, -sy, 0.0, 1.0, 0.0, sy, 0.0, cy);
|
|
22
26
|
mat3 rotX = mat3(1.0, 0.0, 0.0, 0.0, cx, sx, 0.0, -sx, cx);
|
|
23
27
|
vec3 p = rotX * (rotY * aPos);
|
|
24
|
-
p.z
|
|
28
|
+
p.z -= 2.5;
|
|
25
29
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
30
|
+
// Standard right-handed camera at the origin looking down -z, perspective
|
|
31
|
+
// near 1 far 10. Clip y is negated: the target's memory row 0 is clip
|
|
32
|
+
// y = -1, and Impeller samples row 0 as the top, so camera-up needs the
|
|
33
|
+
// flip to be displayed up. With this textbook rig the CCW-outward mesh
|
|
34
|
+
// winds counter-clockwise AS DISPLAYED on its camera-facing faces -
|
|
35
|
+
// exactly the front-face rule cull: "back" tests against.
|
|
36
|
+
float w = -p.z;
|
|
29
37
|
float f = 2.0;
|
|
30
38
|
float a = 11.0 / 9.0;
|
|
31
39
|
float b = -20.0 / 9.0;
|
|
32
|
-
gl_Position = vec4(p.x * f, -p.y * f,
|
|
40
|
+
gl_Position = vec4(p.x * f, -p.y * f, w * a + b, w);
|
|
33
41
|
vColor = aColor;
|
|
34
42
|
}
|
|
35
43
|
`
|
|
@@ -41,12 +49,16 @@ let FRAGMENT = glsl`
|
|
|
41
49
|
}
|
|
42
50
|
`
|
|
43
51
|
|
|
44
|
-
// Interleaved [pos vec3, color vec3]
|
|
45
|
-
|
|
52
|
+
// Interleaved [pos vec3, color vec3]: 24 vertices, 4 per face with that
|
|
53
|
+
// face's color - each corner stored once and stitched into 2 triangles by
|
|
54
|
+
// the index buffer below, the sharing real meshes are made of. Every face
|
|
55
|
+
// winds counter-clockwise seen from outside, so with the projection's y
|
|
56
|
+
// negation the cube culls correctly with cull: "back".
|
|
57
|
+
function cubeVertices(): Float32Array {
|
|
46
58
|
type Vec3 = [number, number, number]
|
|
47
59
|
let verts: number[] = []
|
|
48
60
|
let quad = (a: Vec3, b: Vec3, c: Vec3, d: Vec3, color: Vec3) => {
|
|
49
|
-
for (let p of [a, b, c,
|
|
61
|
+
for (let p of [a, b, c, d]) verts.push(p[0], p[1], p[2], color[0], color[1], color[2])
|
|
50
62
|
}
|
|
51
63
|
let s = 0.5
|
|
52
64
|
quad([-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s], [0.9, 0.3, 0.3]) // front
|
|
@@ -58,26 +70,44 @@ function cube(): Float32Array {
|
|
|
58
70
|
return new Float32Array(verts)
|
|
59
71
|
}
|
|
60
72
|
|
|
73
|
+
// Two triangles per face over its 4 shared vertices: 36 uint16 indices.
|
|
74
|
+
function cubeIndices(): Uint16Array {
|
|
75
|
+
let indices: number[] = []
|
|
76
|
+
for (let face = 0; face < 6; face++) {
|
|
77
|
+
let v = face * 4
|
|
78
|
+
indices.push(v, v + 1, v + 2, v, v + 2, v + 3)
|
|
79
|
+
}
|
|
80
|
+
return new Uint16Array(indices)
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
function App() {
|
|
62
|
-
// Labels name the
|
|
84
|
+
// Labels name the buffers and target in the dev tooling's GPU inventory
|
|
63
85
|
// (and in engine log messages) - free-form, purely diagnostic.
|
|
64
|
-
let bufferId = createBuffer(
|
|
65
|
-
let
|
|
86
|
+
let bufferId = createBuffer(cubeVertices(), { label: "cube-verts" })
|
|
87
|
+
let indexId = createBuffer(cubeIndices(), { label: "cube-indices" })
|
|
88
|
+
let id = createPipelineTexture(VERTEX, FRAGMENT, 1024, 1024, { uTime: 0 }, {
|
|
66
89
|
label: "cube",
|
|
67
90
|
attributes: [
|
|
68
91
|
{ name: "aPos", format: "vec3" },
|
|
69
92
|
{ name: "aColor", format: "vec3" },
|
|
70
93
|
],
|
|
71
94
|
buffer: bufferId,
|
|
95
|
+
indexBuffer: indexId,
|
|
96
|
+
indexFormat: "uint16",
|
|
72
97
|
depth: true,
|
|
98
|
+
cull: "back",
|
|
73
99
|
clearColor: [0.08, 0.08, 0.12, 1],
|
|
74
100
|
})
|
|
75
101
|
let [time, setTime] = createSignal(0)
|
|
76
102
|
onFrame((tick) => setTime(tick / 1000))
|
|
77
103
|
|
|
104
|
+
// Fill the window: the viewBox fits and centers the square content into
|
|
105
|
+
// the full-window view, so the projection is never stretched.
|
|
78
106
|
return (
|
|
79
|
-
<window
|
|
80
|
-
<
|
|
107
|
+
<window>
|
|
108
|
+
<view width={pct(100)} height={pct(100)} viewBox={[1024, 1024]}>
|
|
109
|
+
<texture src={id} params={{ uTime: time() }} width={1024} height={1024} />
|
|
110
|
+
</view>
|
|
81
111
|
</window>
|
|
82
112
|
)
|
|
83
113
|
}
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
// functions - read `capabilities.windowSizeClass` (no call). Reading it inside
|
|
9
9
|
// JSX tracks, so the memo below re-runs on every resize. `windowSize()` IS a
|
|
10
10
|
// function (call it) - we read its width to size each card exactly.
|
|
11
|
+
//
|
|
12
|
+
// This is the REFLOW answer, for layouts that genuinely rearrange across form
|
|
13
|
+
// factors. For content with fixed internal geometry (diagrams, slides,
|
|
14
|
+
// dashboards, game boards) do not branch on window size at all: author one
|
|
15
|
+
// design space and let `viewBox` scale it to fit - see view-viewbox.tsx.
|
|
11
16
|
import { render, capabilities, windowSize, createMemo, For } from "@solidrt/core"
|
|
12
17
|
|
|
13
18
|
const GAP = 16
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Source history on a boundary shader: previous: true retains the prior
|
|
2
|
+
// rasterization of the subtree as uPrevious, rotated when the content
|
|
3
|
+
// actually changes - not per frame. That makes it transition material: on
|
|
4
|
+
// each content change uPrevious holds the old look and uSource the new, and
|
|
5
|
+
// uMix sweeps a cross-dissolve between them. For a static panel uPrevious
|
|
6
|
+
// equals uSource (the previous rasterization IS the same content), so
|
|
7
|
+
// feedback/accumulation is not what this is for - that stays with manual
|
|
8
|
+
// targets.
|
|
9
|
+
//
|
|
10
|
+
// Click the panel to cycle its colors: the old palette dissolves into the
|
|
11
|
+
// new one instead of snapping.
|
|
12
|
+
import { render, onFrame, createSignal } from "@solidrt/core"
|
|
13
|
+
import { compileShader, destroyShader, glsl, linkProgram } from "@solidrt/core/gpu"
|
|
14
|
+
|
|
15
|
+
let VERTEX = glsl`
|
|
16
|
+
out vec2 vUV;
|
|
17
|
+
void main() {
|
|
18
|
+
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
19
|
+
vUV = p;
|
|
20
|
+
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
|
21
|
+
}
|
|
22
|
+
`
|
|
23
|
+
|
|
24
|
+
let DISSOLVE = glsl`
|
|
25
|
+
uniform sampler2D uSource;
|
|
26
|
+
uniform sampler2D uPrevious;
|
|
27
|
+
uniform float uMix;
|
|
28
|
+
in vec2 vUV;
|
|
29
|
+
void main() {
|
|
30
|
+
fragColor = mix(texture(uPrevious, vUV), texture(uSource, vUV), uMix);
|
|
31
|
+
}
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
const PALETTES = [
|
|
35
|
+
["#0077ff", "#ff6a00", "#00c46a"],
|
|
36
|
+
["#e63946", "#ffb703", "#8338ec"],
|
|
37
|
+
["#00b4d8", "#ef476f", "#ffd166"],
|
|
38
|
+
] as const
|
|
39
|
+
|
|
40
|
+
function App() {
|
|
41
|
+
let vs = compileShader("vertex", VERTEX, { header: true })
|
|
42
|
+
let fs = compileShader("fragment", DISSOLVE, { header: true })
|
|
43
|
+
let dissolve = linkProgram(vs, fs, { label: "dissolve" })
|
|
44
|
+
destroyShader(vs)
|
|
45
|
+
destroyShader(fs)
|
|
46
|
+
|
|
47
|
+
let [palette, setPalette] = createSignal(0)
|
|
48
|
+
let colors = () => PALETTES[palette() % PALETTES.length]!
|
|
49
|
+
let [mixv, setMix] = createSignal(1)
|
|
50
|
+
// Sweep uMix back to 1 after each change (~250ms). At 1 the signal stops
|
|
51
|
+
// changing, so the pass stops re-running.
|
|
52
|
+
onFrame((_tick, _frame, rate) => setMix(m => Math.min(1, m + 4 / rate)))
|
|
53
|
+
|
|
54
|
+
let cycle = () => {
|
|
55
|
+
setPalette(p => (p + 1) % PALETTES.length)
|
|
56
|
+
setMix(0)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<window alignItems="center" justifyContent="center">
|
|
61
|
+
<view
|
|
62
|
+
repaintBoundary="snapshot"
|
|
63
|
+
shader={{ program: dissolve, params: { uMix: mixv() }, previous: true }}
|
|
64
|
+
onPointerDown={cycle}
|
|
65
|
+
flexDirection="column"
|
|
66
|
+
gap={12}
|
|
67
|
+
alignItems="center"
|
|
68
|
+
justifyContent="center"
|
|
69
|
+
width={360}
|
|
70
|
+
height={240}
|
|
71
|
+
>
|
|
72
|
+
<rect position="absolute" width="100%" height="100%" radius={16} color="#dde3ec" />
|
|
73
|
+
<text fontSize={24} color="#222">Boundary history</text>
|
|
74
|
+
<view flexDirection="row" gap={12}>
|
|
75
|
+
<rect width={70} height={70} radius={12} color={colors()[0]} />
|
|
76
|
+
<rect width={70} height={70} radius={12} color={colors()[1]} />
|
|
77
|
+
<rect width={70} height={70} radius={12} color={colors()[2]} />
|
|
78
|
+
</view>
|
|
79
|
+
<text fontSize={13} color="#666">Click: colors cross-dissolve</text>
|
|
80
|
+
</view>
|
|
81
|
+
</window>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
render(() => <App />)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// A boundary shader: a view with repaintBoundary="snapshot" runs its
|
|
2
|
+
// rasterized subtree through a linked program and composites the result in
|
|
3
|
+
// its place. The program contract matches shader targets, not the window
|
|
4
|
+
// pass: uSource is the subtree's rasterization (top-left origin, so vUV
|
|
5
|
+
// needs no flip), iResolution is the boundary in physical pixels. The pass
|
|
6
|
+
// is split from content invalidation: animating params here re-runs only
|
|
7
|
+
// the pass - the panel's content is rasterized once and stays cached.
|
|
8
|
+
//
|
|
9
|
+
// outset adds a transparent margin the effect may write into: without it the
|
|
10
|
+
// wave clips at the layout box, with it the crests escape the box edge.
|
|
11
|
+
//
|
|
12
|
+
// Click the panel to toggle the warp between 0 and 1: at 0 the program is an
|
|
13
|
+
// identity pass, which must be indistinguishable from the plain snapshot.
|
|
14
|
+
import { render, onFrame, createSignal } from "@solidrt/core"
|
|
15
|
+
import { compileShader, destroyShader, glsl, linkProgram } from "@solidrt/core/gpu"
|
|
16
|
+
|
|
17
|
+
// { header: true } supplies #version and precision; the varyings are the
|
|
18
|
+
// program's own. Unlike the window pass there is no flip here: a target
|
|
19
|
+
// pass's vUV origin already matches the sampled texture's top-left.
|
|
20
|
+
let VERTEX = glsl`
|
|
21
|
+
out vec2 vUV;
|
|
22
|
+
void main() {
|
|
23
|
+
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
24
|
+
vUV = p;
|
|
25
|
+
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
|
26
|
+
}
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
let WARP = glsl`
|
|
30
|
+
uniform sampler2D uSource;
|
|
31
|
+
uniform float uAmount;
|
|
32
|
+
uniform float uTime;
|
|
33
|
+
in vec2 vUV;
|
|
34
|
+
void main() {
|
|
35
|
+
vec2 uv = vUV;
|
|
36
|
+
uv.x += sin(uv.y * 24.0 + uTime * 3.0) * 0.03 * uAmount;
|
|
37
|
+
uv.y += sin(uv.x * 18.0 - uTime * 2.0) * 0.03 * uAmount;
|
|
38
|
+
fragColor = texture(uSource, uv);
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
function App() {
|
|
43
|
+
let vs = compileShader("vertex", VERTEX, { header: true })
|
|
44
|
+
let fs = compileShader("fragment", WARP, { header: true })
|
|
45
|
+
let warp = linkProgram(vs, fs, { label: "panel-warp" })
|
|
46
|
+
destroyShader(vs)
|
|
47
|
+
destroyShader(fs)
|
|
48
|
+
|
|
49
|
+
let [time, setTime] = createSignal(0)
|
|
50
|
+
let [amount, setAmount] = createSignal(1)
|
|
51
|
+
onFrame(tick => setTime(tick / 1000))
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<window alignItems="center" justifyContent="center">
|
|
55
|
+
<view
|
|
56
|
+
repaintBoundary="snapshot"
|
|
57
|
+
shader={{ program: warp, params: { uTime: time(), uAmount: amount() }, outset: 16 }}
|
|
58
|
+
onPointerDown={() => setAmount(a => (a > 0 ? 0 : 1))}
|
|
59
|
+
flexDirection="column"
|
|
60
|
+
gap={12}
|
|
61
|
+
alignItems="center"
|
|
62
|
+
justifyContent="center"
|
|
63
|
+
width={360}
|
|
64
|
+
height={240}
|
|
65
|
+
>
|
|
66
|
+
{/* Fills the boundary, so the box edge is a content edge: the warp
|
|
67
|
+
visibly ripples it out into the outset margin. */}
|
|
68
|
+
<rect position="absolute" width="100%" height="100%" radius={16} color="#dde3ec" />
|
|
69
|
+
<text fontSize={24} color="#222">Boundary shader</text>
|
|
70
|
+
<view flexDirection="row" gap={12}>
|
|
71
|
+
<rect width={70} height={70} radius={12} color="#0077ff" />
|
|
72
|
+
<rect width={70} height={70} radius={12} color="#ff6a00" />
|
|
73
|
+
<rect width={70} height={70} radius={12} color="#00c46a" />
|
|
74
|
+
</view>
|
|
75
|
+
<text fontSize={13} color="#666">Click to toggle warp (identity at 0)</text>
|
|
76
|
+
</view>
|
|
77
|
+
</window>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
render(() => <App />)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// `viewBox` on a <view> is the fixed-aspect answer to "many screen sizes": you
|
|
2
|
+
// author the whole scene once, in your own made-up design units, and the view
|
|
3
|
+
// scales that space to fit its box. It is SVG's viewBox generalized to any
|
|
4
|
+
// subtree, not an SVG-only thing.
|
|
5
|
+
//
|
|
6
|
+
// Four facts, each demonstrated below:
|
|
7
|
+
// 1. It NEVER sizes the element. `viewBox` is a pure fit transform; the box
|
|
8
|
+
// still takes its size from layout (here `flex={1}`). A view with only a
|
|
9
|
+
// viewBox and no layout size is zero-sized and paints nothing.
|
|
10
|
+
// 2. It fits UNIFORMLY and centers - one scale for both axes, SVG's default
|
|
11
|
+
// preserveAspectRatio. Content never stretches; the leftover on the loose
|
|
12
|
+
// axis is letterbox, showing the window background through it.
|
|
13
|
+
// 3. Children live in DESIGN space. Their x/y, w/h, fontSize, stroke widths -
|
|
14
|
+
// everything resolves against the design size, not the box. The box a child
|
|
15
|
+
// inherits IS the design size, so a bare `d-rect` fills the design space
|
|
16
|
+
// and detached text wraps at its width.
|
|
17
|
+
// 4. Pointer coordinates arrive in design space too. localX/localY on the
|
|
18
|
+
// viewBox view (and on anything under it) read in design units, so no
|
|
19
|
+
// scale factor is threaded through the app's hit math.
|
|
20
|
+
//
|
|
21
|
+
// The payoff: no `windowSizeClass` branching, no per-breakpoint sizes, no
|
|
22
|
+
// scale factor anywhere. The same code runs unchanged from a desktop window to
|
|
23
|
+
// a phone. Reach for reflow (responsive-grid.tsx) only when the layout
|
|
24
|
+
// genuinely rearranges across form factors; for content with fixed internal
|
|
25
|
+
// geometry - diagrams, slides, dashboards, game boards, emulator screens - fit
|
|
26
|
+
// one design space instead.
|
|
27
|
+
//
|
|
28
|
+
// Resize the window and watch: the scene keeps its aspect ratio, everything
|
|
29
|
+
// including text scales together, and the readout reports the same design
|
|
30
|
+
// coordinates for the same spot on the scene at any window size.
|
|
31
|
+
import { render, createSignal, Show } from "@solidrt/core"
|
|
32
|
+
|
|
33
|
+
// The design space. Every number below is in these units - invent whatever
|
|
34
|
+
// suits the content and stay in it. 640x400 is 16:10.
|
|
35
|
+
const DESIGN_W = 640
|
|
36
|
+
const DESIGN_H = 400
|
|
37
|
+
|
|
38
|
+
function App() {
|
|
39
|
+
let [at, setAt] = createSignal<{ x: number; y: number } | null>(null)
|
|
40
|
+
let round = (v: number) => Math.round(v)
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
// The window background is what shows through the letterbox bars.
|
|
44
|
+
<window>
|
|
45
|
+
<d-rect color="#0b0f17" />
|
|
46
|
+
|
|
47
|
+
{/* flex={1} sizes the box; viewBox only maps content into it (fact 1). */}
|
|
48
|
+
<view
|
|
49
|
+
flex={1}
|
|
50
|
+
viewBox={[DESIGN_W, DESIGN_H]}
|
|
51
|
+
onPointerMove={(e) => setAt({ x: e.localX, y: e.localY })}
|
|
52
|
+
onPointerLeave={() => setAt(null)}
|
|
53
|
+
>
|
|
54
|
+
{/* No w/h, so it fills the box it inherits - which under a viewBox is
|
|
55
|
+
the design space (fact 3). Its edges are the letterbox edges. */}
|
|
56
|
+
<d-rect color="#151b28" />
|
|
57
|
+
|
|
58
|
+
{/* A scene at literal design coordinates. No breakpoints, no
|
|
59
|
+
windowSize() reads, no scale factor: authored once, at this size. */}
|
|
60
|
+
<d-rect x={40} y={40} w={240} h={140} radius={12} color="#1f6feb" />
|
|
61
|
+
<d-rect x={300} y={40} w={300} h={140} radius={12} color="#3fb950" />
|
|
62
|
+
<d-oval x={40} y={220} w={140} h={140} color="#a371f7" />
|
|
63
|
+
<d-line x1={200} y1={300} x2={600} y2={300} color="#e3b341" strokeWidth={6} />
|
|
64
|
+
<d-text x={64} y={95} fontSize={28} color="#e6e6e6">
|
|
65
|
+
fixed design space
|
|
66
|
+
</d-text>
|
|
67
|
+
<d-text x={324} y={95} fontSize={28} color="#0b0f17">
|
|
68
|
+
{DESIGN_W} x {DESIGN_H}
|
|
69
|
+
</d-text>
|
|
70
|
+
|
|
71
|
+
{/* Pointer position in design units (fact 4), drawn in design units.
|
|
72
|
+
Non-keyed Show keeps the marker node mounted across moves (a
|
|
73
|
+
ternary on at() would recreate it every event); the accessor
|
|
74
|
+
reads update its props fine-grained. */}
|
|
75
|
+
<d-text x={200} y={330} fontSize={22} color="#8b949e">
|
|
76
|
+
{at() ? `design x ${round(at()!.x)}, y ${round(at()!.y)}` : "move the pointer over the scene"}
|
|
77
|
+
</d-text>
|
|
78
|
+
<Show when={at()}>
|
|
79
|
+
{(a) => <d-oval x={a().x - 8} y={a().y - 8} w={16} h={16} color="#f85149" />}
|
|
80
|
+
</Show>
|
|
81
|
+
</view>
|
|
82
|
+
</window>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
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.45",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"colord": "^2.9.3"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@solidrt/flux-types": "0.0.
|
|
30
|
+
"@solidrt/flux-types": "0.0.45"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@solidjs/signals": "2.0.0-beta.26",
|
package/src/gpu.ts
CHANGED
|
@@ -71,7 +71,7 @@ export type { TextureFormat } from "flux:gpu"
|
|
|
71
71
|
// runtime, distinct types to the checker, so a cross-space slip like
|
|
72
72
|
// destroyBuffer(textureId) fails to compile. Exported so apps can annotate
|
|
73
73
|
// storage (`let ids: TextureId[]`).
|
|
74
|
-
export type { BufferId, ProgramId, RenderPipelineId, ShaderStageId, TextureId } from "flux:gpu"
|
|
74
|
+
export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, TextureId } from "flux:gpu"
|
|
75
75
|
|
|
76
76
|
// Re-exported so callers that depend on @solidrt/core -- like @solidrt/components
|
|
77
77
|
// -- need not import flux directly: destroyTexture for the manual-cleanup path
|
|
@@ -108,7 +108,20 @@ export {
|
|
|
108
108
|
// GPU-side (exact, same size): seed a loadOp "load" accumulator, snapshot a
|
|
109
109
|
// ping-pong buffer, reset state to a known image.
|
|
110
110
|
export { copyTexture, destroyBuffer, renderTarget, setDraw } from "flux:gpu"
|
|
111
|
-
export type { BlendMode, DrawRange, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
111
|
+
export type { BlendMode, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
112
|
+
|
|
113
|
+
// The draw-list verbs, re-exported raw: entries live and die with their draw
|
|
114
|
+
// target (see createDrawTarget below), so there is no per-entry lifetime to
|
|
115
|
+
// wrap. addDraw adds an entry (appended, or inserted via opts.before) and
|
|
116
|
+
// returns its stable DrawId; removeDraw drops one; setDrawParams /
|
|
117
|
+
// setDrawTextures / setDrawRange are the per-entry forms of setShaderParams /
|
|
118
|
+
// setShaderTextures / setDraw, taking (target, draw, value) with identical
|
|
119
|
+
// merge and validation semantics. The per-object hot path is setDrawParams (a
|
|
120
|
+
// moved mesh = one call with its new matrix). setDrawOrder replaces the whole
|
|
121
|
+
// list order with a full permutation of the live ids - the sorting verb
|
|
122
|
+
// (opaque front-to-back, transparent back-to-front, re-issued when the
|
|
123
|
+
// camera moves).
|
|
124
|
+
export { addDraw, removeDraw, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures } from "flux:gpu"
|
|
112
125
|
|
|
113
126
|
// The device ceilings (max texture/target size, sampler inputs per pass,
|
|
114
127
|
// vertex attributes per pipeline), queried once at startup. Creates and binds
|
|
@@ -269,15 +282,20 @@ export function createShaderTexture(
|
|
|
269
282
|
* uniforms with `<texture params>` or `setShaderParams`). Many targets may
|
|
270
283
|
* share one pipeline, and creating a target compiles nothing. The target
|
|
271
284
|
* brings the per-target half: size, the concrete vertex `buffer` the
|
|
272
|
-
* pipeline's attribute layout describes, the
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
285
|
+
* pipeline's attribute layout describes, the `instanceBuffer` its
|
|
286
|
+
* `instanceAttributes` describe (required exactly when it declares any),
|
|
287
|
+
* the draw range (`vertexCount` defaults to the rest of the buffer from
|
|
288
|
+
* `firstVertex` on, `instanceCount` repeats it as instances told apart by
|
|
289
|
+
* `gl_InstanceID` and defaults to one per instance-buffer record; a
|
|
290
|
+
* fullscreen pass over an attributeless pipeline is `{ vertexCount: 3 }`
|
|
291
|
+
* with a covering-triangle vertex stage), uniforms, and
|
|
292
|
+
* `clearColor`. An `indexBuffer` + `indexFormat` pair makes the draw indexed
|
|
293
|
+
* (shared vertices stored once), with the range in `firstIndex`/`indexCount`
|
|
294
|
+
* spelling - see IndexBinding/IndexRange. Draw state (`attributes`,
|
|
295
|
+
* `instanceAttributes`, `topology`, `blend`, `cull`, `depth`, `depthWrite`)
|
|
296
|
+
* lives on the pipeline
|
|
297
|
+
* and throws here. Frees the target when the reactive owner is disposed (opt
|
|
298
|
+
* out with `opts.manual`); the pipeline is yours and outlives it.
|
|
281
299
|
*
|
|
282
300
|
* `render: "manual"` makes it a manual target: the runtime never renders it
|
|
283
301
|
* (it starts cleared to `clearColor`), only an explicit `renderTarget(id)`
|
|
@@ -297,10 +315,11 @@ export function createShaderTarget(
|
|
|
297
315
|
opts?: {
|
|
298
316
|
textures?: Record<string, gpu.TextureId>
|
|
299
317
|
buffer?: gpu.BufferId
|
|
318
|
+
instanceBuffer?: gpu.BufferId
|
|
300
319
|
clearColor?: [number, number, number, number]
|
|
301
320
|
render?: "auto" | "manual"
|
|
302
321
|
loadOp?: "clear" | "load"
|
|
303
|
-
} & gpu.DrawRange &
|
|
322
|
+
} & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
|
|
304
323
|
CreateOptions &
|
|
305
324
|
SamplerOptions,
|
|
306
325
|
): gpu.TextureId {
|
|
@@ -309,6 +328,42 @@ export function createShaderTarget(
|
|
|
309
328
|
return id
|
|
310
329
|
}
|
|
311
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Creates a draw target: a render target holding an ordered, MUTABLE list of
|
|
333
|
+
* draws, rendered as one pass - clear once (color, and depth when declared),
|
|
334
|
+
* then every entry in list order into the same storage. This is the
|
|
335
|
+
* multi-pass primitive (N meshes x N pipelines sharing one depth buffer -
|
|
336
|
+
* what every 3D API calls a render pass), retained: build the list with
|
|
337
|
+
* `addDraw`, prune it with `removeDraw`, and drive per-entry state with
|
|
338
|
+
* `setDrawParams` / `setDrawTextures` / `setDrawRange`. `depth: true` gives
|
|
339
|
+
* the target the depth storage all entries share (cross-entry occlusion);
|
|
340
|
+
* whether an entry tests/writes it stays pipeline state, and a depth-testing
|
|
341
|
+
* pipeline into a depthless target throws at `addDraw`.
|
|
342
|
+
*
|
|
343
|
+
* The render contract is unchanged: the list is input data, so an ordinary
|
|
344
|
+
* (`render: "auto"`) draw target re-renders exactly when its entries or
|
|
345
|
+
* their inputs change - a static scene costs zero passes, and one render is
|
|
346
|
+
* one pass regardless of entry count. `render: "manual"` and `loadOp` work
|
|
347
|
+
* as on `createShaderTarget`. Returns the texture id; frees on owner
|
|
348
|
+
* disposal (opt out with `opts.manual`), taking its entries with it - the
|
|
349
|
+
* entries' pipelines and buffers are yours and outlive it.
|
|
350
|
+
*/
|
|
351
|
+
export function createDrawTarget(
|
|
352
|
+
width: number,
|
|
353
|
+
height: number,
|
|
354
|
+
opts?: {
|
|
355
|
+
depth?: boolean
|
|
356
|
+
clearColor?: [number, number, number, number]
|
|
357
|
+
render?: "auto" | "manual"
|
|
358
|
+
loadOp?: "clear" | "load"
|
|
359
|
+
} & CreateOptions &
|
|
360
|
+
SamplerOptions,
|
|
361
|
+
): gpu.TextureId {
|
|
362
|
+
let id = gpu.createDrawTarget(width, height, opts)
|
|
363
|
+
if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
364
|
+
return id
|
|
365
|
+
}
|
|
366
|
+
|
|
312
367
|
/** The reactive shader description `createShaderTextureMemo` builds from.
|
|
313
368
|
* Sampling (`filter`/`wrap`) is creation-time state, so changing it rebuilds
|
|
314
369
|
* at a fresh id, like a fragment-source or sampler-binding change. */
|
|
@@ -439,7 +494,14 @@ function toUint8(data: ArrayBuffer | ArrayBufferView): Uint8Array {
|
|
|
439
494
|
* see DrawRange) defaults to the whole buffer drawn once and can be changed
|
|
440
495
|
* later with `setDraw`; `instanceCount` is the standard answer to particles
|
|
441
496
|
* and repeated meshes, N copies of the range told apart by `gl_InstanceID`
|
|
442
|
-
* in the vertex stage. `opts.
|
|
497
|
+
* in the vertex stage. `opts.instanceAttributes` + `opts.instanceBuffer`
|
|
498
|
+
* (declare both or neither) give each instance its own interleaved record -
|
|
499
|
+
* real per-instance state instead of `gl_InstanceID` arithmetic - and
|
|
500
|
+
* `instanceCount` then defaults to one instance per record. An
|
|
501
|
+
* `indexBuffer` + `indexFormat` pair makes the draw
|
|
502
|
+
* indexed (shared vertices stored once), with the range in
|
|
503
|
+
* `firstIndex`/`indexCount` spelling; `opts.cull` discards one face set by
|
|
504
|
+
* winding (counter-clockwise as displayed = front). `opts.render: "manual"` and
|
|
443
505
|
* `opts.loadOp` behave exactly as on {@link createShaderTarget}: step the
|
|
444
506
|
* target with `renderTarget(id)`, and `loadOp: "load"` (manual-only) keeps
|
|
445
507
|
* the previous contents under each draw. Frees the texture and GL program when the reactive
|
|
@@ -456,14 +518,17 @@ export function createPipelineTexture(
|
|
|
456
518
|
textures?: Record<string, gpu.TextureId>
|
|
457
519
|
attributes?: gpu.VertexAttribute[]
|
|
458
520
|
buffer?: gpu.BufferId
|
|
521
|
+
instanceAttributes?: gpu.VertexAttribute[]
|
|
522
|
+
instanceBuffer?: gpu.BufferId
|
|
459
523
|
topology?: gpu.Topology
|
|
460
524
|
depth?: boolean
|
|
461
525
|
depthWrite?: boolean
|
|
462
526
|
blend?: gpu.BlendMode
|
|
527
|
+
cull?: gpu.CullMode
|
|
463
528
|
clearColor?: [number, number, number, number]
|
|
464
529
|
render?: "auto" | "manual"
|
|
465
530
|
loadOp?: "clear" | "load"
|
|
466
|
-
} & gpu.DrawRange &
|
|
531
|
+
} & (gpu.DrawRange | (gpu.IndexBinding & gpu.IndexRange)) &
|
|
467
532
|
CreateOptions &
|
|
468
533
|
SamplerOptions,
|
|
469
534
|
): gpu.TextureId {
|
package/src/types.d.ts
CHANGED
|
@@ -447,6 +447,67 @@ export interface ViewOwnProps extends TransformProps, PointerProps {
|
|
|
447
447
|
* axis-aligned rects look identical, so prefer it for plain UI panels.
|
|
448
448
|
*/
|
|
449
449
|
repaintBoundary?: boolean | "snapshot" | "snapshot-no-aa"
|
|
450
|
+
/**
|
|
451
|
+
* Run this view's rasterized subtree through a GPU program and composite
|
|
452
|
+
* the result in its place. Requires repaintBoundary="snapshot" (the cost
|
|
453
|
+
* is snapshot semantics, kept explicit; declared without it the shader is
|
|
454
|
+
* ignored with a warning). The pass is region-sized and split from content
|
|
455
|
+
* invalidation: a params-only change re-runs just the pass against the
|
|
456
|
+
* cached snapshot, so animating an effect over a static subtree never
|
|
457
|
+
* re-rasterizes it.
|
|
458
|
+
*
|
|
459
|
+
* The effect samples only the subtree's own pixels - grading, warping or
|
|
460
|
+
* dissolving the panel works; anything needing what is behind it does not.
|
|
461
|
+
* Sampling outside the content clamps to the edge, and the output is
|
|
462
|
+
* cropped to the layout box like any snapshot. Hit-testing stays on layout
|
|
463
|
+
* geometry: a distortion moves pixels, not hit targets. The view's own
|
|
464
|
+
* transform and opacity apply after the effect, so the program sees
|
|
465
|
+
* unrotated, opaque content.
|
|
466
|
+
*/
|
|
467
|
+
shader?: ViewShaderProps | null
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* A boundary shader declaration. The program contract matches shader targets,
|
|
472
|
+
* not the window pass: the subtree's rasterization binds as
|
|
473
|
+
* `uniform sampler2D uSource` (top-left origin, like every sampled texture)
|
|
474
|
+
* and the pass draws the covering triangle attributeless. `iResolution`,
|
|
475
|
+
* filled by name, is the boundary in physical pixels.
|
|
476
|
+
*/
|
|
477
|
+
export interface ViewShaderProps {
|
|
478
|
+
/** Linked program handle from linkProgram. */
|
|
479
|
+
program: ProgramId
|
|
480
|
+
/**
|
|
481
|
+
* Uniforms filled by name, paced to the next real repaint. A number drives
|
|
482
|
+
* a scalar (`float`/`int`); a flat number array drives the declared GLSL
|
|
483
|
+
* type: 2/3/4 for `vec2`/`vec3`/`vec4`, 16 (column-major) for `mat4`.
|
|
484
|
+
*/
|
|
485
|
+
params?: Record<string, number | number[]>
|
|
486
|
+
/** Extra sampler2D inputs: uniform name to texture id. */
|
|
487
|
+
textures?: Record<string, TextureId>
|
|
488
|
+
/**
|
|
489
|
+
* Transparent margin in logical px on every side of the layout box, for
|
|
490
|
+
* the effect to write into - glow, drop shadow, blur that bleeds past the
|
|
491
|
+
* edge. Grows the rasterized canvas and the composited quad; the subtree's
|
|
492
|
+
* own paint stays clipped to the layout box either way. The pass sees only
|
|
493
|
+
* the bigger iResolution - declare an app uniform if the program needs the
|
|
494
|
+
* margin size. Default 0.
|
|
495
|
+
*/
|
|
496
|
+
outset?: number
|
|
497
|
+
/**
|
|
498
|
+
* Retain the prior rasterization of the subtree as
|
|
499
|
+
* `uniform sampler2D uPrevious`. Source history, not output history: it
|
|
500
|
+
* rotates when the content actually re-rasterizes, not per frame - on a
|
|
501
|
+
* content change uPrevious holds exactly the old look (transition
|
|
502
|
+
* material: cross-dissolve old into new), while for a static subtree with
|
|
503
|
+
* animated params uPrevious equals uSource. Feedback/accumulation is not
|
|
504
|
+
* this; that stays with manual targets. Costs one extra canvas-sized
|
|
505
|
+
* texture while declared; transparent until the first rotation, and reset
|
|
506
|
+
* to transparent by a size or scale change. Only declare the uPrevious
|
|
507
|
+
* uniform together with this flag - without it the uniform stays at unit 0
|
|
508
|
+
* and aliases uSource. Default false.
|
|
509
|
+
*/
|
|
510
|
+
previous?: boolean
|
|
450
511
|
}
|
|
451
512
|
|
|
452
513
|
export interface ViewProps extends ViewOwnProps, LayoutProps {}
|