@solidrt/core 0.0.39 → 0.0.41

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 CHANGED
@@ -55,8 +55,9 @@ you need the raw fact (e.g. `env.displayScale` for asset sizing below).
55
55
  Because the drawn size is fluid and the display DPI varies, asset format is a
56
56
  real design decision, not an afterthought:
57
57
 
58
- - Prefer VECTORS (`<svg>`, `<d-path>`) whenever the render size is fluid or DPI
59
- varies - they stay crisp at any size x `displayScale()`.
58
+ - Prefer VECTORS (`parseSvg` draws mapped to `<d-path>` in a `viewBox` view)
59
+ whenever the render size is fluid or DPI varies - they stay crisp at any
60
+ size x `displayScale()`.
60
61
  - RASTER (`<texture>` / `createImage`) needs source resolution >= displayed size
61
62
  x `env.displayScale`, or it softens on hi-DPI. Author raster at 2-3x the
62
63
  largest size you will ever draw it. A 256px PNG blown up large on a hi-DPI
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A low-level toolkit for creating cross-platform applications.
4
4
 
5
- _SolidRT is in pre-alpha stage. Anything can and will be changed._
5
+ _SolidRT is in alpha: useful today, but APIs are still stabilizing._
6
6
 
7
7
  > LLM agents: see [AGENTS.md](./AGENTS.md) for a dense, self-contained quickstart.
8
8
 
@@ -35,8 +35,12 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
35
35
  ## Images and GPU
36
36
  - `image.tsx` - `createImage` (async value: fetch + decode + upload) read inside a `<Loading>` boundary and shown with `<texture>`.
37
37
  - `inline-image.tsx` - bytes already in memory: `decodeImage` + `createTexture` (both synchronous) show an image with no `<Loading>` boundary. The sync counterpart to `image.tsx`.
38
- - `gpu-shader.tsx` - a GLSL fragment shader rendered to a texture, animated by driving its `iTime` uniform declaratively through the `<texture params={{...}}>` prop.
39
- - `gpu-raw-program.tsx` - the raw shading layer: compileShader/linkProgram/createShaderTarget, one vertex stage shared by two programs, with and without the standard header.
38
+ - `gpu-shader.tsx` - a GLSL fragment shader rendered to a texture via `createShaderTexture`, animated by driving its own `uTime` uniform declaratively through the `<texture params={{...}}>` prop. Shows both source dialects side by side: without a `#version` line the runtime injects the `vUV`/`iResolution`/`fragColor` preamble (exactly what the runtime provides - app-driven uniforms are the source's own declarations), while a source starting with `#version 300 es` is compiled exactly as written and names its own uniforms - which is what lets a shader written elsewhere run unchanged.
39
+ - `gpu-texture-blend.tsx` - compositing two shader targets as stacked `<texture>` layers with `blendMode` ("plus" additive over a base pass), the alternative to a third shader that samples both. Click toggles against `"source-over"` to show why. The tree-level counterpart to blending within one draw (`gpu-particles.tsx`); without `blend: "add"` a target's own draw runs with GL blending disabled.
40
+ - `gpu-raw-program.tsx` - the raw shading layer: compileShader/linkProgram/createRenderPipeline/createShaderTarget, one vertex stage shared by two programs, with and without the standard header.
41
+ - `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
+ - `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
+ - `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.
40
44
  - `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.
41
45
  - `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.
42
46
 
@@ -44,7 +48,8 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
44
48
  - `sound.tsx` - `createSound`: decode a clip once from bytes (here a binary import), replay cheaply; `overlap` stacking vs single-voice, `playing()` signal, release on unmount. Points to `createSoundStream` for long tracks streamed from a path.
45
49
 
46
50
  ## Vector graphics
47
- - `svg.tsx` - `<svg src={...}>` draws a whole SVG *document string* (not HTML/JSX children); multi-color fills vs a `currentColor` icon recolored by the `color` prop. This is how to use existing icon libraries (Lucide, Heroicons, etc.) - hand their SVG source to `src`.
51
+ - `parse-svg.tsx` - `parseSvg` turns a whole SVG *document string* (not HTML/JSX children) into plain draw data mapped to `<d-path>` inside a `viewBox`-fitted view; per-shape hover highlighting shows the payoff (exact-outline hit testing, recolor without re-parse), plus a `currentColor` icon recolored via the `color` option. This is how to use existing icon libraries (Lucide, Heroicons, etc.) - hand their SVG source to `parseSvg`.
48
52
 
49
53
  ## Bundling assets
50
- - `binary-import.tsx` - `import bytes from "./file" with { type: "binary" }` inlines a file's bytes into the bundle as a `Uint8Array` (the bytes are in memory, so `inline-image.tsx` displays them with the synchronous `decodeImage` + `createTexture` path). `with { type: "text" }` works the same way for a string.
54
+ - `binary-import.tsx` - `import bytes from "./file" with { type: "binary" }` inlines a file's bytes into the bundle as a `Uint8Array` (the bytes are in memory, so `inline-image.tsx` displays them with the synchronous `decodeImage` + `createTexture` path).
55
+ - `text-import.tsx` - `with { type: "text" }`, the string counterpart: inlines a `.glsl` shader source (`wave.glsl`) into the bundle, available synchronously with no runtime read. Works on any extension; `.svg` needs no attribute and `.glsl`/`.vert`/`.frag` are declared as text modules already.
@@ -0,0 +1,70 @@
1
+ // Instanced drawing: one 3-vertex triangle in the buffer, drawn hundreds of
2
+ // times with instanceCount (glDrawArraysInstanced, native ES 3.0). The vertex
3
+ // stage tells copies apart by gl_InstanceID - here each instance becomes one
4
+ // petal on a phyllotaxis spiral, placed and tinted from its index alone, so
5
+ // the geometry buffer stays 6 floats no matter the population.
6
+ //
7
+ // The draw range is data: setDraw merges partial updates like params, so the
8
+ // per-frame write below changes only instanceCount while firstVertex and
9
+ // vertexCount keep their values (the whole buffer). instanceCount 0 would
10
+ // draw nothing - the population legitimately breathes down to a single
11
+ // petal and could go dark the same way. gl_InstanceID always counts from 0
12
+ // (ES 3.0 has no base instance).
13
+ import { render, onFrame, createSignal } from "@solidrt/core"
14
+ import { createBuffer, createPipelineTexture, glsl, setDraw } from "@solidrt/core/gpu"
15
+
16
+ let MAX_PETALS = 324
17
+
18
+ let VERTEX = glsl`
19
+ in vec2 aPos;
20
+ out vec3 vTint;
21
+ uniform float uTime;
22
+
23
+ void main() {
24
+ // One instance = one petal: radius grows with sqrt(index) and the angle
25
+ // steps by the golden angle (~2.39996 rad) - the sunflower layout.
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
+ float c = cos(angle), s = sin(angle);
32
+ vec2 p = radius * vec2(c, s) + mat2(c, s, -s, c) * (aPos * scale);
33
+ gl_Position = vec4(p, 0.0, 1.0);
34
+ vTint = 0.5 + 0.5 * cos(6.2832 * (i / 96.0) + vec3(0.0, 2.1, 4.2));
35
+ }
36
+ `
37
+
38
+ let FRAGMENT = glsl`
39
+ in vec3 vTint;
40
+
41
+ void main() {
42
+ fragColor = vec4(vTint, 1.0);
43
+ }
44
+ `
45
+
46
+ function App() {
47
+ // The whole mesh: one triangle, reused by every instance.
48
+ let bufferId = createBuffer(new Float32Array([0, 1.3, -1, -0.75, 1, -0.75]), { label: "petal-tri" })
49
+ let id = createPipelineTexture(VERTEX, FRAGMENT, 512, 512, { uTime: 0 }, {
50
+ label: "petals",
51
+ attributes: [{ name: "aPos", format: "vec2" }],
52
+ buffer: bufferId,
53
+ instanceCount: 1,
54
+ clearColor: [0.03, 0.03, 0.06, 1],
55
+ })
56
+ let [time, setTime] = createSignal(0)
57
+ onFrame((tick) => {
58
+ let t = tick / 1000
59
+ setTime(t)
60
+ setDraw(id, { instanceCount: Math.round(MAX_PETALS / 2 + (MAX_PETALS / 2 - 1) * Math.sin(t * 0.7)) })
61
+ })
62
+
63
+ return (
64
+ <window alignItems="center" justifyContent="center">
65
+ <texture src={id} params={{ uTime: time() }} width={420} height={420} />
66
+ </window>
67
+ )
68
+ }
69
+
70
+ render(() => <App />)
@@ -0,0 +1,96 @@
1
+ // An additive particle field: createPipelineTexture with topology "points" and
2
+ // blend "add". Each vertex is one particle; the vertex stage sets
3
+ // gl_PointSize (honored across 4..64px) and the fragment stage shapes the
4
+ // splat from gl_PointCoord. With blend: "add" overlapping splats accumulate
5
+ // (glBlendFunc(ONE, ONE)) - order-independent, so the buffer needs no
6
+ // sorting - which is what turns discrete discs into a smooth glowing field.
7
+ // Without it a target's draw overwrites, and a point cloud can only thicken
8
+ // into scaly overlap.
9
+ //
10
+ // Additive output is premultiplied by construction: write vec4(color * a, a)
11
+ // and the target stays composite-correct in the tree. No depth buffer here -
12
+ // nothing occludes anything in a pure additive pass. A scene where opaque
13
+ // geometry should occlude the particles would add depth: true and pair the
14
+ // blended draw with depthWrite: false, explicitly - neither option implies
15
+ // the other.
16
+ //
17
+ // The tints are typed (vec3) uniforms driven from 3-number array params.
18
+ import { render, onFrame, createSignal } from "@solidrt/core"
19
+ import { createBuffer, createPipelineTexture, glsl } from "@solidrt/core/gpu"
20
+
21
+ let VERTEX = glsl`
22
+ in vec3 aPos;
23
+ in float aSeed;
24
+ out float vSeed;
25
+ uniform float uTime;
26
+
27
+ void main() {
28
+ float cy = cos(uTime * 0.4), sy = sin(uTime * 0.4);
29
+ vec3 p = vec3(cy * aPos.x - sy * aPos.z, aPos.y, sy * aPos.x + cy * aPos.z);
30
+ // Each particle breathes on its own phase.
31
+ p *= 1.0 + 0.15 * sin(uTime * 1.7 + aSeed * 40.0);
32
+ p.z += 2.2;
33
+
34
+ // Same perspective mapping as gpu-pipeline.tsx (near 1, far 10), clip y
35
+ // negated so camera-up displays up.
36
+ float f = 2.0;
37
+ gl_Position = vec4(p.x * f, -p.y * f, p.z * (11.0 / 9.0) - 20.0 / 9.0, p.z);
38
+ gl_PointSize = mix(10.0, 26.0, aSeed) / p.z;
39
+ vSeed = aSeed;
40
+ }
41
+ `
42
+
43
+ let FRAGMENT = glsl`
44
+ in float vSeed;
45
+ uniform vec3 uTintA;
46
+ uniform vec3 uTintB;
47
+
48
+ void main() {
49
+ // Soft gaussian falloff over the point sprite; gl_PointCoord is 0..1
50
+ // across the splat.
51
+ vec2 d = gl_PointCoord - 0.5;
52
+ float a = exp(-dot(d, d) * 14.0) * 0.35;
53
+ vec3 tint = mix(uTintA, uTintB, vSeed);
54
+ fragColor = vec4(tint * a, a);
55
+ }
56
+ `
57
+
58
+ // Interleaved [pos vec3, seed f32]: points on a fibonacci sphere, so the
59
+ // field reads as a volume from every angle.
60
+ function particles(count: number): Float32Array {
61
+ let verts: number[] = []
62
+ let golden = Math.PI * (3.0 - Math.sqrt(5.0))
63
+ for (let i = 0; i < count; i++) {
64
+ let y = 1.0 - (2.0 * (i + 0.5)) / count
65
+ let r = Math.sqrt(1.0 - y * y)
66
+ let t = golden * i
67
+ let seed = (i * 0.61803399) % 1.0
68
+ verts.push(0.7 * r * Math.cos(t), 0.7 * y, 0.7 * r * Math.sin(t), seed)
69
+ }
70
+ return new Float32Array(verts)
71
+ }
72
+
73
+ function App() {
74
+ let bufferId = createBuffer(particles(1500), { label: "particle-verts" })
75
+ let id = createPipelineTexture(VERTEX, FRAGMENT, 512, 512, { uTime: 0, uTintA: [1.0, 0.45, 0.15], uTintB: [0.25, 0.5, 1.0] }, {
76
+ label: "particles",
77
+ attributes: [
78
+ { name: "aPos", format: "vec3" },
79
+ { name: "aSeed", format: "f32" },
80
+ ],
81
+ buffer: bufferId,
82
+ topology: "points",
83
+ blend: "add",
84
+ clearColor: [0.02, 0.02, 0.05, 1],
85
+ })
86
+ let [time, setTime] = createSignal(0)
87
+ onFrame((tick) => setTime(tick / 1000))
88
+
89
+ return (
90
+ <window alignItems="center" justifyContent="center">
91
+ <texture src={id} params={{ uTime: time() }} width={420} height={420} />
92
+ </window>
93
+ )
94
+ }
95
+
96
+ render(() => <App />)
@@ -1,43 +1,44 @@
1
- // createPipeline compiles a custom GLSL ES 3.00 vertex+fragment pair that draws
2
- // an interleaved vertex buffer into a texture, here a spinning cube with depth
3
- // testing. The vertex shader declares `in` attributes matching the pipeline's
4
- // attribute list (locations are resolved by name) and its own varyings; the
5
- // fragment preamble provides fragColor/iResolution/iTime but no vUV. Uniforms
6
- // are driven exactly like createShader: declaratively via the <texture> params
7
- // prop, applied at the next repaint.
1
+ // createPipelineTexture compiles a custom GLSL ES 3.00 vertex+fragment pair
2
+ // that draws an interleaved vertex buffer into a texture, here a spinning
3
+ // cube with depth testing. The vertex shader declares `in` attributes
4
+ // matching the pipeline's attribute list (locations are resolved by name)
5
+ // and its own varyings; the fragment preamble provides fragColor/iResolution
6
+ // but no vUV, and app-driven uniforms (uTime below) are the source's own
7
+ // declarations. Uniforms are driven exactly like createShaderTexture:
8
+ // declaratively via the <texture> params prop, applied at the next repaint.
8
9
  import { render, onFrame, createSignal } from "@solidrt/core"
9
- import { createBuffer, createPipeline } from "@solidrt/core/gpu"
10
+ import { createBuffer, createPipelineTexture, glsl } from "@solidrt/core/gpu"
10
11
 
11
- let VERTEX = `
12
- in vec3 aPos;
13
- in vec3 aColor;
14
- out vec3 vColor;
15
- uniform float uTime;
12
+ let VERTEX = glsl`
13
+ in vec3 aPos;
14
+ in vec3 aColor;
15
+ out vec3 vColor;
16
+ uniform float uTime;
16
17
 
17
- void main() {
18
- float cy = cos(uTime), sy = sin(uTime);
19
- float cx = cos(uTime * 0.7), sx = sin(uTime * 0.7);
20
- mat3 rotY = mat3(cy, 0.0, -sy, 0.0, 1.0, 0.0, sy, 0.0, cy);
21
- mat3 rotX = mat3(1.0, 0.0, 0.0, 0.0, cx, sx, 0.0, -sx, cx);
22
- vec3 p = rotX * (rotY * aPos);
23
- p.z += 2.5;
18
+ void main() {
19
+ float cy = cos(uTime), sy = sin(uTime);
20
+ float cx = cos(uTime * 0.7), sx = sin(uTime * 0.7);
21
+ mat3 rotY = mat3(cy, 0.0, -sy, 0.0, 1.0, 0.0, sy, 0.0, cy);
22
+ mat3 rotX = mat3(1.0, 0.0, 0.0, 0.0, cx, sx, 0.0, -sx, cx);
23
+ vec3 p = rotX * (rotY * aPos);
24
+ p.z += 2.5;
24
25
 
25
- // Perspective projection (near 1, far 10). Clip y is negated: the target's
26
- // memory row 0 is clip y = -1, and Impeller samples row 0 as the top, so
27
- // camera-up needs the flip to be displayed up.
28
- float f = 2.0;
29
- float a = 11.0 / 9.0;
30
- float b = -20.0 / 9.0;
31
- gl_Position = vec4(p.x * f, -p.y * f, p.z * a + b, p.z);
32
- vColor = aColor;
33
- }
26
+ // Perspective projection (near 1, far 10). Clip y is negated: the target's
27
+ // memory row 0 is clip y = -1, and Impeller samples row 0 as the top, so
28
+ // camera-up needs the flip to be displayed up.
29
+ float f = 2.0;
30
+ float a = 11.0 / 9.0;
31
+ float b = -20.0 / 9.0;
32
+ gl_Position = vec4(p.x * f, -p.y * f, p.z * a + b, p.z);
33
+ vColor = aColor;
34
+ }
34
35
  `
35
36
 
36
- let FRAGMENT = `
37
- in vec3 vColor;
38
- void main() {
39
- fragColor = vec4(vColor, 1.0);
40
- }
37
+ let FRAGMENT = glsl`
38
+ in vec3 vColor;
39
+ void main() {
40
+ fragColor = vec4(vColor, 1.0);
41
+ }
41
42
  `
42
43
 
43
44
  // Interleaved [pos vec3, color vec3], 6 faces x 2 triangles, one color per face.
@@ -58,9 +59,11 @@ function cube(): Float32Array {
58
59
  }
59
60
 
60
61
  function App() {
61
- let bufferId = createBuffer(cube())
62
- let id = createPipeline(VERTEX, FRAGMENT, 512, 512, {
63
- params: { uTime: 0 },
62
+ // Labels name the buffer and target in the dev tooling's GPU inventory
63
+ // (and in engine log messages) - free-form, purely diagnostic.
64
+ let bufferId = createBuffer(cube(), { label: "cube-verts" })
65
+ let id = createPipelineTexture(VERTEX, FRAGMENT, 512, 512, { uTime: 0 }, {
66
+ label: "cube",
64
67
  attributes: [
65
68
  { name: "aPos", format: "vec3" },
66
69
  { name: "aColor", format: "vec3" },
@@ -0,0 +1,95 @@
1
+ // The raw shading layer: compileShader compiles one stage from complete GLSL
2
+ // ES (nothing injected - the source declares its own #version, precision,
3
+ // varyings and uniforms), linkProgram links a vertex and a fragment stage
4
+ // into a program, createRenderPipeline pairs the program with draw state
5
+ // (none here: attributeless triangles are the defaults), and
6
+ // createShaderTarget builds a render target over the pipeline. One program
7
+ // can back many pipelines, one pipeline many targets, and only the compile
8
+ // step compiles, so swapping precompiled programs is free of compilation.
9
+ // Stages can be destroyed right after linking; the program keeps its own
10
+ // compiled copies.
11
+ //
12
+ // A raw program carries its own vertex stage, so a fullscreen pass is a
13
+ // covering triangle from gl_VertexID with vertexCount: 3. Compare
14
+ // gpu-shader.tsx, where the fused createShaderTexture does all of this in
15
+ // one call with an injected preamble.
16
+ import { render, onFrame, createSignal } from "@solidrt/core"
17
+ import {
18
+ compileShader,
19
+ createRenderPipeline,
20
+ createShaderTarget,
21
+ destroyShader,
22
+ glsl,
23
+ linkProgram,
24
+ } from "@solidrt/core/gpu"
25
+
26
+ let VERTEX = glsl`#version 300 es
27
+ precision highp float;
28
+ out vec2 vUV;
29
+ void main() {
30
+ vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
31
+ vUV = p;
32
+ gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
33
+ }
34
+ `
35
+
36
+ // Two fragment stages linked against the same vertex stage: two programs,
37
+ // one shared compile of the vertex half.
38
+ let WAVES = glsl`#version 300 es
39
+ precision highp float;
40
+ in vec2 vUV;
41
+ out vec4 fragColor;
42
+ uniform float uTime;
43
+ void main() {
44
+ float t = uTime * 2.0;
45
+ float a = 0.5 + 0.5 * sin(vUV.x * 10.0 + t);
46
+ float b = 0.5 + 0.5 * sin(vUV.y * 10.0 - t * 1.3);
47
+ fragColor = vec4(a, b, 1.0 - a * b, 1.0);
48
+ }
49
+ `
50
+
51
+ // The standard header ({ header: true }) declares #version, precision,
52
+ // iResolution and fragColor, so this source only adds its own inputs -
53
+ // including the app-driven time uniform.
54
+ let RINGS = glsl`
55
+ in vec2 vUV;
56
+ uniform float uTime;
57
+ void main() {
58
+ float d = length(vUV - 0.5);
59
+ float r = 0.5 + 0.5 * sin(d * 40.0 - uTime * 3.0);
60
+ fragColor = vec4(r, r * 0.6, 1.0 - r, 1.0);
61
+ }
62
+ `
63
+
64
+ function App() {
65
+ let vs = compileShader("vertex", VERTEX)
66
+ let wavesFs = compileShader("fragment", WAVES)
67
+ let ringsFs = compileShader("fragment", RINGS, { header: true })
68
+ // Labels name each object in the dev tooling's GPU inventory, which is
69
+ // what keeps a chain of programs, pipelines, and targets readable. They
70
+ // are free-form and need not be unique - one name per chain works, since
71
+ // the inventory already groups by kind.
72
+ let waves = linkProgram(vs, wavesFs, { label: "waves" })
73
+ let rings = linkProgram(vs, ringsFs, { label: "rings" })
74
+ destroyShader(vs)
75
+ destroyShader(wavesFs)
76
+ destroyShader(ringsFs)
77
+
78
+ let wavesPipeline = createRenderPipeline(waves, { label: "waves" })
79
+ let ringsPipeline = createRenderPipeline(rings, { label: "rings" })
80
+
81
+ let wavesId = createShaderTarget(wavesPipeline, 512, 512, { uTime: 0 }, { vertexCount: 3, label: "waves" })
82
+ let ringsId = createShaderTarget(ringsPipeline, 512, 512, { uTime: 0 }, { vertexCount: 3, label: "rings" })
83
+
84
+ let [time, setTime] = createSignal(0)
85
+ onFrame(tick => setTime(tick / 1000))
86
+
87
+ return (
88
+ <window flexDirection="row" gap={16} alignItems="center" justifyContent="center">
89
+ <texture src={wavesId} params={{ uTime: time() }} width={360} height={360} />
90
+ <texture src={ringsId} params={{ uTime: time() }} width={360} height={360} />
91
+ </window>
92
+ )
93
+ }
94
+
95
+ render(() => <App />)
@@ -1,38 +1,80 @@
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.
1
+ // createShaderTexture compiles a GLSL ES 3.00 fragment shader and renders it
2
+ // into a texture, returning a texture id you display with <texture src={id}>
3
+ // (the name says what comes back). The texture is freed automatically when
4
+ // the reactive owner is disposed.
7
5
  //
8
- // iResolution is filled in for you, but iTime is NOT - drive it (and any other
9
- // uniform) declaratively via the <texture> element's params prop; it applies at
10
- // the next repaint, so a signal updated every frame stays paced to actual frames.
11
- // The shader's size is baked in at creation.
6
+ // There are two source dialects and the source itself picks which one applies.
7
+ // WITHOUT a #version line the runtime injects a preamble, so the body may
8
+ // reference vUV (0..1, top-left origin), iResolution, and any uniform it
9
+ // declares - the left square below. A source that STARTS
10
+ // with #version 300 es is taken as complete and compiled exactly as written:
11
+ // nothing is injected and it names its own uniforms - the right square.
12
+ //
13
+ // That second dialect is what lets a shader written for somewhere else run
14
+ // here unchanged, without dropping to the raw layer; see gpu-raw-program.tsx
15
+ // for what compileShader/linkProgram are actually for (sharing one compile
16
+ // across several targets). The built-in vertex stage supplies vUV either way -
17
+ // a complete source just has to declare `in vec2 vUV;` itself - and a uniform
18
+ // named iResolution is filled with the target size by name in both dialects.
19
+ //
20
+ // The preamble declares exactly what the runtime provides; anything the app
21
+ // drives is the shader's own declaration in both dialects - `uniform float
22
+ // uTime;` below - fed declaratively via the <texture> element's params prop.
23
+ // A param applies at the next repaint, so a signal updated every frame stays
24
+ // paced to actual frames. A param value is a number for a scalar uniform or a
25
+ // flat number array for a typed one (2/3/4 numbers for vec2/vec3/vec4, 16
26
+ // column-major for mat4), dispatched by the shader's own declaration - uTint
27
+ // below is a vec3 driven from one array value. The shader's size is baked in
28
+ // at creation.
12
29
  import { render, onFrame, createSignal } from "@solidrt/core"
13
- import { createShader } from "@solidrt/core/gpu"
30
+ import { createShaderTexture, glsl } from "@solidrt/core/gpu"
14
31
 
15
- let FRAGMENT = `
16
- void main() {
17
- vec2 uv = vUV;
18
- float t = iTime * 2.0;
19
- float a = 0.5 + 0.5 * sin(uv.x * 10.0 + t);
20
- float b = 0.5 + 0.5 * sin(uv.y * 10.0 - t * 1.3);
21
- float c = 0.5 + 0.5 * sin((uv.x + uv.y) * 8.0 + t * 0.7);
22
- fragColor = vec4(a, b, c, 1.0);
23
- }
32
+ // Injected-preamble dialect: no #version, no main() plumbing; uniforms the
33
+ // app drives are declared here, like any other.
34
+ let FRAGMENT = glsl`
35
+ uniform float uTime;
36
+ void main() {
37
+ vec2 uv = vUV;
38
+ float t = uTime * 2.0;
39
+ float a = 0.5 + 0.5 * sin(uv.x * 10.0 + t);
40
+ float b = 0.5 + 0.5 * sin(uv.y * 10.0 - t * 1.3);
41
+ float c = 0.5 + 0.5 * sin((uv.x + uv.y) * 8.0 + t * 0.7);
42
+ fragColor = vec4(a, b, c, 1.0);
43
+ }
44
+ `
45
+
46
+ // Complete-source dialect: declares its own version, precision, varying and
47
+ // output, and calls its time uniform uSpin. uTint is a typed (vec3) uniform,
48
+ // filled from a 3-number array param.
49
+ let RAW_FRAGMENT = glsl`#version 300 es
50
+ precision highp float;
51
+ in vec2 vUV;
52
+ out vec4 fragColor;
53
+ uniform float uSpin;
54
+ uniform vec3 uTint;
55
+ void main() {
56
+ vec2 p = vUV - 0.5;
57
+ float a = atan(p.y, p.x) + uSpin;
58
+ float r = length(p);
59
+ float band = 0.5 + 0.5 * sin(a * 6.0 + r * 18.0);
60
+ fragColor = vec4(band * uTint.r, band * uTint.g, 1.0 - band * uTint.b, 1.0);
61
+ }
24
62
  `
25
63
 
26
64
  function App() {
27
- let id = createShader(FRAGMENT, 512, 512, { iTime: 0 })
65
+ // The label names the target in the dev tooling's GPU inventory and in
66
+ // engine log messages - free-form, purely diagnostic, worth the habit.
67
+ let id = createShaderTexture(FRAGMENT, 512, 512, { uTime: 0 }, { label: "waves" })
68
+ let rawId = createShaderTexture(RAW_FRAGMENT, 512, 512, { uSpin: 0, uTint: [0.9, 0.4, 0.6] }, { label: "spiral" })
28
69
  let [time, setTime] = createSignal(0)
29
70
  onFrame((tick) => setTime(tick / 1000))
30
71
 
31
72
  return (
32
- <window alignItems="center" justifyContent="center">
33
- <texture src={id} params={{ iTime: time() }} width={400} height={400} />
73
+ <window alignItems="center" justifyContent="center" flexDirection="row" gap={16}>
74
+ <texture src={id} params={{ uTime: time() }} width={260} height={260} />
75
+ <texture src={rawId} params={{ uSpin: time() }} width={260} height={260} />
34
76
  </window>
35
77
  )
36
78
  }
37
79
 
38
- render(() => <App />)
80
+ render(() => <App />)
@@ -0,0 +1,79 @@
1
+ // Compositing two GPU passes in the element tree: stack <texture> layers and
2
+ // give the upper one a blendMode. This is how several shader targets combine -
3
+ // a base pass plus an additive pass - without writing a third shader that
4
+ // samples both. The full Skia blend set is available ("plus", "screen",
5
+ // "multiply", ...), and texture alpha is premultiplied, so additive modes need
6
+ // no manual premultiplication.
7
+ //
8
+ // It is also the ONLY blending there is. A target's own draw runs with GL
9
+ // blending disabled, so overlapping geometry inside one shader or pipeline
10
+ // overwrites rather than accumulates; splitting the work across targets and
11
+ // compositing them here is the way to get transparency between passes.
12
+ //
13
+ // Click to toggle the upper layer between "plus" and "source-over". The glow
14
+ // pass paints opaque black outside its ring, so source-over hides the base
15
+ // entirely while plus adds only the lit pixels - black contributes nothing.
16
+ import { render, onFrame, createSignal } from "@solidrt/core"
17
+ import { createShaderTexture, glsl } from "@solidrt/core/gpu"
18
+
19
+ let SIZE = 360
20
+
21
+ // Base pass: a static gradient with a soft vignette. Nothing drives it, so it
22
+ // renders once at creation and then holds - shaders re-render on params writes.
23
+ let BASE = glsl`
24
+ void main() {
25
+ vec2 uv = vUV;
26
+ float v = 1.0 - length(uv - 0.5) * 1.1;
27
+ vec3 col = mix(vec3(0.04, 0.05, 0.14), vec3(0.15, 0.10, 0.42), uv.y);
28
+ fragColor = vec4(col * v, 1.0);
29
+ }
30
+ `
31
+
32
+ // Additive pass: a breathing ring, black everywhere else.
33
+ let GLOW = glsl`
34
+ uniform float uTime;
35
+ void main() {
36
+ vec2 uv = vUV;
37
+ float r = length(uv - 0.5);
38
+ float radius = 0.28 + 0.04 * sin(uTime * 2.0);
39
+ float ring = smoothstep(0.06, 0.0, abs(r - radius));
40
+ fragColor = vec4(vec3(1.0, 0.55, 0.15) * ring, 1.0);
41
+ }
42
+ `
43
+
44
+ function App() {
45
+ let baseId = createShaderTexture(BASE, SIZE, SIZE, null, { label: "base" })
46
+ let glowId = createShaderTexture(GLOW, SIZE, SIZE, { uTime: 0 }, { label: "glow" })
47
+ let [time, setTime] = createSignal(0)
48
+ let [mode, setMode] = createSignal<"plus" | "source-over">("plus")
49
+ onFrame((tick) => setTime(tick / 1000))
50
+
51
+ return (
52
+ <window
53
+ onPointerDown={() => setMode((m) => (m === "plus" ? "source-over" : "plus"))}
54
+ flexDirection="column"
55
+ alignItems="center"
56
+ justifyContent="center"
57
+ gap={12}
58
+ >
59
+ {/* The layers resolve against this box: absolute children need an
60
+ ancestor with position "relative". */}
61
+ <view position="relative" width={SIZE} height={SIZE}>
62
+ <texture src={baseId} position="absolute" top={0} left={0} width={SIZE} height={SIZE} />
63
+ <texture
64
+ src={glowId}
65
+ position="absolute"
66
+ top={0}
67
+ left={0}
68
+ width={SIZE}
69
+ height={SIZE}
70
+ blendMode={mode()}
71
+ params={{ uTime: time() }}
72
+ />
73
+ </view>
74
+ <text fontSize={14} color="#888">blendMode "{mode()}" - click to toggle</text>
75
+ </window>
76
+ )
77
+ }
78
+
79
+ render(() => <App />)
@@ -15,7 +15,7 @@ import bytes from "./logo.png" with { type: "binary" }
15
15
 
16
16
  function App() {
17
17
  let { data, width, height } = decodeImage(bytes)
18
- let id = createTexture(data, width, height)
18
+ let id = createTexture(data, width, height, { label: "logo" })
19
19
 
20
20
  return (
21
21
  <window alignItems="center" justifyContent="center">