UNPKG

8.1 kBTypeScriptView Raw
1import { CodedError, Platform, UnavailabilityError } from '@unimodules/core';
2import invariant from 'invariant';
3import * as React from 'react';
4import { Dimensions } from 'react-native';
5
6import Canvas from './Canvas';
7import {
8 BaseGLViewProps,
9 ComponentOrHandle,
10 ExpoWebGLRenderingContext,
11 GLSnapshot,
12 SnapshotOptions,
13} from './GLView.types';
14
15function getImageForAsset(asset: {
16 downloadAsync: () => Promise<any>;
17 uri?: string;
18 localUri?: string;
19}): HTMLImageElement | any {
20 if (asset != null && typeof asset === 'object' && asset !== null && asset.downloadAsync) {
21 const dataURI = asset.localUri || asset.uri || '';
22 const image = new Image();
23 image.src = dataURI;
24 return image;
25 }
26 return asset;
27}
28
29function isOffscreenCanvas(element: any): element is OffscreenCanvas {
30 return element && typeof element.convertToBlob === 'function';
31}
32
33function asExpoContext(gl: ExpoWebGLRenderingContext): WebGLRenderingContext {
34 gl.endFrameEXP = function glEndFrameEXP(): void {};
35
36 if (!gl['_expo_texImage2D']) {
37 gl['_expo_texImage2D'] = gl.texImage2D;
38 gl.texImage2D = (...props: any[]): any => {
39 const nextProps = [...props];
40 nextProps.push(getImageForAsset(nextProps.pop()));
41 return gl['_expo_texImage2D'](...nextProps);
42 };
43 }
44
45 if (!gl['_expo_texSubImage2D']) {
46 gl['_expo_texSubImage2D'] = gl.texSubImage2D;
47 gl.texSubImage2D = (...props: any[]): any => {
48 const nextProps = [...props];
49 nextProps.push(getImageForAsset(nextProps.pop()));
50 return gl['_expo_texSubImage2D'](...nextProps);
51 };
52 }
53
54 return gl;
55}
56
57function ensureContext(
58 canvas?: HTMLCanvasElement,
59 contextAttributes?: WebGLContextAttributes
60): WebGLRenderingContext {
61 if (!canvas) {
62 throw new CodedError(
63 'ERR_GL_INVALID',
64 'Attempting to use the GL context before it has been created.'
65 );
66 }
67
68 // Apple disables WebGL 2.0 and doesn't provide any way to detect if it's disabled.
69 const isIOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
70
71 const context =
72 (!isIOS && canvas.getContext('webgl2', contextAttributes)) ||
73 canvas.getContext('webgl', contextAttributes) ||
74 canvas.getContext('webgl-experimental', contextAttributes) ||
75 canvas.getContext('experimental-webgl', contextAttributes);
76 invariant(context, 'Browser does not support WebGL');
77 return asExpoContext(context as ExpoWebGLRenderingContext);
78}
79
80export interface GLViewProps extends BaseGLViewProps {
81 onContextCreate: (gl: WebGLRenderingContext) => void;
82 onContextRestored?: (gl?: WebGLRenderingContext) => void;
83 onContextLost?: () => void;
84 webglContextAttributes?: WebGLContextAttributes;
85 /**
86 * [iOS only] Number of samples for Apple's built-in multisampling.
87 */
88 msaaSamples: number;
89
90 /**
91 * A ref callback for the native GLView
92 */
93 nativeRef_EXPERIMENTAL?(callback: ComponentOrHandle | HTMLCanvasElement | null);
94}
95
96async function getBlobFromWebGLRenderingContext(
97 gl: WebGLRenderingContext,
98 options: SnapshotOptions = {}
99): Promise<{ width: number; height: number; blob: Blob | null }> {
100 invariant(gl, 'getBlobFromWebGLRenderingContext(): WebGL Rendering Context is not defined');
101
102 const { canvas } = gl;
103
104 let blob: Blob | null = null;
105
106 if (typeof (canvas as any).msToBlob === 'function') {
107 // @ts-ignore: polyfill: https://stackoverflow.com/a/29815058/4047926
108 blob = await canvas.msToBlob();
109 } else if (isOffscreenCanvas(canvas)) {
110 blob = await canvas.convertToBlob({ quality: options.compress, type: options.format });
111 } else {
112 blob = await new Promise(resolve => {
113 canvas.toBlob((blob: Blob | null) => resolve(blob), options.format, options.compress);
114 });
115 }
116
117 return {
118 blob,
119 width: canvas.width,
120 height: canvas.height,
121 };
122}
123
124export class GLView extends React.Component<GLViewProps> {
125 canvas?: HTMLCanvasElement;
126
127 gl?: WebGLRenderingContext;
128
129 static async createContextAsync(): Promise<WebGLRenderingContext | null> {
130 if (!Platform.isDOMAvailable) {
131 return null;
132 }
133 const canvas = document.createElement('canvas');
134 const { width, height, scale } = Dimensions.get('window');
135 canvas.width = width * scale;
136 canvas.height = height * scale;
137 return ensureContext(canvas);
138 }
139
140 static async destroyContextAsync(exgl?: WebGLRenderingContext | number): Promise<boolean> {
141 // Do nothing
142 return true;
143 }
144
145 static async takeSnapshotAsync(
146 gl: WebGLRenderingContext,
147 options: SnapshotOptions = {}
148 ): Promise<GLSnapshot> {
149 const { blob, width, height } = await getBlobFromWebGLRenderingContext(gl, options);
150
151 if (!blob) {
152 throw new CodedError('ERR_GL_SNAPSHOT', 'Failed to save the GL context');
153 }
154
155 return {
156 uri: blob,
157 localUri: '',
158 width,
159 height,
160 };
161 }
162
163 componentWillUnmount() {
164 if (this.gl) {
165 const loseContextExt = this.gl.getExtension('WEBGL_lose_context');
166 if (loseContextExt) {
167 loseContextExt.loseContext();
168 }
169 this.gl = undefined;
170 }
171 if (this.canvas) {
172 this.canvas.removeEventListener('webglcontextlost', this.onContextLost);
173 this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored);
174 }
175 }
176
177 render() {
178 const {
179 onContextCreate,
180 onContextRestored,
181 onContextLost,
182 webglContextAttributes,
183 msaaSamples,
184 nativeRef_EXPERIMENTAL,
185 // @ts-ignore: ref does not exist
186 ref,
187 ...domProps
188 } = this.props;
189
190 return <Canvas {...domProps} canvasRef={this.setCanvasRef} />;
191 }
192
193 componentDidUpdate(prevProps: GLViewProps) {
194 const { webglContextAttributes } = this.props;
195 if (this.canvas && webglContextAttributes !== prevProps.webglContextAttributes) {
196 this.onContextLost(null);
197 this.onContextRestored();
198 }
199 }
200
201 private getGLContextOrReject(): WebGLRenderingContext {
202 const gl = this.getGLContext();
203 if (!gl) {
204 throw new CodedError(
205 'ERR_GL_INVALID',
206 'Attempting to use the GL context before it has been created.'
207 );
208 }
209 return gl;
210 }
211
212 private onContextLost = (event: Event | null): void => {
213 if (event && event.preventDefault) {
214 event.preventDefault();
215 }
216 this.gl = undefined;
217
218 if (typeof this.props.onContextLost === 'function') {
219 this.props.onContextLost();
220 }
221 };
222
223 private onContextRestored = (): void => {
224 this.gl = undefined;
225 if (this.getGLContext() == null) {
226 throw new CodedError('ERR_GL_INVALID', 'Failed to restore GL context.');
227 }
228 };
229
230 private getGLContext(): WebGLRenderingContext | null {
231 if (this.gl) return this.gl;
232
233 if (this.canvas) {
234 this.gl = ensureContext(this.canvas, this.props.webglContextAttributes);
235 if (typeof this.props.onContextCreate === 'function') {
236 this.props.onContextCreate(this.gl);
237 }
238 return this.gl;
239 }
240 return null;
241 }
242
243 private setCanvasRef = (canvas: HTMLCanvasElement): void => {
244 this.canvas = canvas;
245
246 if (typeof this.props.nativeRef_EXPERIMENTAL === 'function') {
247 this.props.nativeRef_EXPERIMENTAL(canvas);
248 }
249
250 if (this.canvas) {
251 this.canvas.addEventListener('webglcontextlost', this.onContextLost);
252 this.canvas.addEventListener('webglcontextrestored', this.onContextRestored);
253
254 this.getGLContext();
255 }
256 };
257
258 public async takeSnapshotAsync(options: SnapshotOptions = {}): Promise<GLSnapshot> {
259 if (!GLView.takeSnapshotAsync) {
260 throw new UnavailabilityError('expo-gl', 'takeSnapshotAsync');
261 }
262
263 const gl = this.getGLContextOrReject();
264 return await GLView.takeSnapshotAsync(gl, options);
265 }
266
267 public async startARSessionAsync(): Promise<void> {
268 throw new UnavailabilityError('GLView', 'startARSessionAsync');
269 }
270
271 public async createCameraTextureAsync(): Promise<void> {
272 throw new UnavailabilityError('GLView', 'createCameraTextureAsync');
273 }
274
275 public async destroyObjectAsync(glObject: WebGLObject): Promise<void> {
276 throw new UnavailabilityError('GLView', 'destroyObjectAsync');
277 }
278}