react-native-effects 0.0.1 → 0.2.0
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/LICENSE +20 -0
- package/README.md +313 -0
- package/lib/module/components/Aurora.js +184 -0
- package/lib/module/components/Aurora.js.map +1 -0
- package/lib/module/components/CalicoSwirl.js +155 -0
- package/lib/module/components/CalicoSwirl.js.map +1 -0
- package/lib/module/components/Campfire.js +225 -0
- package/lib/module/components/Campfire.js.map +1 -0
- package/lib/module/components/CircularGradient.js +52 -0
- package/lib/module/components/CircularGradient.js.map +1 -0
- package/lib/module/components/Iridescence.js +57 -0
- package/lib/module/components/Iridescence.js.map +1 -0
- package/lib/module/components/LinearGradient.js +48 -0
- package/lib/module/components/LinearGradient.js.map +1 -0
- package/lib/module/components/LiquidChrome.js +75 -0
- package/lib/module/components/LiquidChrome.js.map +1 -0
- package/lib/module/components/ShaderView/index.js +252 -0
- package/lib/module/components/ShaderView/index.js.map +1 -0
- package/lib/module/components/ShaderView/types.js +4 -0
- package/lib/module/components/ShaderView/types.js.map +1 -0
- package/lib/module/components/ShaderViewWithPanGesture/index.js +196 -0
- package/lib/module/components/ShaderViewWithPanGesture/index.js.map +1 -0
- package/lib/module/components/Silk.js +83 -0
- package/lib/module/components/Silk.js.map +1 -0
- package/lib/module/consts.js +154 -0
- package/lib/module/consts.js.map +1 -0
- package/lib/module/hooks/useClock.js +15 -0
- package/lib/module/hooks/useClock.js.map +1 -0
- package/lib/module/hooks/useParamsSynchronizable.js +37 -0
- package/lib/module/hooks/useParamsSynchronizable.js.map +1 -0
- package/lib/module/hooks/useWGPUSetup.js +54 -0
- package/lib/module/hooks/useWGPUSetup.js.map +1 -0
- package/lib/module/index.js +15 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/shaders/TRIANGLE_VERTEX_SHADER.js +20 -0
- package/lib/module/shaders/TRIANGLE_VERTEX_SHADER.js.map +1 -0
- package/lib/module/shaders/uniforms.js +21 -0
- package/lib/module/shaders/uniforms.js.map +1 -0
- package/lib/module/utils/backgroundRuntime.js +12 -0
- package/lib/module/utils/backgroundRuntime.js.map +1 -0
- package/lib/module/utils/colors.js +94 -0
- package/lib/module/utils/colors.js.map +1 -0
- package/lib/module/utils/initWebGPU.js +40 -0
- package/lib/module/utils/initWebGPU.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/components/Aurora.d.ts +17 -0
- package/lib/typescript/src/components/Aurora.d.ts.map +1 -0
- package/lib/typescript/src/components/CalicoSwirl.d.ts +13 -0
- package/lib/typescript/src/components/CalicoSwirl.d.ts.map +1 -0
- package/lib/typescript/src/components/Campfire.d.ts +17 -0
- package/lib/typescript/src/components/Campfire.d.ts.map +1 -0
- package/lib/typescript/src/components/CircularGradient.d.ts +19 -0
- package/lib/typescript/src/components/CircularGradient.d.ts.map +1 -0
- package/lib/typescript/src/components/Iridescence.d.ts +11 -0
- package/lib/typescript/src/components/Iridescence.d.ts.map +1 -0
- package/lib/typescript/src/components/LinearGradient.d.ts +15 -0
- package/lib/typescript/src/components/LinearGradient.d.ts.map +1 -0
- package/lib/typescript/src/components/LiquidChrome.d.ts +17 -0
- package/lib/typescript/src/components/LiquidChrome.d.ts.map +1 -0
- package/lib/typescript/src/components/ShaderView/index.d.ts +3 -0
- package/lib/typescript/src/components/ShaderView/index.d.ts.map +1 -0
- package/lib/typescript/src/components/ShaderView/types.d.ts +35 -0
- package/lib/typescript/src/components/ShaderView/types.d.ts.map +1 -0
- package/lib/typescript/src/components/ShaderViewWithPanGesture/index.d.ts +35 -0
- package/lib/typescript/src/components/ShaderViewWithPanGesture/index.d.ts.map +1 -0
- package/lib/typescript/src/components/Silk.d.ts +17 -0
- package/lib/typescript/src/components/Silk.d.ts.map +1 -0
- package/lib/typescript/src/consts.d.ts +2 -0
- package/lib/typescript/src/consts.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useClock.d.ts +3 -0
- package/lib/typescript/src/hooks/useClock.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useParamsSynchronizable.d.ts +22 -0
- package/lib/typescript/src/hooks/useParamsSynchronizable.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useWGPUSetup.d.ts +15 -0
- package/lib/typescript/src/hooks/useWGPUSetup.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +16 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/shaders/TRIANGLE_VERTEX_SHADER.d.ts +2 -0
- package/lib/typescript/src/shaders/TRIANGLE_VERTEX_SHADER.d.ts.map +1 -0
- package/lib/typescript/src/shaders/uniforms.d.ts +6 -0
- package/lib/typescript/src/shaders/uniforms.d.ts.map +1 -0
- package/lib/typescript/src/utils/backgroundRuntime.d.ts +3 -0
- package/lib/typescript/src/utils/backgroundRuntime.d.ts.map +1 -0
- package/lib/typescript/src/utils/colors.d.ts +22 -0
- package/lib/typescript/src/utils/colors.d.ts.map +1 -0
- package/lib/typescript/src/utils/initWebGPU.d.ts +23 -0
- package/lib/typescript/src/utils/initWebGPU.d.ts.map +1 -0
- package/package.json +175 -7
- package/src/components/Aurora.tsx +203 -0
- package/src/components/CalicoSwirl.tsx +167 -0
- package/src/components/Campfire.tsx +244 -0
- package/src/components/CircularGradient.tsx +76 -0
- package/src/components/Iridescence.tsx +67 -0
- package/src/components/LinearGradient.tsx +62 -0
- package/src/components/LiquidChrome.tsx +94 -0
- package/src/components/ShaderView/index.tsx +262 -0
- package/src/components/ShaderView/types.ts +36 -0
- package/src/components/ShaderViewWithPanGesture/index.tsx +225 -0
- package/src/components/Silk.tsx +102 -0
- package/src/consts.ts +152 -0
- package/src/hooks/useClock.ts +20 -0
- package/src/hooks/useParamsSynchronizable.ts +52 -0
- package/src/hooks/useWGPUSetup.tsx +73 -0
- package/src/index.tsx +32 -0
- package/src/shaders/TRIANGLE_VERTEX_SHADER.ts +17 -0
- package/src/shaders/uniforms.ts +18 -0
- package/src/utils/backgroundRuntime.ts +10 -0
- package/src/utils/colors.ts +117 -0
- package/src/utils/initWebGPU.ts +47 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
import type { ColorInput } from '../utils/colors';
|
|
4
|
+
import ShaderView from './ShaderView';
|
|
5
|
+
|
|
6
|
+
type Props = ViewProps & {
|
|
7
|
+
/** The color tint for the iridescence effect. */
|
|
8
|
+
color?: ColorInput;
|
|
9
|
+
/** Animation speed multiplier. Default: 1.0 */
|
|
10
|
+
speed?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function Iridescence({
|
|
14
|
+
color = '#ffffff',
|
|
15
|
+
speed = 1.0,
|
|
16
|
+
...viewProps
|
|
17
|
+
}: Props) {
|
|
18
|
+
const colors = useMemo(() => [color], [color]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<ShaderView
|
|
22
|
+
fragmentShader={IRIDESCENCE_SHADER}
|
|
23
|
+
colors={colors}
|
|
24
|
+
params={[]}
|
|
25
|
+
speed={speed}
|
|
26
|
+
isStatic={false}
|
|
27
|
+
{...viewProps}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const IRIDESCENCE_SHADER = /* wgsl */ `
|
|
33
|
+
struct Uniforms {
|
|
34
|
+
resolution: vec4<f32>,
|
|
35
|
+
time: vec4<f32>,
|
|
36
|
+
color0: vec4<f32>,
|
|
37
|
+
color1: vec4<f32>,
|
|
38
|
+
params0: vec4<f32>,
|
|
39
|
+
params1: vec4<f32>,
|
|
40
|
+
};
|
|
41
|
+
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
42
|
+
|
|
43
|
+
@fragment
|
|
44
|
+
fn main(@location(0) ndc: vec2<f32>) -> @location(0) vec4<f32> {
|
|
45
|
+
let time = u.time.x;
|
|
46
|
+
|
|
47
|
+
let vUv = ndc * 0.5 + vec2<f32>(0.5, 0.5);
|
|
48
|
+
let mr = min(u.resolution.x, u.resolution.y);
|
|
49
|
+
var uv = (vUv * 2.0 - vec2<f32>(1.0, 1.0)) * (u.resolution.xy / mr);
|
|
50
|
+
|
|
51
|
+
var d = -time * 0.5;
|
|
52
|
+
var a = 0.0;
|
|
53
|
+
for (var i: f32 = 0.0; i < 8.0; i = i + 1.0) {
|
|
54
|
+
a = a + cos(i - d - a * uv.x);
|
|
55
|
+
d = d + sin(uv.y * i + a);
|
|
56
|
+
}
|
|
57
|
+
d = d + time * 0.5;
|
|
58
|
+
|
|
59
|
+
let c1 = cos(uv * vec2<f32>(d, a)) * 0.6 + 0.4;
|
|
60
|
+
let c2 = cos(a + d) * 0.5 + 0.5;
|
|
61
|
+
let col = vec3<f32>(c1.x, c1.y, c2);
|
|
62
|
+
|
|
63
|
+
let finalCol = cos(col * cos(vec3<f32>(d, a, 2.5)) * 0.5 + 0.5);
|
|
64
|
+
let coloredCol = finalCol * u.color0.rgb;
|
|
65
|
+
return vec4<f32>(clamp(coloredCol, vec3<f32>(0.0), vec3<f32>(1.0)), u.color0.a);
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
import type { ColorInput } from '../utils/colors';
|
|
4
|
+
import ShaderView from './ShaderView';
|
|
5
|
+
|
|
6
|
+
type Props = ViewProps & {
|
|
7
|
+
/** The color of the start of the gradient. */
|
|
8
|
+
startColor: ColorInput;
|
|
9
|
+
/** The color of the end of the gradient. */
|
|
10
|
+
endColor: ColorInput;
|
|
11
|
+
/** The angle of the gradient in degrees (0-360). */
|
|
12
|
+
angle: number;
|
|
13
|
+
/** Rotation speed in degrees/second. 0 = static. Default: 0 */
|
|
14
|
+
speed?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default function LinearGradient({
|
|
18
|
+
startColor,
|
|
19
|
+
endColor,
|
|
20
|
+
angle,
|
|
21
|
+
speed = 0,
|
|
22
|
+
...viewProps
|
|
23
|
+
}: Props) {
|
|
24
|
+
const colors = useMemo(() => [startColor, endColor], [startColor, endColor]);
|
|
25
|
+
const params = useMemo(() => [angle, speed], [angle, speed]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<ShaderView
|
|
29
|
+
fragmentShader={LINEAR_GRADIENT_SHADER}
|
|
30
|
+
colors={colors}
|
|
31
|
+
params={params}
|
|
32
|
+
speed={speed === 0 ? 0 : 1}
|
|
33
|
+
isStatic={speed === 0}
|
|
34
|
+
{...viewProps}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const LINEAR_GRADIENT_SHADER = /* wgsl */ `
|
|
40
|
+
struct Uniforms {
|
|
41
|
+
resolution: vec4<f32>,
|
|
42
|
+
time: vec4<f32>,
|
|
43
|
+
color0: vec4<f32>,
|
|
44
|
+
color1: vec4<f32>,
|
|
45
|
+
params0: vec4<f32>,
|
|
46
|
+
params1: vec4<f32>,
|
|
47
|
+
};
|
|
48
|
+
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
49
|
+
|
|
50
|
+
@fragment
|
|
51
|
+
fn main(@location(0) ndc: vec2<f32>) -> @location(0) vec4<f32> {
|
|
52
|
+
let uv = ndc * 0.5 + vec2<f32>(0.5, 0.5);
|
|
53
|
+
let baseAngle = u.params0.x;
|
|
54
|
+
let rotationSpeed = u.params0.y;
|
|
55
|
+
let angle = (baseAngle + u.time.x * rotationSpeed) * 3.14159265359 / 180.0;
|
|
56
|
+
let dir = vec2<f32>(cos(angle), sin(angle));
|
|
57
|
+
let fromCenter = uv - vec2<f32>(0.5, 0.5);
|
|
58
|
+
let corrected = vec2<f32>(fromCenter.x * u.resolution.z, fromCenter.y);
|
|
59
|
+
let t = clamp(dot(corrected, dir) + 0.5, 0.0, 1.0);
|
|
60
|
+
return mix(u.color0, u.color1, t);
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
import type { ColorInput } from '../utils/colors';
|
|
4
|
+
import ShaderView from './ShaderView';
|
|
5
|
+
|
|
6
|
+
type Props = ViewProps & {
|
|
7
|
+
/** The base color for the liquid chrome effect. */
|
|
8
|
+
color?: ColorInput;
|
|
9
|
+
/** Animation speed multiplier. Default: 0.2 */
|
|
10
|
+
speed?: number;
|
|
11
|
+
/** Amplitude of the distortion. Default: 0.3 */
|
|
12
|
+
amplitude?: number;
|
|
13
|
+
/** Horizontal frequency. Default: 3 */
|
|
14
|
+
frequencyX?: number;
|
|
15
|
+
/** Vertical frequency. Default: 3 */
|
|
16
|
+
frequencyY?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function LiquidChrome({
|
|
20
|
+
color = '#1a1a1a',
|
|
21
|
+
speed = 0.2,
|
|
22
|
+
amplitude = 0.3,
|
|
23
|
+
frequencyX = 3,
|
|
24
|
+
frequencyY = 3,
|
|
25
|
+
...viewProps
|
|
26
|
+
}: Props) {
|
|
27
|
+
const colors = useMemo(() => [color], [color]);
|
|
28
|
+
const params = useMemo(
|
|
29
|
+
() => [amplitude, frequencyX, frequencyY],
|
|
30
|
+
[amplitude, frequencyX, frequencyY]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<ShaderView
|
|
35
|
+
fragmentShader={LIQUID_CHROME_SHADER}
|
|
36
|
+
colors={colors}
|
|
37
|
+
params={params}
|
|
38
|
+
speed={speed}
|
|
39
|
+
isStatic={false}
|
|
40
|
+
{...viewProps}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const LIQUID_CHROME_SHADER = /* wgsl */ `
|
|
46
|
+
struct Uniforms {
|
|
47
|
+
resolution: vec4<f32>,
|
|
48
|
+
time: vec4<f32>,
|
|
49
|
+
color0: vec4<f32>,
|
|
50
|
+
color1: vec4<f32>,
|
|
51
|
+
params0: vec4<f32>,
|
|
52
|
+
params1: vec4<f32>,
|
|
53
|
+
};
|
|
54
|
+
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
55
|
+
|
|
56
|
+
fn renderImage(uvCoord: vec2<f32>) -> vec4<f32> {
|
|
57
|
+
let resolution2D = u.resolution.xy;
|
|
58
|
+
let time = u.time.x;
|
|
59
|
+
let amplitude = u.params0.x;
|
|
60
|
+
let frequencyX = u.params0.y;
|
|
61
|
+
let frequencyY = u.params0.z;
|
|
62
|
+
|
|
63
|
+
let fragCoord = uvCoord * resolution2D;
|
|
64
|
+
var uv = (2.0 * fragCoord - resolution2D) / min(u.resolution.x, u.resolution.y);
|
|
65
|
+
|
|
66
|
+
for (var i: f32 = 1.0; i < 10.0; i = i + 1.0) {
|
|
67
|
+
uv.x = uv.x + amplitude / i * cos(i * frequencyX * uv.y + time);
|
|
68
|
+
uv.y = uv.y + amplitude / i * cos(i * frequencyY * uv.x + time);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let baseColor = u.color0;
|
|
72
|
+
let color = baseColor.rgb / abs(sin(time - uv.y - uv.x));
|
|
73
|
+
return vec4<f32>(color, baseColor.a);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@fragment
|
|
77
|
+
fn main(@location(0) ndc: vec2<f32>) -> @location(0) vec4<f32> {
|
|
78
|
+
let vUv = ndc * 0.5 + vec2<f32>(0.5, 0.5);
|
|
79
|
+
let resolution2D = u.resolution.xy;
|
|
80
|
+
|
|
81
|
+
var col = vec4<f32>(0.0);
|
|
82
|
+
var samples = 0;
|
|
83
|
+
|
|
84
|
+
for (var i: i32 = -1; i <= 1; i = i + 1) {
|
|
85
|
+
for (var j: i32 = -1; j <= 1; j = j + 1) {
|
|
86
|
+
let offset = vec2<f32>(f32(i), f32(j)) * (1.0 / min(resolution2D.x, resolution2D.y));
|
|
87
|
+
col = col + renderImage(vUv + offset);
|
|
88
|
+
samples = samples + 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return col / f32(samples);
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { PixelRatio, StyleSheet } from 'react-native';
|
|
2
|
+
import { Canvas } from 'react-native-webgpu';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { createSynchronizable, scheduleOnRuntime } from 'react-native-worklets';
|
|
5
|
+
import { colorToVec4 } from '../../utils/colors';
|
|
6
|
+
import { useWGPUSetup } from '../../hooks/useWGPUSetup';
|
|
7
|
+
import { TRIANGLE_VERTEX_SHADER } from '../../shaders/TRIANGLE_VERTEX_SHADER';
|
|
8
|
+
import {
|
|
9
|
+
UNIFORM_BUFFER_SIZE,
|
|
10
|
+
UNIFORM_FLOAT_COUNT,
|
|
11
|
+
} from '../../shaders/uniforms';
|
|
12
|
+
import type { ShaderViewProps } from './types';
|
|
13
|
+
|
|
14
|
+
// Synchronizable layout: [c0r,c0g,c0b,c0a, c1r,c1g,c1b,c1a, speed, p0..p7, alive]
|
|
15
|
+
// Total: 4 + 4 + 1 + 8 + 1 = 18 floats
|
|
16
|
+
const SYNC_SIZE = 18;
|
|
17
|
+
const IDX_SPEED = 8;
|
|
18
|
+
const IDX_PARAMS = 9; // 9..16
|
|
19
|
+
const IDX_ALIVE = 17;
|
|
20
|
+
|
|
21
|
+
export default function ShaderView({
|
|
22
|
+
fragmentShader,
|
|
23
|
+
colors = [],
|
|
24
|
+
speed = 1.0,
|
|
25
|
+
params = [],
|
|
26
|
+
isStatic = false,
|
|
27
|
+
transparent = false,
|
|
28
|
+
paramsSynchronizable,
|
|
29
|
+
style,
|
|
30
|
+
...viewProps
|
|
31
|
+
}: ShaderViewProps) {
|
|
32
|
+
const { canvasRef, runtime, resources } = useWGPUSetup();
|
|
33
|
+
|
|
34
|
+
const propsSync = useRef(
|
|
35
|
+
createSynchronizable<Float64Array>(new Float64Array(SYNC_SIZE))
|
|
36
|
+
).current;
|
|
37
|
+
|
|
38
|
+
// Convert props to flat floats and push to synchronizable
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const data = new Float64Array(SYNC_SIZE);
|
|
41
|
+
|
|
42
|
+
// color0 (indices 0-3)
|
|
43
|
+
if (colors[0] !== undefined) {
|
|
44
|
+
const c0 = colorToVec4(colors[0]);
|
|
45
|
+
data[0] = c0.r;
|
|
46
|
+
data[1] = c0.g;
|
|
47
|
+
data[2] = c0.b;
|
|
48
|
+
data[3] = c0.a;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// color1 (indices 4-7)
|
|
52
|
+
if (colors[1] !== undefined) {
|
|
53
|
+
const c1 = colorToVec4(colors[1]);
|
|
54
|
+
data[4] = c1.r;
|
|
55
|
+
data[5] = c1.g;
|
|
56
|
+
data[6] = c1.b;
|
|
57
|
+
data[7] = c1.a;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// speed
|
|
61
|
+
data[IDX_SPEED] = speed;
|
|
62
|
+
|
|
63
|
+
// params (indices 9-16)
|
|
64
|
+
for (let i = 0; i < 8; i++) {
|
|
65
|
+
data[IDX_PARAMS + i] = params[i] ?? 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// alive
|
|
69
|
+
data[IDX_ALIVE] = 1;
|
|
70
|
+
|
|
71
|
+
propsSync.setBlocking(() => data);
|
|
72
|
+
}, [colors, speed, params, propsSync]);
|
|
73
|
+
|
|
74
|
+
// Signal cleanup on unmount
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
return () => {
|
|
77
|
+
propsSync.setBlocking((prev) => {
|
|
78
|
+
prev[IDX_ALIVE] = 0;
|
|
79
|
+
return prev;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
}, [propsSync]);
|
|
83
|
+
|
|
84
|
+
// Start render loop when GPU resources are ready
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!resources) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { device, context, presentationFormat } = resources;
|
|
91
|
+
const dpr = PixelRatio.get();
|
|
92
|
+
|
|
93
|
+
// Per-run cancellation token. On Fast Refresh / dep change / unmount, React
|
|
94
|
+
// runs this effect's cleanup, which flips the flag and stops *this* loop —
|
|
95
|
+
// otherwise the old worklet RAF loop keeps running forever alongside the new
|
|
96
|
+
// one, stacking a duplicate render loop on every Metro reload.
|
|
97
|
+
const cancelled = createSynchronizable<Float64Array>(new Float64Array(1));
|
|
98
|
+
|
|
99
|
+
scheduleOnRuntime(runtime, () => {
|
|
100
|
+
'worklet';
|
|
101
|
+
|
|
102
|
+
// Create pipeline once
|
|
103
|
+
const pipeline = device.createRenderPipeline({
|
|
104
|
+
layout: 'auto',
|
|
105
|
+
vertex: {
|
|
106
|
+
module: device.createShaderModule({ code: TRIANGLE_VERTEX_SHADER }),
|
|
107
|
+
entryPoint: 'main',
|
|
108
|
+
},
|
|
109
|
+
fragment: {
|
|
110
|
+
module: device.createShaderModule({ code: fragmentShader }),
|
|
111
|
+
entryPoint: 'main',
|
|
112
|
+
targets: [{ format: presentationFormat }],
|
|
113
|
+
},
|
|
114
|
+
primitive: { topology: 'triangle-list' },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Create uniform buffer + bind group once, reuse via writeBuffer
|
|
118
|
+
const uniformBuffer = device.createBuffer({
|
|
119
|
+
size: UNIFORM_BUFFER_SIZE,
|
|
120
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const bindGroup = device.createBindGroup({
|
|
124
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
125
|
+
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const uniformData = new Float32Array(UNIFORM_FLOAT_COUNT);
|
|
129
|
+
let accumulatedTime = 0;
|
|
130
|
+
let lastTimestamp = 0;
|
|
131
|
+
|
|
132
|
+
function render(timestamp: number) {
|
|
133
|
+
const props = propsSync.getDirty();
|
|
134
|
+
if (props[IDX_ALIVE] === 0) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// This loop was superseded (Fast Refresh / unmount) — bail without
|
|
139
|
+
// scheduling another frame so it can be garbage-collected.
|
|
140
|
+
if (cancelled.getDirty()[0] === 1) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Compute dt
|
|
145
|
+
const dt = lastTimestamp === 0 ? 0 : (timestamp - lastTimestamp) / 1000;
|
|
146
|
+
lastTimestamp = timestamp;
|
|
147
|
+
|
|
148
|
+
// Accumulate time with speed
|
|
149
|
+
const currentSpeed = props[IDX_SPEED]!;
|
|
150
|
+
accumulatedTime += dt * currentSpeed;
|
|
151
|
+
|
|
152
|
+
// Resolution
|
|
153
|
+
const canvas = context.canvas as typeof context.canvas & {
|
|
154
|
+
width: number;
|
|
155
|
+
height: number;
|
|
156
|
+
};
|
|
157
|
+
const width = canvas.width || 1;
|
|
158
|
+
const height = canvas.height || 1;
|
|
159
|
+
const aspect = width / height;
|
|
160
|
+
|
|
161
|
+
// Fill uniform data (7 × vec4 = 28 floats)
|
|
162
|
+
// resolution: vec4<f32>
|
|
163
|
+
uniformData[0] = width;
|
|
164
|
+
uniformData[1] = height;
|
|
165
|
+
uniformData[2] = aspect;
|
|
166
|
+
uniformData[3] = dpr;
|
|
167
|
+
|
|
168
|
+
// time: vec4<f32>
|
|
169
|
+
uniformData[4] = accumulatedTime;
|
|
170
|
+
uniformData[5] = dt;
|
|
171
|
+
uniformData[6] = 0;
|
|
172
|
+
uniformData[7] = 0;
|
|
173
|
+
|
|
174
|
+
// color0: vec4<f32>
|
|
175
|
+
uniformData[8] = props[0]!;
|
|
176
|
+
uniformData[9] = props[1]!;
|
|
177
|
+
uniformData[10] = props[2]!;
|
|
178
|
+
uniformData[11] = props[3]!;
|
|
179
|
+
|
|
180
|
+
// color1: vec4<f32>
|
|
181
|
+
uniformData[12] = props[4]!;
|
|
182
|
+
uniformData[13] = props[5]!;
|
|
183
|
+
uniformData[14] = props[6]!;
|
|
184
|
+
uniformData[15] = props[7]!;
|
|
185
|
+
|
|
186
|
+
// params0: vec4<f32>
|
|
187
|
+
uniformData[16] = props[IDX_PARAMS]!;
|
|
188
|
+
uniformData[17] = props[IDX_PARAMS + 1]!;
|
|
189
|
+
uniformData[18] = props[IDX_PARAMS + 2]!;
|
|
190
|
+
uniformData[19] = props[IDX_PARAMS + 3]!;
|
|
191
|
+
|
|
192
|
+
// params1: vec4<f32> — static params[4..7]
|
|
193
|
+
uniformData[20] = props[IDX_PARAMS + 4]!;
|
|
194
|
+
uniformData[21] = props[IDX_PARAMS + 5]!;
|
|
195
|
+
uniformData[22] = props[IDX_PARAMS + 6]!;
|
|
196
|
+
uniformData[23] = props[IDX_PARAMS + 7]!;
|
|
197
|
+
|
|
198
|
+
// live: vec4<f32> — off-thread input (touch/scroll/audio) from
|
|
199
|
+
// paramsSynchronizable, written into its own slot so it never collides
|
|
200
|
+
// with the static params. Stays (0,0,0,0) when no channel is attached.
|
|
201
|
+
if (paramsSynchronizable) {
|
|
202
|
+
const live = paramsSynchronizable.getDirty();
|
|
203
|
+
uniformData[24] = live[0]!;
|
|
204
|
+
uniformData[25] = live[1]!;
|
|
205
|
+
uniformData[26] = live[2]!;
|
|
206
|
+
uniformData[27] = live[3]!;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
device.queue.writeBuffer(uniformBuffer, 0, uniformData);
|
|
210
|
+
|
|
211
|
+
const commandEncoder = device.createCommandEncoder();
|
|
212
|
+
const textureView = context.getCurrentTexture().createView();
|
|
213
|
+
const passEncoder = commandEncoder.beginRenderPass({
|
|
214
|
+
colorAttachments: [
|
|
215
|
+
{
|
|
216
|
+
view: textureView,
|
|
217
|
+
clearValue: transparent ? [0, 0, 0, 0] : [0, 0, 0, 1],
|
|
218
|
+
loadOp: 'clear',
|
|
219
|
+
storeOp: 'store',
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
passEncoder.setPipeline(pipeline);
|
|
225
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
226
|
+
passEncoder.draw(3);
|
|
227
|
+
passEncoder.end();
|
|
228
|
+
|
|
229
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
230
|
+
context.present();
|
|
231
|
+
|
|
232
|
+
if (!isStatic) {
|
|
233
|
+
requestAnimationFrame(render);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
requestAnimationFrame(render);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return () => {
|
|
241
|
+
cancelled.setBlocking(() => Float64Array.of(1));
|
|
242
|
+
};
|
|
243
|
+
}, [
|
|
244
|
+
resources,
|
|
245
|
+
runtime,
|
|
246
|
+
propsSync,
|
|
247
|
+
paramsSynchronizable,
|
|
248
|
+
fragmentShader,
|
|
249
|
+
isStatic,
|
|
250
|
+
transparent,
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<Canvas ref={canvasRef} style={[styles.canvas, style]} {...viewProps} />
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const styles = StyleSheet.create({
|
|
259
|
+
canvas: {
|
|
260
|
+
flex: 1,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ViewProps } from 'react-native';
|
|
2
|
+
import type { Synchronizable } from 'react-native-worklets';
|
|
3
|
+
import type { ColorInput } from '../../utils/colors';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A 4-float synchronizable whose values are written into the dedicated `u.live`
|
|
7
|
+
* uniform slot every frame. It has its own slot, so it never collides with the
|
|
8
|
+
* 8 static `params` (`u.params0`/`u.params1`).
|
|
9
|
+
*
|
|
10
|
+
* This is the bridge for live, per-frame input (touch position, scroll
|
|
11
|
+
* progress, velocity) coming from the JS thread into the off-thread render
|
|
12
|
+
* loop. Create one with `useParamsSynchronizable` and update it from
|
|
13
|
+
* gesture/scroll handlers. See `ShaderViewWithPanGesture`.
|
|
14
|
+
*/
|
|
15
|
+
export type ParamsSynchronizable = Synchronizable<Float64Array>;
|
|
16
|
+
|
|
17
|
+
export type ShaderViewProps = ViewProps & {
|
|
18
|
+
/** WGSL fragment shader source (must declare the Uniforms struct) */
|
|
19
|
+
fragmentShader: string;
|
|
20
|
+
/** Array of colors mapped to u.color0, u.color1 (max 2). Default: [] */
|
|
21
|
+
colors?: ColorInput[];
|
|
22
|
+
/** Time multiplier — controls animation speed. Default: 1.0 */
|
|
23
|
+
speed?: number;
|
|
24
|
+
/** Up to 8 shader-specific floats mapped to u.params0.xyzw and u.params1.xyzw */
|
|
25
|
+
params?: number[];
|
|
26
|
+
/** Render once then stop the RAF loop. Default: false */
|
|
27
|
+
isStatic?: boolean;
|
|
28
|
+
/** Use transparent background (clear to alpha 0). Default: false */
|
|
29
|
+
transparent?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Optional live input. Its 4 floats are written into the dedicated `u.live`
|
|
32
|
+
* slot every frame — independent of the static `params`. Use for
|
|
33
|
+
* touch/scroll/audio. Create it with `useParamsSynchronizable`.
|
|
34
|
+
*/
|
|
35
|
+
paramsSynchronizable?: ParamsSynchronizable;
|
|
36
|
+
};
|