xref: /expo/packages/expo-gl/build/GLView.web.js (revision 9b26454e)
1import { CodedError, Platform, UnavailabilityError } from 'expo-modules-core';
2import invariant from 'invariant';
3import * as React from 'react';
4import { Dimensions } from 'react-native';
5import Canvas from './Canvas';
6function getImageForAsset(asset) {
7    if (asset != null && typeof asset === 'object' && asset.downloadAsync) {
8        const dataURI = asset.localUri || asset.uri || '';
9        const image = new Image();
10        image.src = dataURI;
11        return image;
12    }
13    return asset;
14}
15function isOffscreenCanvas(element) {
16    return element && typeof element.convertToBlob === 'function';
17}
18function asExpoContext(gl) {
19    gl.endFrameEXP = function glEndFrameEXP() { };
20    if (!gl['_expo_texImage2D']) {
21        gl['_expo_texImage2D'] = gl.texImage2D;
22        gl.texImage2D = (...props) => {
23            const nextProps = [...props];
24            nextProps.push(getImageForAsset(nextProps.pop()));
25            return gl['_expo_texImage2D'](...nextProps);
26        };
27    }
28    if (!gl['_expo_texSubImage2D']) {
29        gl['_expo_texSubImage2D'] = gl.texSubImage2D;
30        gl.texSubImage2D = (...props) => {
31            const nextProps = [...props];
32            nextProps.push(getImageForAsset(nextProps.pop()));
33            return gl['_expo_texSubImage2D'](...nextProps);
34        };
35    }
36    return gl;
37}
38function ensureContext(canvas, contextAttributes) {
39    if (!canvas) {
40        throw new CodedError('ERR_GL_INVALID', 'Attempting to use the GL context before it has been created.');
41    }
42    // Apple disables WebGL 2.0 and doesn't provide any way to detect if it's disabled.
43    const isIOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
44    const context = (!isIOS && canvas.getContext('webgl2', contextAttributes)) ||
45        canvas.getContext('webgl', contextAttributes) ||
46        canvas.getContext('webgl-experimental', contextAttributes) ||
47        canvas.getContext('experimental-webgl', contextAttributes);
48    invariant(context, 'Browser does not support WebGL');
49    return asExpoContext(context);
50}
51async function getBlobFromWebGLRenderingContext(gl, options = {}) {
52    invariant(gl, 'getBlobFromWebGLRenderingContext(): WebGL Rendering Context is not defined');
53    const { canvas } = gl;
54    let blob = null;
55    if (typeof canvas.msToBlob === 'function') {
56        // @ts-ignore: polyfill: https://stackoverflow.com/a/29815058/4047926
57        blob = await canvas.msToBlob();
58    }
59    else if (isOffscreenCanvas(canvas)) {
60        blob = await canvas.convertToBlob({ quality: options.compress, type: options.format });
61    }
62    else {
63        blob = await new Promise((resolve) => {
64            canvas.toBlob((blob) => resolve(blob), options.format, options.compress);
65        });
66    }
67    return {
68        blob,
69        width: canvas.width,
70        height: canvas.height,
71    };
72}
73export class GLView extends React.Component {
74    canvas;
75    gl;
76    static async createContextAsync() {
77        if (!Platform.isDOMAvailable) {
78            return null;
79        }
80        const canvas = document.createElement('canvas');
81        const { width, height, scale } = Dimensions.get('window');
82        canvas.width = width * scale;
83        canvas.height = height * scale;
84        return ensureContext(canvas);
85    }
86    static async destroyContextAsync(exgl) {
87        // Do nothing
88        return true;
89    }
90    static async takeSnapshotAsync(gl, options = {}) {
91        const { blob, width, height } = await getBlobFromWebGLRenderingContext(gl, options);
92        if (!blob) {
93            throw new CodedError('ERR_GL_SNAPSHOT', 'Failed to save the GL context');
94        }
95        return {
96            uri: blob,
97            localUri: '',
98            width,
99            height,
100        };
101    }
102    componentWillUnmount() {
103        if (this.gl) {
104            const loseContextExt = this.gl.getExtension('WEBGL_lose_context');
105            if (loseContextExt) {
106                loseContextExt.loseContext();
107            }
108            this.gl = undefined;
109        }
110        if (this.canvas) {
111            this.canvas.removeEventListener('webglcontextlost', this.onContextLost);
112            this.canvas.removeEventListener('webglcontextrestored', this.onContextRestored);
113        }
114    }
115    render() {
116        const { onContextCreate, onContextRestored, onContextLost, webglContextAttributes, msaaSamples, nativeRef_EXPERIMENTAL,
117        // @ts-ignore: ref does not exist
118        ref, ...domProps } = this.props;
119        return React.createElement(Canvas, { ...domProps, canvasRef: this.setCanvasRef });
120    }
121    componentDidUpdate(prevProps) {
122        const { webglContextAttributes } = this.props;
123        if (this.canvas && webglContextAttributes !== prevProps.webglContextAttributes) {
124            this.onContextLost(null);
125            this.onContextRestored();
126        }
127    }
128    getGLContextOrReject() {
129        const gl = this.getGLContext();
130        if (!gl) {
131            throw new CodedError('ERR_GL_INVALID', 'Attempting to use the GL context before it has been created.');
132        }
133        return gl;
134    }
135    onContextLost = (event) => {
136        if (event && event.preventDefault) {
137            event.preventDefault();
138        }
139        this.gl = undefined;
140        if (typeof this.props.onContextLost === 'function') {
141            this.props.onContextLost();
142        }
143    };
144    onContextRestored = () => {
145        this.gl = undefined;
146        if (this.getGLContext() == null) {
147            throw new CodedError('ERR_GL_INVALID', 'Failed to restore GL context.');
148        }
149    };
150    getGLContext() {
151        if (this.gl)
152            return this.gl;
153        if (this.canvas) {
154            this.gl = ensureContext(this.canvas, this.props.webglContextAttributes);
155            if (typeof this.props.onContextCreate === 'function') {
156                this.props.onContextCreate(this.gl);
157            }
158            return this.gl;
159        }
160        return null;
161    }
162    setCanvasRef = (canvas) => {
163        this.canvas = canvas;
164        if (typeof this.props.nativeRef_EXPERIMENTAL === 'function') {
165            this.props.nativeRef_EXPERIMENTAL(canvas);
166        }
167        if (this.canvas) {
168            this.canvas.addEventListener('webglcontextlost', this.onContextLost);
169            this.canvas.addEventListener('webglcontextrestored', this.onContextRestored);
170            this.getGLContext();
171        }
172    };
173    async takeSnapshotAsync(options = {}) {
174        if (!GLView.takeSnapshotAsync) {
175            throw new UnavailabilityError('expo-gl', 'takeSnapshotAsync');
176        }
177        const gl = this.getGLContextOrReject();
178        return await GLView.takeSnapshotAsync(gl, options);
179    }
180    async startARSessionAsync() {
181        throw new UnavailabilityError('GLView', 'startARSessionAsync');
182    }
183    async createCameraTextureAsync() {
184        throw new UnavailabilityError('GLView', 'createCameraTextureAsync');
185    }
186    async destroyObjectAsync(glObject) {
187        throw new UnavailabilityError('GLView', 'destroyObjectAsync');
188    }
189}
190//# sourceMappingURL=GLView.web.js.map