1 | import invariant from 'invariant';
|
2 | import PropTypes from 'prop-types';
|
3 | import React from 'react';
|
4 | import { StyleSheet } from 'react-native';
|
5 | import { UnavailabilityError, CodedError } from '@unimodules/core';
|
6 | import {
|
7 | BaseGLViewProps,
|
8 | GLSnapshot,
|
9 | ExpoWebGLRenderingContext,
|
10 | SnapshotOptions,
|
11 | } from './GLView.types';
|
12 | export { BaseGLViewProps, ExpoWebGLRenderingContext, SnapshotOptions, GLViewProps };
|
13 |
|
14 | declare const window: Window;
|
15 |
|
16 | function getImageForAsset(asset: {
|
17 | downloadAsync: () => Promise<any>;
|
18 | uri?: string;
|
19 | localUri?: string;
|
20 | }): HTMLImageElement | any {
|
21 | if (asset != null && typeof asset === 'object' && asset !== null && asset.downloadAsync) {
|
22 | const dataURI = asset.localUri || asset.uri || '';
|
23 | const image = new Image();
|
24 | image.src = dataURI;
|
25 | return image;
|
26 | }
|
27 | return asset;
|
28 | }
|
29 |
|
30 | function asExpoContext(gl: ExpoWebGLRenderingContext): WebGLRenderingContext {
|
31 | gl.endFrameEXP = function glEndFrameEXP(): void {};
|
32 |
|
33 | if (!gl['_expo_texImage2D']) {
|
34 | gl['_expo_texImage2D'] = gl.texImage2D;
|
35 | gl.texImage2D = (...props: any[]): any => {
|
36 | let nextProps = [...props];
|
37 | nextProps.push(getImageForAsset(nextProps.pop()));
|
38 | return gl['_expo_texImage2D'](...nextProps);
|
39 | };
|
40 | }
|
41 |
|
42 | if (!gl['_expo_texSubImage2D']) {
|
43 | gl['_expo_texSubImage2D'] = gl.texSubImage2D;
|
44 | gl.texSubImage2D = (...props: any[]): any => {
|
45 | let nextProps = [...props];
|
46 | nextProps.push(getImageForAsset(nextProps.pop()));
|
47 | return gl['_expo_texSubImage2D'](...nextProps);
|
48 | };
|
49 | }
|
50 |
|
51 | return gl;
|
52 | }
|
53 |
|
54 | function ensureContext(
|
55 | canvas?: HTMLCanvasElement,
|
56 | contextAttributes?: WebGLContextAttributes
|
57 | ): WebGLRenderingContext {
|
58 | if (!canvas) {
|
59 | throw new CodedError(
|
60 | 'ERR_GL_INVALID',
|
61 | 'Attempting to use the GL context before it has been created.'
|
62 | );
|
63 | }
|
64 | const context =
|
65 | canvas.getContext('webgl', contextAttributes) ||
|
66 | canvas.getContext('webgl-experimental', contextAttributes) ||
|
67 | canvas.getContext('experimental-webgl', contextAttributes);
|
68 | invariant(context, 'Browser does not support WebGL');
|
69 | return asExpoContext(context as ExpoWebGLRenderingContext);
|
70 | }
|
71 |
|
72 | function stripNonDOMProps(props: { [key: string]: any }): { [key: string]: any } {
|
73 | for (let k in propTypes) {
|
74 | if (k in props) {
|
75 | delete props[k];
|
76 | }
|
77 | }
|
78 | return props;
|
79 | }
|
80 |
|
81 | const propTypes = {
|
82 | onContextCreate: PropTypes.func.isRequired,
|
83 | onContextRestored: PropTypes.func,
|
84 | onContextLost: PropTypes.func,
|
85 | webglContextAttributes: PropTypes.object,
|
86 | };
|
87 |
|
88 | interface GLViewProps extends BaseGLViewProps {
|
89 | onContextCreate: (gl: WebGLRenderingContext) => void;
|
90 | onContextRestored?: (gl?: WebGLRenderingContext) => void;
|
91 | onContextLost?: () => void;
|
92 | webglContextAttributes?: WebGLContextAttributes;
|
93 | }
|
94 |
|
95 | type State = {
|
96 | width: number;
|
97 | height: number;
|
98 | };
|
99 |
|
100 | export class GLView extends React.Component<GLViewProps, State> {
|
101 | state = {
|
102 | width: 0,
|
103 | height: 0,
|
104 | };
|
105 |
|
106 | static propTypes = propTypes;
|
107 |
|
108 | _hasContextBeenCreated = false;
|
109 |
|
110 | _webglContextAttributes: WebGLContextAttributes | undefined;
|
111 |
|
112 | canvas: HTMLCanvasElement | undefined;
|
113 |
|
114 | container?: HTMLElement;
|
115 |
|
116 | gl?: WebGLRenderingContext;
|
117 |
|
118 | static async createContextAsync(): Promise<WebGLRenderingContext> {
|
119 | const canvas = document.createElement('canvas');
|
120 | canvas.width = window.innerWidth * window.devicePixelRatio;
|
121 | canvas.height = window.innerHeight * window.devicePixelRatio;
|
122 | return ensureContext(canvas);
|
123 | }
|
124 |
|
125 | static async destroyContextAsync(exgl?: WebGLRenderingContext | number): Promise<boolean> {
|
126 |
|
127 | return true;
|
128 | }
|
129 |
|
130 | static async takeSnapshotAsync(
|
131 | exgl: WebGLRenderingContext,
|
132 | options: SnapshotOptions = {}
|
133 | ): Promise<GLSnapshot> {
|
134 | invariant(exgl, 'GLView.takeSnapshotAsync(): canvas is not defined');
|
135 | const canvas: HTMLCanvasElement = exgl.canvas;
|
136 | return await new Promise(resolve => {
|
137 | canvas.toBlob(
|
138 | (blob: Blob | null) => {
|
139 |
|
140 | resolve({
|
141 | uri: blob,
|
142 | localUri: '',
|
143 | width: canvas.width,
|
144 | height: canvas.height,
|
145 | });
|
146 | },
|
147 | options.format,
|
148 | options.compress
|
149 | );
|
150 | });
|
151 | }
|
152 |
|
153 | componentDidMount() {
|
154 | if (window.addEventListener) {
|
155 | window.addEventListener('resize', this._updateLayout);
|
156 | }
|
157 | }
|
158 |
|
159 | _contextCreated = (): void => {
|
160 | this.gl = this._createContext();
|
161 | this.props.onContextCreate(this.gl);
|
162 | if (this.canvas) {
|
163 | this.canvas.addEventListener('webglcontextlost', this._contextLost);
|
164 | this.canvas.addEventListener('webglcontextrestored', this._contextRestored);
|
165 | }
|
166 | };
|
167 |
|
168 | componentWillUnmount() {
|
169 | if (this.gl) {
|
170 | const loseContextExt = this.gl.getExtension('WEBGL_lose_context');
|
171 | if (loseContextExt) {
|
172 | loseContextExt.loseContext();
|
173 | }
|
174 | this.gl = undefined;
|
175 | }
|
176 | if (this.canvas) {
|
177 | this.canvas.removeEventListener('webglcontextlost', this._contextLost);
|
178 | this.canvas.removeEventListener('webglcontextrestored', this._contextRestored);
|
179 | }
|
180 | window.removeEventListener('resize', this._updateLayout);
|
181 | }
|
182 |
|
183 | _updateLayout = (): void => {
|
184 | if (this.container) {
|
185 | const { clientWidth: width = 0, clientHeight: height = 0 } = this.container;
|
186 | this.setState({ width, height });
|
187 | }
|
188 | };
|
189 |
|
190 | render() {
|
191 | const { devicePixelRatio = 1 } = window;
|
192 | const { style, ...props } = this.props;
|
193 | const { width, height } = this.state;
|
194 | const domProps = stripNonDOMProps(props);
|
195 |
|
196 | const containerStyle: any = StyleSheet.flatten([{ flex: 1 }, style]);
|
197 | return (
|
198 | <div ref={this._assignContainerRef} style={containerStyle}>
|
199 | <canvas
|
200 | ref={this._assignCanvasRef}
|
201 | style={{ flex: 1, width, height }}
|
202 | width={width * devicePixelRatio}
|
203 | height={height * devicePixelRatio}
|
204 | {...domProps}
|
205 | />
|
206 | </div>
|
207 | );
|
208 | }
|
209 |
|
210 | componentDidUpdate() {
|
211 | if (this.canvas && !this._hasContextBeenCreated) {
|
212 | this._hasContextBeenCreated = true;
|
213 | this._contextCreated();
|
214 | }
|
215 | }
|
216 |
|
217 | _createContext(): WebGLRenderingContext {
|
218 | const { webglContextAttributes } = this.props;
|
219 | const gl = ensureContext(this.canvas, webglContextAttributes);
|
220 | this._webglContextAttributes = webglContextAttributes || {};
|
221 | return gl;
|
222 | }
|
223 |
|
224 | _getGlOrReject(): WebGLRenderingContext {
|
225 | if (!this.gl) {
|
226 | throw new CodedError(
|
227 | 'ERR_GL_INVALID',
|
228 | 'Attempting to use the GL context before it has been created.'
|
229 | );
|
230 | }
|
231 | return this.gl;
|
232 | }
|
233 |
|
234 | _contextLost = (event: Event): void => {
|
235 | event.preventDefault();
|
236 | this.gl = undefined;
|
237 | if (this.props.onContextLost) {
|
238 | this.props.onContextLost();
|
239 | }
|
240 | };
|
241 |
|
242 | _contextRestored = (): void => {
|
243 | if (this.props.onContextRestored) {
|
244 | this.gl = this._createContext();
|
245 | this.props.onContextRestored(this.gl);
|
246 | }
|
247 | };
|
248 |
|
249 | _assignCanvasRef = (canvas: HTMLCanvasElement): void => {
|
250 | this.canvas = canvas;
|
251 | };
|
252 |
|
253 | _assignContainerRef = (element: HTMLElement | null): void => {
|
254 | if (element) {
|
255 | this.container = element;
|
256 | } else {
|
257 | this.container = undefined;
|
258 | }
|
259 | this._updateLayout();
|
260 | };
|
261 |
|
262 | async takeSnapshotAsync(options: SnapshotOptions = {}): Promise<GLSnapshot> {
|
263 | if (!GLView.takeSnapshotAsync) {
|
264 | throw new UnavailabilityError('expo-gl', 'takeSnapshotAsync');
|
265 | }
|
266 |
|
267 | const gl = this._getGlOrReject();
|
268 |
|
269 | return await GLView.takeSnapshotAsync(gl, options);
|
270 | }
|
271 |
|
272 | async startARSessionAsync(): Promise<void> {
|
273 | throw new UnavailabilityError('GLView', 'startARSessionAsync');
|
274 | }
|
275 |
|
276 | async createCameraTextureAsync(): Promise<void> {
|
277 | throw new UnavailabilityError('GLView', 'createCameraTextureAsync');
|
278 | }
|
279 |
|
280 | async destroyObjectAsync(glObject: WebGLObject): Promise<void> {
|
281 | throw new UnavailabilityError('GLView', 'destroyObjectAsync');
|
282 | }
|
283 | }
|