1 | import { CodedError, Platform, UnavailabilityError } from '@unimodules/core';
|
2 | import invariant from 'invariant';
|
3 | import * as React from 'react';
|
4 | import { Dimensions } from 'react-native';
|
5 |
|
6 | import Canvas from './Canvas';
|
7 | import {
|
8 | BaseGLViewProps,
|
9 | ComponentOrHandle,
|
10 | ExpoWebGLRenderingContext,
|
11 | GLSnapshot,
|
12 | SnapshotOptions,
|
13 | } from './GLView.types';
|
14 |
|
15 | function 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 |
|
29 | function isOffscreenCanvas(element: any): element is OffscreenCanvas {
|
30 | return element && typeof element.convertToBlob === 'function';
|
31 | }
|
32 |
|
33 | function 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 |
|
57 | function 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 |
|
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 |
|
80 | export interface GLViewProps extends BaseGLViewProps {
|
81 | onContextCreate: (gl: WebGLRenderingContext) => void;
|
82 | onContextRestored?: (gl?: WebGLRenderingContext) => void;
|
83 | onContextLost?: () => void;
|
84 | webglContextAttributes?: WebGLContextAttributes;
|
85 | |
86 |
|
87 |
|
88 | msaaSamples: number;
|
89 |
|
90 | |
91 |
|
92 |
|
93 | nativeRef_EXPERIMENTAL?(callback: ComponentOrHandle | HTMLCanvasElement | null);
|
94 | }
|
95 |
|
96 | async 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 |
|
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 |
|
124 | export 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 |
|
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 |
|
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 | }
|