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