1import { Asset, useAssets } from 'expo-asset';
2import { ExpoWebGLRenderingContext, GLView } from 'expo-gl';
3import React, { useState, useEffect } from 'react';
4import { Text, StyleSheet, View } from 'react-native';
5import { PanGestureHandler, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
6import Animated, {
7  runOnUI,
8  useSharedValue,
9  useAnimatedGestureHandler,
10  withSpring,
11} from 'react-native-reanimated';
12
13interface RenderContext {
14  rotationLocation: WebGLUniformLocation;
15  verticesLength: number;
16}
17type AnimatedGHContext = {
18  startX: number;
19  startY: number;
20};
21
22function initializeContext(gl: ExpoWebGLRenderingContext, asset: Asset): RenderContext {
23  'worklet';
24  const vertShader = `
25  precision highp float;
26  uniform vec2 u_translate;
27  attribute vec2 a_position;
28  varying vec2 uv;
29  void main () {
30    vec2 translatedPosition = vec2(
31      (a_position.x - 0.5) * 0.5 + (u_translate.x * 2.0),
32      (a_position.y - 0.5) * 0.3 - (u_translate.y * (1.0 - a_position.y) * 2.0)
33    );
34
35    uv = vec2(1.0 - a_position.y,  1.0 - a_position.x);
36    gl_Position = vec4(translatedPosition, 0, 1);
37  }
38`;
39
40  const fragShader = `
41  precision highp float;
42  uniform sampler2D u_texture;
43  varying vec2 uv;
44  void main () {
45    gl_FragColor = texture2D(u_texture, vec2(uv.y, uv.x));
46  }
47`;
48  const vertices = new Float32Array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]);
49  const vert = gl.createShader(gl.VERTEX_SHADER)!;
50  gl.shaderSource(vert, vertShader);
51  gl.compileShader(vert);
52
53  const frag = gl.createShader(gl.FRAGMENT_SHADER)!;
54  gl.shaderSource(frag, fragShader);
55  gl.compileShader(frag);
56
57  const program = gl.createProgram()!;
58  gl.attachShader(program, vert);
59  gl.attachShader(program, frag);
60  gl.linkProgram(program);
61  gl.useProgram(program);
62
63  const buffer = gl.createBuffer();
64  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
65  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
66  const positionAttrib = gl.getAttribLocation(program, 'a_position');
67  gl.enableVertexAttribArray(positionAttrib);
68  gl.vertexAttribPointer(positionAttrib, 2, gl.FLOAT, false, 0, 0);
69
70  const texture = gl.createTexture();
71  gl.activeTexture(gl.TEXTURE0);
72  gl.bindTexture(gl.TEXTURE_2D, texture);
73  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
74  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
75  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
76  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
77  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, asset as any);
78
79  const textureLocation = gl.getUniformLocation(program, 'u_texture');
80  const rotationLocation = gl.getUniformLocation(program, 'u_translate')!;
81
82  gl.clearColor(0, 0, 0, 0);
83  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
84
85  gl.uniform1i(textureLocation, 0);
86  return { rotationLocation, verticesLength: vertices.length };
87}
88
89interface ExpoGlHandlers<RenderContext> {
90  shouldRunOnUI?: boolean;
91  onInit(gl: ExpoWebGLRenderingContext): RenderContext;
92  onRender(gl: ExpoWebGLRenderingContext, ctx: RenderContext): void;
93}
94
95function useWorkletAwareGlContext<T>(
96  { onInit, onRender, shouldRunOnUI = !!(globalThis as any)._WORKLET_RUNTIME }: ExpoGlHandlers<T>,
97  dependencies: unknown[] = []
98) {
99  const [gl, setGl] = useState<ExpoWebGLRenderingContext>();
100  const rafId = useSharedValue<number | null>(null);
101  const canceled = useSharedValue<boolean>(false);
102
103  useEffect(() => {
104    if (!gl) {
105      return;
106    }
107    if (shouldRunOnUI) {
108      runOnUI((glCtxId: number) => {
109        'worklet';
110        const workletGl = GLView.getWorkletContext(glCtxId)!;
111        const ctx = onInit(workletGl);
112        const renderer = () => {
113          'worklet';
114          if (canceled.value) {
115            return;
116          }
117          onRender(workletGl, ctx);
118          rafId.value = requestAnimationFrame(renderer);
119        };
120        renderer();
121      })(gl.contextId);
122    } else {
123      const ctx = onInit(gl);
124      const renderer = () => {
125        onRender(gl, ctx);
126        requestAnimationFrame(renderer);
127      };
128      renderer();
129    }
130    return () => {
131      if (shouldRunOnUI) {
132        canceled.value = true;
133      } else if (rafId.value !== null) {
134        cancelAnimationFrame(rafId.value);
135      }
136    };
137  }, [gl, ...dependencies]);
138  return (gl: ExpoWebGLRenderingContext) => {
139    setGl(gl);
140  };
141}
142
143export default function GLReanimated() {
144  const translation = {
145    x: useSharedValue(0),
146    y: useSharedValue(0),
147  };
148
149  const [assets] = useAssets([require('../../../assets/images/expo-icon.png')]);
150
151  const gestureHandler = useAnimatedGestureHandler<
152    PanGestureHandlerGestureEvent,
153    AnimatedGHContext
154  >({
155    onStart: (_, ctx) => {
156      ctx.startX = translation.x.value;
157      ctx.startY = translation.y.value;
158    },
159    onActive: (event, ctx) => {
160      translation.x.value = ctx.startX + event.translationX;
161      translation.y.value = ctx.startY + event.translationY;
162    },
163    onEnd: (_) => {
164      translation.x.value = withSpring(0);
165      translation.y.value = withSpring(0);
166    },
167  });
168
169  const onContextCreate = useWorkletAwareGlContext<RenderContext>(
170    {
171      onInit: (gl: ExpoWebGLRenderingContext) => {
172        'worklet';
173        return initializeContext(gl, assets?.[0]!);
174      },
175      onRender: (
176        gl: ExpoWebGLRenderingContext,
177        { rotationLocation, verticesLength }: RenderContext
178      ) => {
179        'worklet';
180        gl.clearColor(0, 0, 0, 0);
181        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
182        gl.uniform2fv(rotationLocation, [
183          (translation.x.value * 2) / gl.drawingBufferWidth,
184          (translation.y.value * 2) / gl.drawingBufferHeight,
185        ]);
186        gl.drawArrays(gl.TRIANGLES, 0, verticesLength / 2);
187        gl.flush();
188        gl.flushEXP();
189        gl.endFrameEXP();
190      },
191    },
192    [assets?.[0]]
193  );
194
195  return (
196    <View style={styles.flex}>
197      <PanGestureHandler onGestureEvent={gestureHandler}>
198        <Animated.View style={styles.flex}>
199          {assets ? (
200            <GLView style={styles.flex} onContextCreate={onContextCreate} />
201          ) : (
202            <Text>Loading</Text>
203          )}
204        </Animated.View>
205      </PanGestureHandler>
206      <Text style={styles.text}>
207        {(globalThis as any)._WORKLET_RUNTIME
208          ? 'Running on UI thread inside reanimated worklet'
209          : 'Running on main JS thread, unsupported version of reanimated'}
210      </Text>
211    </View>
212  );
213}
214
215GLReanimated.title = 'Reanimated worklets + gesture handler';
216
217const styles = StyleSheet.create({
218  flex: {
219    flex: 1,
220  },
221  text: {
222    padding: 20,
223    fontSize: 20,
224  },
225});
226