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