feat: add SkSL GPU shaders for wallpaper animations
Ken Burns (zoom+pan), Color Shift (HSL cycling), Particles (snow/rain/bokeh overlay), Vignette Pulse, Glitch (RGB split), and Depth Parallax (gyroscope-driven 2.5D via depth map).
This commit is contained in:
82
src/shaders/color-shift.ts
Normal file
82
src/shaders/color-shift.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Skia } from "@shopify/react-native-skia";
|
||||
|
||||
// Color Shift: cyclic hue/saturation breathing effect
|
||||
// Uniforms: time, intensity, speed
|
||||
export const colorShiftShader = Skia.RuntimeEffect.Make(`
|
||||
uniform shader image;
|
||||
uniform float2 resolution;
|
||||
uniform float time;
|
||||
uniform float intensity;
|
||||
uniform float speed;
|
||||
|
||||
// RGB to HSL conversion
|
||||
float3 rgb2hsl(float3 c) {
|
||||
float mx = max(c.r, max(c.g, c.b));
|
||||
float mn = min(c.r, min(c.g, c.b));
|
||||
float l = (mx + mn) * 0.5;
|
||||
float s = 0.0;
|
||||
float h = 0.0;
|
||||
|
||||
if (mx != mn) {
|
||||
float d = mx - mn;
|
||||
s = l > 0.5 ? d / (2.0 - mx - mn) : d / (mx + mn);
|
||||
|
||||
if (mx == c.r) {
|
||||
h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
|
||||
} else if (mx == c.g) {
|
||||
h = (c.b - c.r) / d + 2.0;
|
||||
} else {
|
||||
h = (c.r - c.g) / d + 4.0;
|
||||
}
|
||||
h /= 6.0;
|
||||
}
|
||||
|
||||
return float3(h, s, l);
|
||||
}
|
||||
|
||||
float hue2rgb(float p, float q, float t) {
|
||||
float tt = t;
|
||||
if (tt < 0.0) tt += 1.0;
|
||||
if (tt > 1.0) tt -= 1.0;
|
||||
if (tt < 1.0/6.0) return p + (q - p) * 6.0 * tt;
|
||||
if (tt < 1.0/2.0) return q;
|
||||
if (tt < 2.0/3.0) return p + (q - p) * (2.0/3.0 - tt) * 6.0;
|
||||
return p;
|
||||
}
|
||||
|
||||
float3 hsl2rgb(float3 hsl) {
|
||||
float h = hsl.x;
|
||||
float s = hsl.y;
|
||||
float l = hsl.z;
|
||||
|
||||
if (s == 0.0) return float3(l);
|
||||
|
||||
float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
|
||||
float p = 2.0 * l - q;
|
||||
|
||||
return float3(
|
||||
hue2rgb(p, q, h + 1.0/3.0),
|
||||
hue2rgb(p, q, h),
|
||||
hue2rgb(p, q, h - 1.0/3.0)
|
||||
);
|
||||
}
|
||||
|
||||
half4 main(float2 coord) {
|
||||
half4 color = image.eval(coord);
|
||||
float3 hsl = rgb2hsl(float3(color.rgb));
|
||||
|
||||
float t = time * speed * 0.15;
|
||||
|
||||
// Shift hue cyclically
|
||||
hsl.x = fract(hsl.x + intensity * 0.1 * sin(t));
|
||||
|
||||
// Pulse saturation
|
||||
hsl.y = clamp(hsl.y + intensity * 0.15 * sin(t * 1.3), 0.0, 1.0);
|
||||
|
||||
// Subtle luminance breathing
|
||||
hsl.z = clamp(hsl.z + intensity * 0.03 * sin(t * 0.8), 0.0, 1.0);
|
||||
|
||||
float3 rgb = hsl2rgb(hsl);
|
||||
return half4(half3(rgb), color.a);
|
||||
}
|
||||
`)!;
|
||||
37
src/shaders/depth-parallax.ts
Normal file
37
src/shaders/depth-parallax.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Skia } from "@shopify/react-native-skia";
|
||||
|
||||
// Depth Parallax 2.5D: uses a depth map to create parallax displacement
|
||||
// Requires ML-generated depth map (MiDaS) — V1.5 feature
|
||||
// Uniforms: time, intensity, speed, gyroX, gyroY
|
||||
export const depthParallaxShader = Skia.RuntimeEffect.Make(`
|
||||
uniform shader image;
|
||||
uniform shader depthMap;
|
||||
uniform float2 resolution;
|
||||
uniform float time;
|
||||
uniform float intensity;
|
||||
uniform float speed;
|
||||
uniform float gyroX;
|
||||
uniform float gyroY;
|
||||
|
||||
half4 main(float2 coord) {
|
||||
float2 uv = coord / resolution;
|
||||
|
||||
// Sample depth (grayscale, 0=far, 1=near)
|
||||
float depth = depthMap.eval(coord).r;
|
||||
|
||||
// Combine gyroscope input with time-based oscillation
|
||||
float t = time * speed * 0.2;
|
||||
float2 offset = float2(
|
||||
gyroX * 0.8 + sin(t) * 0.3,
|
||||
gyroY * 0.8 + cos(t * 0.7) * 0.3
|
||||
);
|
||||
|
||||
// Displacement proportional to depth
|
||||
// Near objects (depth=1) move more, far objects (depth=0) stay still
|
||||
float2 displacement = offset * depth * intensity * 0.04;
|
||||
|
||||
float2 sampleUV = clamp(uv + displacement, float2(0.0), float2(1.0));
|
||||
|
||||
return image.eval(sampleUV * resolution);
|
||||
}
|
||||
`)!;
|
||||
74
src/shaders/glitch.ts
Normal file
74
src/shaders/glitch.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Skia } from "@shopify/react-native-skia";
|
||||
|
||||
// Glitch: periodic RGB channel split with noise displacement
|
||||
// Uniforms: time, intensity, speed
|
||||
export const glitchShader = Skia.RuntimeEffect.Make(`
|
||||
uniform shader image;
|
||||
uniform float2 resolution;
|
||||
uniform float time;
|
||||
uniform float intensity;
|
||||
uniform float speed;
|
||||
|
||||
float hash(float n) {
|
||||
return fract(sin(n) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(float2 p) {
|
||||
float2 i = floor(p);
|
||||
float2 f = fract(p);
|
||||
float a = hash(i.x + i.y * 57.0);
|
||||
float b = hash(i.x + 1.0 + i.y * 57.0);
|
||||
float c = hash(i.x + (i.y + 1.0) * 57.0);
|
||||
float d = hash(i.x + 1.0 + (i.y + 1.0) * 57.0);
|
||||
float2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||||
}
|
||||
|
||||
half4 main(float2 coord) {
|
||||
float2 uv = coord / resolution;
|
||||
float t = time * speed * 0.5;
|
||||
|
||||
// Glitch trigger: periodic bursts
|
||||
float glitchTrigger = step(0.92, sin(t * 2.0) * sin(t * 7.3));
|
||||
float glitchAmount = intensity * glitchTrigger;
|
||||
|
||||
// Horizontal displacement based on noise
|
||||
float blockY = floor(uv.y * 20.0) / 20.0;
|
||||
float noiseVal = noise(float2(blockY * 50.0, t * 10.0));
|
||||
float displacement = (noiseVal - 0.5) * glitchAmount * 0.08;
|
||||
|
||||
// Scanline flicker
|
||||
float scanline = sin(uv.y * resolution.y * 1.5 + t * 20.0) * 0.02 * glitchAmount;
|
||||
|
||||
// Sample RGB channels with offset
|
||||
float shift = intensity * 0.006 + glitchAmount * 0.015;
|
||||
float2 uvR = float2(uv.x + shift + displacement, uv.y + scanline);
|
||||
float2 uvG = float2(uv.x + displacement, uv.y + scanline);
|
||||
float2 uvB = float2(uv.x - shift + displacement, uv.y + scanline);
|
||||
|
||||
// Clamp UVs
|
||||
uvR = clamp(uvR, float2(0.0), float2(1.0));
|
||||
uvG = clamp(uvG, float2(0.0), float2(1.0));
|
||||
uvB = clamp(uvB, float2(0.0), float2(1.0));
|
||||
|
||||
float r = image.eval(uvR * resolution).r;
|
||||
float g = image.eval(uvG * resolution).g;
|
||||
float b = image.eval(uvB * resolution).b;
|
||||
|
||||
float3 result = float3(r, g, b);
|
||||
|
||||
// Subtle persistent RGB split (even without glitch burst)
|
||||
float persistentShift = intensity * 0.003;
|
||||
float2 uvR2 = float2(uv.x + persistentShift, uv.y) * resolution;
|
||||
float2 uvB2 = float2(uv.x - persistentShift, uv.y) * resolution;
|
||||
float3 persistent = float3(
|
||||
image.eval(uvR2).r,
|
||||
image.eval(coord).g,
|
||||
image.eval(uvB2).b
|
||||
);
|
||||
|
||||
result = mix(persistent, result, glitchTrigger);
|
||||
|
||||
return half4(half3(result), 1.0);
|
||||
}
|
||||
`)!;
|
||||
26
src/shaders/index.ts
Normal file
26
src/shaders/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { SkRuntimeEffect } from "@shopify/react-native-skia";
|
||||
import { kenBurnsShader } from "./ken-burns";
|
||||
import { colorShiftShader } from "./color-shift";
|
||||
import { particlesShader } from "./particles";
|
||||
import { vignettePulseShader } from "./vignette-pulse";
|
||||
import { glitchShader } from "./glitch";
|
||||
import { depthParallaxShader } from "./depth-parallax";
|
||||
import type { AnimationType } from "@/types/wallpaper";
|
||||
|
||||
export const shaders: Record<AnimationType, SkRuntimeEffect | null> = {
|
||||
"ken-burns": kenBurnsShader,
|
||||
"color-shift": colorShiftShader,
|
||||
particles: particlesShader,
|
||||
"vignette-pulse": vignettePulseShader,
|
||||
glitch: glitchShader,
|
||||
"depth-parallax": depthParallaxShader,
|
||||
};
|
||||
|
||||
export {
|
||||
kenBurnsShader,
|
||||
colorShiftShader,
|
||||
particlesShader,
|
||||
vignettePulseShader,
|
||||
glitchShader,
|
||||
depthParallaxShader,
|
||||
};
|
||||
33
src/shaders/ken-burns.ts
Normal file
33
src/shaders/ken-burns.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Skia } from "@shopify/react-native-skia";
|
||||
|
||||
// Ken Burns: slow zoom + pan across the image
|
||||
// Uniforms: time, intensity (zoom amplitude), speed, direction (angle in radians)
|
||||
export const kenBurnsShader = Skia.RuntimeEffect.Make(`
|
||||
uniform shader image;
|
||||
uniform float2 resolution;
|
||||
uniform float time;
|
||||
uniform float intensity;
|
||||
uniform float speed;
|
||||
uniform float direction;
|
||||
|
||||
half4 main(float2 coord) {
|
||||
float t = time * speed * 0.1;
|
||||
|
||||
// Compute zoom: oscillates between 1.0 and 1.0 + intensity * 0.15
|
||||
float zoom = 1.0 + intensity * 0.15 * (0.5 + 0.5 * sin(t));
|
||||
|
||||
// Compute pan offset based on direction
|
||||
float panX = cos(direction) * intensity * 0.05 * sin(t * 0.7);
|
||||
float panY = sin(direction) * intensity * 0.05 * cos(t * 0.7);
|
||||
|
||||
// Transform coordinates: center, scale, offset, uncenter
|
||||
float2 center = resolution * 0.5;
|
||||
float2 uv = (coord - center) / zoom + center;
|
||||
uv += float2(panX, panY) * resolution;
|
||||
|
||||
// Clamp to image bounds
|
||||
uv = clamp(uv, float2(0.0), resolution);
|
||||
|
||||
return image.eval(uv);
|
||||
}
|
||||
`)!;
|
||||
102
src/shaders/particles.ts
Normal file
102
src/shaders/particles.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Skia } from "@shopify/react-native-skia";
|
||||
|
||||
// Particle overlay: snow, rain, bokeh rendered as shader
|
||||
// Uniforms: time, intensity, speed, particleType (0=snow, 1=rain, 2=bokeh)
|
||||
export const particlesShader = Skia.RuntimeEffect.Make(`
|
||||
uniform shader image;
|
||||
uniform float2 resolution;
|
||||
uniform float time;
|
||||
uniform float intensity;
|
||||
uniform float speed;
|
||||
uniform float particleType;
|
||||
|
||||
// Hash function for pseudo-random values
|
||||
float hash(float2 p) {
|
||||
float3 p3 = fract(float3(p.xyx) * 0.1031);
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
// Snow particle layer
|
||||
float snow(float2 uv, float layer) {
|
||||
float t = time * speed * 0.4 + layer * 12.345;
|
||||
float2 grid = floor(uv * (8.0 + layer * 6.0));
|
||||
float2 frac_uv = fract(uv * (8.0 + layer * 6.0));
|
||||
|
||||
float rnd = hash(grid);
|
||||
float2 center = float2(
|
||||
0.5 + 0.3 * sin(t + rnd * 6.28),
|
||||
fract(rnd + t * (0.1 + rnd * 0.08))
|
||||
);
|
||||
|
||||
float d = length(frac_uv - center);
|
||||
float size = 0.04 + rnd * 0.06;
|
||||
float alpha = smoothstep(size, size * 0.3, d);
|
||||
|
||||
return alpha * (0.4 + 0.6 * rnd);
|
||||
}
|
||||
|
||||
// Rain streak
|
||||
float rain(float2 uv, float layer) {
|
||||
float t = time * speed * 1.2 + layer * 7.89;
|
||||
float2 grid = floor(uv * float2(20.0, 4.0 + layer * 2.0));
|
||||
float2 frac_uv = fract(uv * float2(20.0, 4.0 + layer * 2.0));
|
||||
|
||||
float rnd = hash(grid);
|
||||
float2 center = float2(0.5, fract(rnd - t * (0.3 + rnd * 0.2)));
|
||||
|
||||
// Elongated shape for rain drops
|
||||
float dx = abs(frac_uv.x - center.x);
|
||||
float dy = abs(frac_uv.y - center.y);
|
||||
float d = dx * 8.0 + dy * 1.5;
|
||||
|
||||
float alpha = smoothstep(0.5, 0.1, d) * step(0.5, rnd);
|
||||
return alpha * 0.5;
|
||||
}
|
||||
|
||||
// Bokeh circle
|
||||
float bokeh(float2 uv, float layer) {
|
||||
float t = time * speed * 0.2 + layer * 5.67;
|
||||
float2 grid = floor(uv * (3.0 + layer * 2.0));
|
||||
float2 frac_uv = fract(uv * (3.0 + layer * 2.0));
|
||||
|
||||
float rnd = hash(grid);
|
||||
float2 center = float2(
|
||||
0.5 + 0.2 * sin(t * 0.5 + rnd * 6.28),
|
||||
0.5 + 0.2 * cos(t * 0.3 + rnd * 6.28)
|
||||
);
|
||||
|
||||
float d = length(frac_uv - center);
|
||||
float size = 0.1 + rnd * 0.15;
|
||||
|
||||
// Ring shape for bokeh
|
||||
float ring = smoothstep(size, size - 0.02, d) - smoothstep(size - 0.03, size - 0.06, d);
|
||||
float fill = smoothstep(size, size * 0.2, d) * 0.3;
|
||||
|
||||
return (ring + fill) * (0.3 + 0.7 * rnd) * step(0.3, rnd);
|
||||
}
|
||||
|
||||
half4 main(float2 coord) {
|
||||
half4 color = image.eval(coord);
|
||||
float2 uv = coord / resolution;
|
||||
|
||||
float particles = 0.0;
|
||||
|
||||
// Stack 3 layers for depth
|
||||
for (float i = 0.0; i < 3.0; i += 1.0) {
|
||||
if (particleType < 0.5) {
|
||||
particles += snow(uv, i);
|
||||
} else if (particleType < 1.5) {
|
||||
particles += rain(uv, i);
|
||||
} else {
|
||||
particles += bokeh(uv, i);
|
||||
}
|
||||
}
|
||||
|
||||
particles *= intensity;
|
||||
|
||||
// Additive blending
|
||||
float3 result = float3(color.rgb) + float3(particles);
|
||||
return half4(half3(clamp(result, float3(0.0), float3(1.0))), color.a);
|
||||
}
|
||||
`)!;
|
||||
34
src/shaders/vignette-pulse.ts
Normal file
34
src/shaders/vignette-pulse.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Skia } from "@shopify/react-native-skia";
|
||||
|
||||
// Vignette Pulse: dynamic vignette with oscillating intensity
|
||||
// Uniforms: time, intensity, speed
|
||||
export const vignettePulseShader = Skia.RuntimeEffect.Make(`
|
||||
uniform shader image;
|
||||
uniform float2 resolution;
|
||||
uniform float time;
|
||||
uniform float intensity;
|
||||
uniform float speed;
|
||||
|
||||
half4 main(float2 coord) {
|
||||
half4 color = image.eval(coord);
|
||||
float2 uv = coord / resolution;
|
||||
|
||||
// Distance from center
|
||||
float2 center = uv - 0.5;
|
||||
float dist = length(center);
|
||||
|
||||
// Oscillating vignette
|
||||
float t = time * speed * 0.3;
|
||||
float vignetteStrength = intensity * (0.6 + 0.4 * sin(t));
|
||||
|
||||
// Smooth radial gradient with asymmetric falloff
|
||||
float vignette = 1.0 - smoothstep(0.2, 0.85, dist * (1.0 + vignetteStrength));
|
||||
|
||||
// Subtle color tinting at the edges
|
||||
float tint = (1.0 - vignette) * 0.15 * intensity;
|
||||
float3 tinted = float3(color.rgb) * vignette;
|
||||
tinted.b += tint * 0.3; // Slight blue tint in shadows
|
||||
|
||||
return half4(half3(clamp(tinted, float3(0.0), float3(1.0))), color.a);
|
||||
}
|
||||
`)!;
|
||||
Reference in New Issue
Block a user