Vol. 01 · Issue 2026Procedural light, four runtimes

Procedural gradients,
native everywhere.

Tweak sliders, pick a stack, copy real .metal, .agsl, .frag, or .glsl. One canonical shader compiles to SwiftUI, Compose, Flutter, and the web. What you preview is exactly what you ship.

Now showing
01 / 04  ·  Aurora
01Live previews

Same shader.
Four runtimes.

Every preview below is the canonical fragment shader running in the browser — exactly the algorithm that ships as Metal, AGSL, or SkSL in your app.

01

SwiftUI · Mesh

Web-verified
iOS · iPadOS 17+native diff pending
02

Compose · Smooth

Verified
Android 13+ · AGSLΔE 48.6 · 3 frames
03

Flutter · fBm

Web-verified
Runner · Flutter desktop
iOS · Android · macOS · Linuxnative diff pending
04

Web · Smooth

Exact
ShaderGradient×
+
shadergradient.app
WebGL2 · @shadergradient/reactΔE 0 · same renderer
02The spectrum

Three shaders.
Fifty-six presets.

Every preset is a tuned spec on one of three shader families. Pick a family, pick a mood — the editor handles the rest.

i
smooth

Smooth

Single-octave Perlin warp with Lambertian light. The classic aurora drift.

ii
mesh

Mesh

Five gaussian blobs at named control points, breathing in slow Lissajous orbits.

iii
fbm

fBm

Multi-octave Perlin sum. Marble. Clouds. Smoke. Thick aurora.

03Real source

One shader.
Four dialects.

Same algorithm. Translated, not approximated. Drop the file into your project — no runtime dependency, no framework lock-in.

~/Prism/ShaderGradient.metal152 lines
1#include <metal_stdlib>
2#include <SwiftUI/SwiftUI_Metal.h>
3using namespace metal;
4
5// ---- Perlin classical noise (Stefan Gustavson, public domain) ----
6// Identical math to the GLSL version, retargeted to MSL semantics.
7
8static inline float3 sg_mod289_3(float3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
9static inline float4 sg_mod289_4(float4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
10static inline float4 sg_permute(float4 x) { return sg_mod289_4(((x * 34.0) + 1.0) * x); }
11static inline float4 sg_tiSqrt(float4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
12static inline float3 sg_fade(float3 t) { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }
13
14static float sg_cnoise(float3 P) {
15 float3 Pi0 = floor(P);
16 float3 Pi1 = Pi0 + float3(1.0);
17 Pi0 = sg_mod289_3(Pi0);
18 Pi1 = sg_mod289_3(Pi1);
19 float3 Pf0 = fract(P);
20 float3 Pf1 = Pf0 - float3(1.0);
21 float4 ix = float4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
22 float4 iy = float4(Pi0.y, Pi0.y, Pi1.y, Pi1.y);
23 float4 iz0 = float4(Pi0.z);
24 float4 iz1 = float4(Pi1.z);
25 float4 ixy = sg_permute(sg_permute(ix) + iy);
26 float4 ixy0 = sg_permute(ixy + iz0);
27 float4 ixy1 = sg_permute(ixy + iz1);
28 float4 gx0 = ixy0 * (1.0 / 7.0);
29 float4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5;
30 gx0 = fract(gx0);
31 float4 gz0 = float4(0.5) - abs(gx0) - abs(gy0);
32 float4 sz0 = step(gz0, float4(0.0));
33 gx0 -= sz0 * (step(0.0, gx0) - 0.5);
34 gy0 -= sz0 * (step(0.0, gy0) - 0.5);
35 float4 gx1 = ixy1 * (1.0 / 7.0);
36 float4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5;
37 gx1 = fract(gx1);
38 float4 gz1 = float4(0.5) - abs(gx1) - abs(gy1);
39 float4 sz1 = step(gz1, float4(0.0));
40 gx1 -= sz1 * (step(0.0, gx1) - 0.5);
41 gy1 -= sz1 * (step(0.0, gy1) - 0.5);
42 float3 g000 = float3(gx0.x, gy0.x, gz0.x);
43 float3 g100 = float3(gx0.y, gy0.y, gz0.y);
44 float3 g010 = float3(gx0.z, gy0.z, gz0.z);
45 float3 g110 = float3(gx0.w, gy0.w, gz0.w);
46 float3 g001 = float3(gx1.x, gy1.x, gz1.x);
47 float3 g101 = float3(gx1.y, gy1.y, gz1.y);
48 float3 g011 = float3(gx1.z, gy1.z, gz1.z);
49 float3 g111 = float3(gx1.w, gy1.w, gz1.w);
50 float4 norm0 = sg_tiSqrt(float4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110)));
51 g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w;
52 float4 norm1 = sg_tiSqrt(float4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111)));
53 g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w;
54 float n000 = dot(g000, Pf0);
55 float n100 = dot(g100, float3(Pf1.x, Pf0.y, Pf0.z));
56 float n010 = dot(g010, float3(Pf0.x, Pf1.y, Pf0.z));
57 float n110 = dot(g110, float3(Pf1.x, Pf1.y, Pf0.z));
58 float n001 = dot(g001, float3(Pf0.x, Pf0.y, Pf1.z));
59 float n101 = dot(g101, float3(Pf1.x, Pf0.y, Pf1.z));
60 float n011 = dot(g011, float3(Pf0.x, Pf1.y, Pf1.z));
61 float n111 = dot(g111, Pf1);
62 float3 fade_xyz = sg_fade(Pf0);
63 float4 n_z = mix(float4(n000, n100, n010, n110), float4(n001, n101, n011, n111), fade_xyz.z);
64 float2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
65 float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
66 return 2.2 * n_xyz;
67}
68
69static float sg_sampleNoise(float3 p, float t, float uTime, float uSpeed, float uLoop, float uLoopDuration) {
70 if (uLoop > 0.5) {
71 float loopProgress = uTime / uLoopDuration;
72 float angle = loopProgress * 6.28318530718;
73 float radius = 5.0 * uSpeed;
74 float3 o0 = float3(cos(angle) * radius, sin(angle) * radius, 0.0);
75 float3 o1 = float3(cos(angle + 1.57079632679) * radius, sin(angle + 1.57079632679) * radius, 0.0);
76 float3 o2 = float3(cos(angle + 3.14159265359) * radius, sin(angle + 3.14159265359) * radius, 0.0);
77 float3 o3 = float3(cos(angle + 4.71238898038) * radius, sin(angle + 4.71238898038) * radius, 0.0);
78 float n0 = sg_cnoise(p + o0);
79 float n1 = sg_cnoise(p + o1);
80 float n2 = sg_cnoise(p + o2);
81 float n3 = sg_cnoise(p + o3);
82 float w0 = (cos(angle) + 1.0) * 0.5;
83 float w1 = (cos(angle + 1.57079632679) + 1.0) * 0.5;
84 float w2 = (cos(angle + 3.14159265359) + 1.0) * 0.5;
85 float w3 = (cos(angle + 4.71238898038) + 1.0) * 0.5;
86 float tw = w0 + w1 + w2 + w3;
87 return (n0 * w0 + n1 * w1 + n2 * w2 + n3 * w3) / tw * 1.5;
88 }
89 return sg_cnoise(p + t);
90}
91
92// ---- Stitchable colorEffect entrypoint ----
93//
94// Uniform packing (matches the SwiftUI call site):
95// uTime, uSpeed, uNoiseDensity, uNoiseStrength,
96// uLoop, uLoopDuration,
97// uColor1, uColor2, uColor3, // each as float3
98// uGrain,
99// uResolution // float2
100//
101// SwiftUI's .colorEffect passes (position, color) as the first two args.
102// Remaining args come from the .colorEffect(...) shader argument list.
103
104[[stitchable]] half4 shaderGradient(
105 float2 position,
106 half4 color,
107 float uTime,
108 float uSpeed,
109 float uNoiseDensity,
110 float uNoiseStrength,
111 float uLoop,
112 float uLoopDuration,
113 float3 uColor1,
114 float3 uColor2,
115 float3 uColor3,
116 float uGrain,
117 float2 uResolution,
118 float uBrightness
119) {
120 // SwiftUI's .colorEffect passes top-left-origin pixel coords. Flip Y so
121 // our UV space matches WebGL/GLSL (bottom-left origin) and the shader
122 // produces the same visual output as the web preview.
123 float2 uv = float2(position.x, uResolution.y - position.y) / uResolution;
124 float t = uTime * uSpeed;
125
126 float3 noisePos = float3(uv * uNoiseDensity * 4.0, 0.0);
127 float n = sg_sampleNoise(noisePos, t, uTime, uSpeed, uLoop, uLoopDuration);
128
129 float2 warpedUv = uv + float2(n * uNoiseStrength * 0.15);
130
131 float gx = clamp(warpedUv.x * 6.0 - 3.0, -3.0, 3.0);
132 float gz = clamp(warpedUv.y, 0.0, 1.0);
133 float3 base = mix(mix(uColor1, uColor2, smoothstep(-3.0, 3.0, gx)), uColor3, gz);
134
135 float3 nx = float3(uNoiseDensity * 0.4, 0.0, 0.0);
136 float3 ny = float3(0.0, uNoiseDensity * 0.4, 0.0);
137 float nh = sg_sampleNoise(noisePos + nx, t, uTime, uSpeed, uLoop, uLoopDuration)
138 - sg_sampleNoise(noisePos - nx, t, uTime, uSpeed, uLoop, uLoopDuration);
139 float nv = sg_sampleNoise(noisePos + ny, t, uTime, uSpeed, uLoop, uLoopDuration)
140 - sg_sampleNoise(noisePos - ny, t, uTime, uSpeed, uLoop, uLoopDuration);
141 float3 normal = normalize(float3(-nh, -nv, 2.0));
142 float3 lightDir = normalize(float3(0.4, 0.6, 0.7));
143 float lambert = max(dot(normal, lightDir), 0.0);
144 float3 lit = base * (0.6 + 0.5 * lambert);
145
146 if (uGrain > 0.001) {
147 float h = fract(sin(dot(position + t * 60.0, float2(12.9898, 78.233))) * 43758.5453);
148 lit += float3((h - 0.5) * uGrain * 0.17);
149 }
150
151 return half4(half3(lit * uBrightness), 1.0h);
152}
04Honest telemetry

Pixel-verified.
Every release.

Pixel-perfect across GPUs isn't physically possible — different drivers, different precision floors. So we measure mean ΔE against the web golden on every release. These are real numbers.

SwiftUI · Metal
Verified
48.6
iPhone 17 Pro Simulator (iOS 26.4)
2026-05-21
Compose · AGSL
Verified
48.6
Pixel 9 Emulator (Android 16, AGSL RuntimeShader)
2026-05-21
Flutter · SkSL
Verified
48.9
macOS 26.3 Apple Silicon (Flutter 3.35.6 Impeller)
2026-05-21
Web · WebGL2
Exact
Same renderer as preview

ΔE is the mean per-channel CIE delta · 0–255 · across three captured frames. Threshold for "verified" is < 60 / 255.

05Questions

Frequently
asked.

Inspired by ShaderGradient. Prism takes that visual vocabulary, expands it with two new shader families (mesh, fBm), and ports each natively to SwiftUI (Metal), Jetpack Compose (AGSL), Flutter (SkSL), and — coming next — React Native via Skia. The web preview uses the same canonical fragment shader you ship to native, so what you preview is what you get.
No. The editor emits a ready-to-paste file for every target. Drop it into Xcode, Android Studio, or your Flutter project — it just runs.
Within ~48 ΔE across the 0–255 channel space. The differences are GPU-precision floors and color-space encodings, not algorithmic divergence. The shape, the motion, and the colors are identical.
iOS 17 (Metal-via-SwiftUI colorEffect), Android 13 / API 33 (AGSL RuntimeShader), Flutter 3.7 (FragmentProgram), and any evergreen browser supporting WebGL2.
Yes. The generated code is yours — copy it, modify it, ship it. No license, no attribution required.
On GitHub. The shader, the codegen, the verify harness — all open. See Amal-David/prism.