UNPKG

7.89 kBTypeScriptView Raw
1import invariant from 'invariant';
2import PropTypes from 'prop-types';
3import React from 'react';
4import { StyleSheet } from 'react-native';
5import { UnavailabilityError, CodedError } from '@unimodules/core';
6import {
7 BaseGLViewProps,
8 GLSnapshot,
9 ExpoWebGLRenderingContext,
10 SnapshotOptions,
11} from './GLView.types';
12export { BaseGLViewProps, ExpoWebGLRenderingContext, SnapshotOptions, GLViewProps };
13
14declare const window: Window;
15
16function 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
30function 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
54function 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
72function 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
81const propTypes = {
82 onContextCreate: PropTypes.func.isRequired,
83 onContextRestored: PropTypes.func,
84 onContextLost: PropTypes.func,
85 webglContextAttributes: PropTypes.object,
86};
87
88interface GLViewProps extends BaseGLViewProps {
89 onContextCreate: (gl: WebGLRenderingContext) => void;
90 onContextRestored?: (gl?: WebGLRenderingContext) => void;
91 onContextLost?: () => void;
92 webglContextAttributes?: WebGLContextAttributes;
93}
94
95type State = {
96 width: number;
97 height: number;
98};
99
100export 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 // Do nothing
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 // TODO: Bacon: Should we add data URI?
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}