@solidrt/core 0.0.44 → 0.0.46
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 +1 -1
- package/examples/README.md +4 -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/gpu-shared-params.tsx +135 -0
- 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 +5 -5
- package/src/gpu.ts +151 -59
- package/src/image.ts +8 -20
- package/src/index.ts +1 -1
- package/src/types.d.ts +61 -4
package/AGENTS.md
CHANGED
|
@@ -94,7 +94,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
|
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
97
|
-
2.0.0-beta.
|
|
97
|
+
2.0.0-beta.31); bun resolves them from peerDependencies.
|
|
98
98
|
|
|
99
99
|
## Element model (the parts that are easy to get wrong)
|
|
100
100
|
|
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.
|
|
@@ -41,6 +42,8 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
41
42
|
- `gpu-pipeline.tsx` - `createPipelineTexture`: a custom vertex+fragment pair drawing an interleaved vertex buffer (position and color attributes resolved by name), a depth-tested spinning cube whose `uTime` uniform is driven through `<texture params>` exactly like a fragment shader's.
|
|
42
43
|
- `gpu-particles.tsx` - an additive particle field: `topology: "points"` with `blend: "add"`, so overlapping gaussian splats (`gl_PointSize` from the vertex stage, `gl_PointCoord` falloff, premultiplied output) accumulate into a glowing volume instead of overwriting. The tints are typed vec3 uniforms filled from 3-number array params.
|
|
43
44
|
- `gpu-instancing.tsx` - instanced drawing: one 3-vertex triangle drawn hundreds of times via `instanceCount`, each instance placed and tinted on a phyllotaxis spiral from `gl_InstanceID` alone. `setDraw(id, { instanceCount })` merges into the draw range per frame (absent keys keep their values, like params), so the population breathes without touching the buffer.
|
|
45
|
+
- `gpu-draw-list.tsx` - `createDrawTarget`: one render target holding an ordered, mutable LIST of draws - two orbiting triangles from different programs sharing one depth buffer (the target owns the depth storage, each pipeline the test/write behavior), a third entry added and removed live via its stable `DrawId`, and `setDrawParams` as the per-object channel.
|
|
46
|
+
- `gpu-shared-params.tsx` - shared target state on a draw target: `setTargetParams`/`setTargetTextures` write values every entry reads - a ring of quads spins and color-cycles from ONE write per frame, and the shared sampler source swaps for the whole target at once. The spin is imperative (`setTargetParams` in `onFrame`) while the tint rides the `<texture params>` prop - the declarative channel, which on a draw target writes the same shared record. The two entries that seeded their own uTint/uMap show the precedence rule (an entry's own value beats the shared one), and mixed programs show partial coverage (a program that does not declare a shared name skips it). `createDrawTarget`'s positional `params` and `opts.textures` seed both channels before any entry exists.
|
|
44
47
|
- `window-shader.tsx` - the `shader` prop on `<window>`: the finished frame drawn through a raw-linked warp program before present, click to toggle between warp and identity.
|
|
45
48
|
- `window-shader-history.tsx` - the window shader's frame history: `previous` binds last frame as uPrevious, drawn as a one-frame motion echo behind an orbiting square; click toggles the echo term.
|
|
46
49
|
|
|
@@ -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, null, { 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
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Shared target state on a draw target: values every entry reads, written
|
|
2
|
+
// ONCE per target instead of once per entry. A ring of quads spins under a
|
|
3
|
+
// single "camera" uniform (uView) and color-cycles under a shared tint
|
|
4
|
+
// (uTint) - one target-level write per frame however many entries the
|
|
5
|
+
// list holds, where per-entry setDrawParams would cost one call and one
|
|
6
|
+
// value's worth of JS arithmetic per quad (the cost profile @solidrt/3d's
|
|
7
|
+
// camera rides on). setTargetTextures is the sampler analog: the patterned
|
|
8
|
+
// quads read one shared uMap source, swapped for the whole target every
|
|
9
|
+
// two seconds.
|
|
10
|
+
//
|
|
11
|
+
// The rules to notice: an entry's OWN value beats the shared one - the
|
|
12
|
+
// amber quad seeds uTint at addDraw and ignores the color cycle, the
|
|
13
|
+
// striped quad binds its own uMap and ignores the swap - and coverage may
|
|
14
|
+
// be partial: the tint program never declares uMap and the patterned
|
|
15
|
+
// program never declares uTint, so each shared write simply skips entries
|
|
16
|
+
// whose program does not declare the name. Shared state is target state:
|
|
17
|
+
// createDrawTarget seeds it (positional params + opts.textures) before any
|
|
18
|
+
// entry exists, and entry add/remove/rebuild cannot lose it.
|
|
19
|
+
//
|
|
20
|
+
// Two channels drive the same shared params. uView goes imperatively
|
|
21
|
+
// (setTargetParams in onFrame), uTint goes declaratively: the `<texture
|
|
22
|
+
// params>` prop means "the target's params" on every target kind, so on a
|
|
23
|
+
// draw target it writes the shared record - a signal into the prop is all
|
|
24
|
+
// the wiring a shared value needs.
|
|
25
|
+
import { render, onFrame, createSignal } from "@solidrt/core"
|
|
26
|
+
import {
|
|
27
|
+
addDraw,
|
|
28
|
+
compileShader,
|
|
29
|
+
createBuffer,
|
|
30
|
+
createDrawTarget,
|
|
31
|
+
createRenderPipeline,
|
|
32
|
+
createTexture,
|
|
33
|
+
glsl,
|
|
34
|
+
linkProgram,
|
|
35
|
+
setTargetParams,
|
|
36
|
+
setTargetTextures,
|
|
37
|
+
} from "@solidrt/core/gpu"
|
|
38
|
+
|
|
39
|
+
let VERTEX = glsl`
|
|
40
|
+
in vec2 aPos;
|
|
41
|
+
uniform float uView;
|
|
42
|
+
uniform vec2 uCenter;
|
|
43
|
+
|
|
44
|
+
void main() {
|
|
45
|
+
vec2 p = uCenter + aPos * 0.13;
|
|
46
|
+
float c = cos(uView), s = sin(uView);
|
|
47
|
+
gl_Position = vec4(c * p.x - s * p.y, s * p.x + c * p.y, 0.0, 1.0);
|
|
48
|
+
}
|
|
49
|
+
`
|
|
50
|
+
|
|
51
|
+
let FRAGMENT_TINT = glsl`
|
|
52
|
+
uniform vec4 uTint;
|
|
53
|
+
void main() {
|
|
54
|
+
fragColor = uTint;
|
|
55
|
+
}
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
let FRAGMENT_MAP = glsl`
|
|
59
|
+
uniform sampler2D uMap;
|
|
60
|
+
void main() {
|
|
61
|
+
fragColor = texture(uMap, gl_FragCoord.xy / 32.0);
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
function App() {
|
|
66
|
+
let quad = createBuffer(new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]), { label: "quad" })
|
|
67
|
+
let vs = compileShader("vertex", VERTEX, { header: true })
|
|
68
|
+
let attrs = [{ name: "aPos", format: "vec2" as const }]
|
|
69
|
+
let tint = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_TINT, { header: true })), {
|
|
70
|
+
attributes: attrs,
|
|
71
|
+
})
|
|
72
|
+
let mapped = createRenderPipeline(linkProgram(vs, compileShader("fragment", FRAGMENT_MAP, { header: true })), {
|
|
73
|
+
attributes: attrs,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// 2x2 patterns, nearest + repeat, so gl_FragCoord tiling shows hard cells.
|
|
77
|
+
let pattern = (a: number[], b: number[]) =>
|
|
78
|
+
createTexture(new Uint8Array([...a, ...b, ...b, ...a]), 2, 2, { filter: "nearest", wrap: "repeat" })
|
|
79
|
+
let checker = pattern([15, 15, 20, 255], [235, 235, 235, 255])
|
|
80
|
+
let ember = pattern([250, 160, 30, 255], [45, 10, 60, 255])
|
|
81
|
+
let stripes = createTexture(new Uint8Array([220, 40, 60, 255, 245, 245, 245, 255]), 2, 1, {
|
|
82
|
+
filter: "nearest",
|
|
83
|
+
wrap: "repeat",
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// The positional params argument and opts.textures seed the shared state
|
|
87
|
+
// before any entry exists; the entries added below pick it up.
|
|
88
|
+
let target = createDrawTarget(
|
|
89
|
+
512,
|
|
90
|
+
512,
|
|
91
|
+
{ uView: 0, uTint: [0.3, 0.8, 0.9, 1] },
|
|
92
|
+
{ textures: { uMap: checker }, clearColor: [0.05, 0.05, 0.09, 1], label: "shared-ring" },
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const RING = 8
|
|
96
|
+
for (let i = 0; i < RING; i++) {
|
|
97
|
+
let a = (i / RING) * Math.PI * 2
|
|
98
|
+
let uCenter = [0.62 * Math.cos(a), 0.62 * Math.sin(a)]
|
|
99
|
+
if (i === 1 || i === 5) {
|
|
100
|
+
// Patterned entries read the shared uMap; the one at i === 5 brings
|
|
101
|
+
// its own binding and keeps its stripes through every shared swap.
|
|
102
|
+
addDraw(target, mapped, { uCenter }, i === 5 ? { buffer: quad, textures: { uMap: stripes } } : { buffer: quad })
|
|
103
|
+
} else if (i === 3) {
|
|
104
|
+
// The override quad: its own uTint beats the shared color cycle.
|
|
105
|
+
addDraw(target, tint, { uCenter, uTint: [1, 0.62, 0.1, 1] }, { buffer: quad })
|
|
106
|
+
} else {
|
|
107
|
+
addDraw(target, tint, { uCenter }, { buffer: quad })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let [sharedTint, setSharedTint] = createSignal([0.3, 0.8, 0.9, 1])
|
|
112
|
+
let mapFlip = -1
|
|
113
|
+
onFrame(tick => {
|
|
114
|
+
let t = tick / 1000
|
|
115
|
+
// The whole ring, one write: uView spins every entry. uTint takes the
|
|
116
|
+
// declarative channel instead - the signal feeds the params prop below.
|
|
117
|
+
setTargetParams(target, { uView: t * 0.5 })
|
|
118
|
+
setSharedTint([0.45 + 0.45 * Math.sin(t), 0.45 + 0.45 * Math.sin(t + 2.1), 0.45 + 0.45 * Math.sin(t + 4.2), 1])
|
|
119
|
+
// The shared sampler source swaps every two seconds; only the entry
|
|
120
|
+
// with its own uMap keeps its pattern.
|
|
121
|
+
let flip = Math.floor(t / 2) % 2
|
|
122
|
+
if (flip !== mapFlip) {
|
|
123
|
+
mapFlip = flip
|
|
124
|
+
setTargetTextures(target, { uMap: flip === 0 ? checker : ember })
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<window alignItems="center" justifyContent="center">
|
|
130
|
+
<texture src={target} width={420} height={420} params={{ uTint: sharedTint() }} />
|
|
131
|
+
</window>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
render(() => <App />)
|
|
@@ -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 />)
|