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 gl = global.__EXGLContexts[String(contextid)];
50  // This sets drawing buffer size to physical pixel size.
51  // For example, our GL View size is 150x150 virtual pixels and PixelRatio.get() returns 3.
52  // Then, gl.drawingBufferWidth and gl.drawingBufferHeight will equal 450 physical pixels.
53  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
54  const vert = gl.createShader(gl.VERTEX_SHADER)!;
55  gl.shaderSource(vert, vertShader);
56  gl.compileShader(vert);
57
58  const frag = gl.createShader(gl.FRAGMENT_SHADER)!;
59  gl.shaderSource(frag, fragShader);
60  gl.compileShader(frag);
61
62  const program = gl.createProgram()!;
63  gl.attachShader(program, vert);
64  gl.attachShader(program, frag);
65  gl.linkProgram(program);
66  gl.useProgram(program);
67
68  const buffer = gl.createBuffer();
69  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
70  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
71  const positionAttrib = gl.getAttribLocation(program, 'a_position');
72  gl.enableVertexAttribArray(positionAttrib);
73  gl.vertexAttribPointer(positionAttrib, 2, gl.FLOAT, false, 0, 0);
74
75  const texture = gl.createTexture();
76  gl.activeTexture(gl.TEXTURE0);
77  gl.bindTexture(gl.TEXTURE_2D, texture);
78  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
79  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
80  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
81  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
82  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, asset as any);
83
84  const textureLocation = gl.getUniformLocation(program, 'u_texture');
85  const rotationLocation = gl.getUniformLocation(program, 'u_translate')!;
86
87  gl.clearColor(0, 0, 0, 0);
88  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
89
90  gl.uniform1i(textureLocation, 0);
91  return { rotationLocation, verticesLength: vertices.length };
92}
93
94interface ExpoGlHandlers<RenderContext> {
95  shouldRunOnUI?: boolean;
96  onInit(gl: ExpoWebGLRenderingContext): RenderContext;
97  onRender(gl: ExpoWebGLRenderingContext, ctx: RenderContext): void;
98}
99
100function useWorkletAwareGlContext<T>(
101  { onInit, onRender, shouldRunOnUI = !!(global as any)._WORKLET_RUNTIME }: ExpoGlHandlers<T>,
102  dependencies: unknown[] = []
103) {
104  const [gl, setGl] = useState<ExpoWebGLRenderingContext>();
105  const rafId = useSharedValue<number | null>(null);
106  const canceled = useSharedValue<boolean>(false);
107
108  useEffect(() => {
109    if (!gl) {
110      return;
111    }
112    if (shouldRunOnUI) {
113      runOnUI((glCtxId: number) => {
114        'worklet';
115        const workletGl = GLView.getWorkletContext(glCtxId)!;
116        const ctx = onInit(workletGl);
117        const renderer = () => {
118          'worklet';
119          if (canceled.value) {
120            return;
121          }
122          onRender(workletGl, ctx);
123          rafId.value = requestAnimationFrame(renderer);
124        };
125        renderer();
126      })(gl.contextId);
127    } else {
128      const ctx = onInit(gl);
129      const renderer = () => {
130        onRender(gl, ctx);
131        requestAnimationFrame(renderer);
132      };
133      renderer();
134    }
135    return () => {
136      if (shouldRunOnUI) {
137        canceled.value = true;
138      } else if (rafId.value !== null) {
139        cancelAnimationFrame(rafId.value);
140      }
141    };
142  }, [gl, ...dependencies]);
143  return (gl: ExpoWebGLRenderingContext) => {
144    setGl(gl);
145  };
146}
147
148export default function GLReanimated() {
149  const translation = {
150    x: useSharedValue(0),
151    y: useSharedValue(0),
152  };
153
154  const [assets] = useAssets([require('../../../assets/images/exponent-icon.png')]);
155
156  const gestureHandler = useAnimatedGestureHandler<
157    PanGestureHandlerGestureEvent,
158    AnimatedGHContext
159  >({
160    onStart: (_, ctx) => {
161      ctx.startX = translation.x.value;
162      ctx.startY = translation.y.value;
163    },
164    onActive: (event, ctx) => {
165      translation.x.value = ctx.startX + event.translationX;
166      translation.y.value = ctx.startY + event.translationY;
167    },
168    onEnd: (_) => {
169      translation.x.value = withSpring(0);
170      translation.y.value = withSpring(0);
171    },
172  });
173
174  const onContextCreate = useWorkletAwareGlContext<RenderContext>(
175    {
176      onInit: (gl: ExpoWebGLRenderingContext) => {
177        'worklet';
178        return initializeContext(gl, assets?.[0]!);
179      },
180      onRender: (
181        gl: ExpoWebGLRenderingContext,
182        { rotationLocation, verticesLength }: RenderContext
183      ) => {
184        'worklet';
185        gl.clearColor(0, 0, 0, 0);
186        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
187        gl.uniform2fv(rotationLocation, [
188          (translation.x.value * 2) / gl.drawingBufferWidth,
189          (translation.y.value * 2) / gl.drawingBufferHeight,
190        ]);
191        gl.drawArrays(gl.TRIANGLES, 0, verticesLength / 2);
192        gl.flush();
193        gl.flushEXP();
194        gl.endFrameEXP();
195      },
196    },
197    [assets?.[0]]
198  );
199
200  return (
201    <View style={styles.flex}>
202      <PanGestureHandler onGestureEvent={gestureHandler}>
203        <Animated.View style={styles.flex}>
204          {assets ? (
205            <GLView style={styles.flex} onContextCreate={onContextCreate} />
206          ) : (
207            <Text>Loading</Text>
208          )}
209        </Animated.View>
210      </PanGestureHandler>
211      <Text style={styles.text}>
212        {(global as any)._WORKLET_RUNTIME
213          ? 'Running on UI thread inside reanimated worklet'
214          : 'Running on main JS thread, unsupported version of reanimated'}
215      </Text>
216    </View>
217  );
218}
219
220GLReanimated.title = 'Reanimated worklets + gesture handler';
221
222const styles = StyleSheet.create({
223  flex: {
224    flex: 1,
225  },
226  text: {
227    padding: 20,
228    fontSize: 20,
229  },
230});
231