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.
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.
SwiftUI · Mesh
Compose · Smooth
Flutter · fBm
Web · Smooth
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.
Smooth
Single-octave Perlin warp with Lambertian light. The classic aurora drift.
Mesh
Five gaussian blobs at named control points, breathing in slow Lissajous orbits.
fBm
Multi-octave Perlin sum. Marble. Clouds. Smoke. Thick aurora.
One shader.
Four dialects.
Same algorithm. Translated, not approximated. Drop the file into your project — no runtime dependency, no framework lock-in.
1#include <metal_stdlib>2#include <SwiftUI/SwiftUI_Metal.h>3using namespace metal;45// ---- Perlin classical noise (Stefan Gustavson, public domain) ----6// Identical math to the GLSL version, retargeted to MSL semantics.78static 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); }1314static 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}6869static 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}9192// ---- 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 float398// uGrain,99// uResolution // float2100//101// SwiftUI's .colorEffect passes (position, color) as the first two args.102// Remaining args come from the .colorEffect(...) shader argument list.103104[[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 uBrightness119) {120 // SwiftUI's .colorEffect passes top-left-origin pixel coords. Flip Y so121 // our UV space matches WebGL/GLSL (bottom-left origin) and the shader122 // 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;125126 float3 noisePos = float3(uv * uNoiseDensity * 4.0, 0.0);127 float n = sg_sampleNoise(noisePos, t, uTime, uSpeed, uLoop, uLoopDuration);128129 float2 warpedUv = uv + float2(n * uNoiseStrength * 0.15);130131 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);134135 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);145146 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 }150151 return half4(half3(lit * uBrightness), 1.0h);152}
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.
ΔE is the mean per-channel CIE delta · 0–255 · across three captured frames. Threshold for "verified" is < 60 / 255.
Frequently
asked.
colorEffect), Android 13 / API 33 (AGSL RuntimeShader), Flutter 3.7 (FragmentProgram), and any evergreen browser supporting WebGL2.