UNPKG

gl-react

Version:

Universal React library, write and compose WebGL shaders, implement complex effects using a descriptive paradigm

615 lines (554 loc) 17.9 kB
//@flow import invariant from "invariant"; import React, { Component } from "react"; import PropTypes from "prop-types"; import createShader from "gl-shader"; import Bus from "./Bus"; import Shaders from "./Shaders"; import Visitors from "./Visitors"; import { LoaderResolver } from "webgltexture-loader"; import type { NDArray } from "ndarray"; import type { ShaderIdentifier, ShaderInfo } from "./Shaders"; import type { Shader } from "gl-shader"; import type { VisitorLike } from "./Visitor"; import type { WebGLTextureLoader } from "webgltexture-loader"; import type Node from "./Node"; const __DEV__ = process.env.NODE_ENV === "development"; type SurfaceProps = { children?: any, style?: Object, preload?: Array<mixed>, onLoad?: () => void, onLoadError?: (e: Error) => void, onContextLost?: () => void, onContextRestored?: () => void, visitor?: VisitorLike, }; interface ISurface extends Component<SurfaceProps, *> { props: SurfaceProps; gl: ?WebGLRenderingContext; RenderLessElement: React$ComponentType<*>; root: ?Node; id: number; +mapRenderableContent: ?(inst: mixed) => mixed; +getVisitors: () => Array<VisitorLike>; +getGLSize: () => [number, number]; +getGLName: () => string; +getGLShortName: () => string; +captureAsDataURL: (...args: any) => string; +captureAsBlob: (...args: any) => Promise<Blob>; +capture: (x?: number, y?: number, w?: number, h?: number) => NDArray; +redraw: () => void; +flush: () => void; +getEmptyTexture: () => WebGLTexture; +glIsAvailable: () => boolean; +rebootForDebug: () => void; +_addGLNodeChild: (node: Node) => void; +_removeGLNodeChild: (node: Node) => void; +_resolveTextureLoader: ( raw: any ) => { loader: ?WebGLTextureLoader<*>, input: mixed }; +_getShader: (shaderId: ShaderIdentifier) => Shader; +_makeShader: (shaderInfo: ShaderInfo) => Shader; +_draw: () => void; +_bindRootNode: () => void; } export type Surface = ISurface; export type SurfaceContext = { glParent: Node | Surface | Bus, glSurface: Surface, glSizable: { +getGLSize: () => [number, number] }, }; const SurfacePropTypes = { children: PropTypes.any.isRequired, style: PropTypes.any, preload: PropTypes.array, onLoad: PropTypes.func, onLoadError: PropTypes.func, onContextLost: PropTypes.func, onContextRestored: PropTypes.func, visitor: PropTypes.object, }; let surfaceId = 0; const _instances: Array<ISurface> = []; export const list = (): Array<ISurface> => _instances.slice(0); const allSurfaceProps = Object.keys(SurfacePropTypes); type SurfaceOpts = { GLView: *, RenderLessElement: React$ComponentType<*>, mapRenderableContent?: (instance: mixed) => mixed, requestFrame: (f: Function) => number, cancelFrame: (id: number) => void, }; export default ({ GLView, RenderLessElement, mapRenderableContent, requestFrame, cancelFrame, }: SurfaceOpts): Class<ISurface> => { /** * **Renders the final tree of [Node](#node) in a WebGL Canvas / OpenGLView /...** * * `<Surface>` performs the final GL draws for a given implementation. * * `width` and `height` props are required for `gl-react-dom` and `gl-react-headless`, but are not supported for React Native, where the paradigm is to use `style` (and either use flexbox or set a width/height from there). * * > Surface is the only component that isn't "universal", * therefore **Surface is exposed by the platform implementation** * (`gl-react-dom` / `gl-react-native` / ...), * unlike the rest of the API exposed through `gl-react`. * Each platform have its own implementation but most props are shared. * If you write a gl-react library, you shouldn't use `<Surface>` but only * let the final user doing it. Therefore your code should remain platform-independant. * * @class Surface * @extends Component * @prop {any} children - a tree of React Element that renders some [Node](#node) and/or [Bus](#bus). * @prop {number} [width] **(only for DOM)** - width of the Surface. multiplied by `pixelRatio` for the actual canvas pixel size. * @prop {number} [height] **(only for DOM)** - height of the Surface. multiplied by `pixelRatio` for the actual canvas pixel size. * @prop {object} [style] - CSS styles that get passed to the underlying `<canvas/>` or `<View/>` * @prop {Array<any>} [preload] - an array of things to preload before the Surface start rendering. Help avoiding blinks and providing required textures to render an initial state. * @prop {function} [onLoad] - a callback called when Surface is ready and just after it rendered. * @prop {function(error:Error):void} [onLoadError] - a callback called when the Surface was not able to load initially. * @prop {function} [onContextLost] - a callback called when the Surface context was lost. * @prop {function} [onContextRestored] - a callback called when the Surface was restored and ready. * @prop {Visitor} [visitor] - an internal visitor used for logs and tests. * * @prop {WebGLContextAttributes} [webglContextAttributes] **(gl-react-dom only)** a optional set of attributes to init WebGL with. * @prop {number} [pixelRatio=window.devicePixelRatio] **(gl-react-dom only)** allows to override the pixelRatio. (default `devicePixelRatio`) * * @example * * <Surface width={300} height={200}> * <Node shader={shaders.helloGL} /> * </Surface> * * @example * * <Surface width={200} height={100}> * <HelloGL /> * </Surface> * * @example * * <Surface width={200} height={100}> * <Blur factor={2}> * <Negative> * https://i.imgur.com/wxqlQkh.jpg * </Negative> * </Blur> * </Surface> */ return class Surface extends Component< SurfaceProps, { ready: boolean, rebootId: number, debug: boolean, } > { id: number = ++surfaceId; gl: ?WebGLRenderingContext; buffer: WebGLBuffer; loaderResolver: ?LoaderResolver; glView: *; root: ?Node; shaders: { [key: string]: Shader } = {}; _preparingGL: Array<*> = []; _needsRedraw: boolean = false; state = { ready: false, rebootId: 0, debug: false, }; RenderLessElement = RenderLessElement; mapRenderableContent = mapRenderableContent; static propTypes = SurfacePropTypes; static childContextTypes: { [_: $Keys<SurfaceContext>]: any } = { glSurface: PropTypes.object.isRequired, glParent: PropTypes.object.isRequired, glSizable: PropTypes.object.isRequired, }; getChildContext(): SurfaceContext { return { glParent: this, glSurface: this, glSizable: this, }; } componentDidMount() { _instances.push(this); this.getVisitors().forEach((v) => v.onSurfaceMount(this)); } componentWillUnmount() { this._stopLoop(); this._destroyGL(); const i = _instances.indexOf(this); if (i !== -1) _instances.splice(i, 1); this.getVisitors().forEach((v) => v.onSurfaceUnmount(this)); } componentDidUpdate() { this.redraw(); } render() { const { props, state: { ready, rebootId, debug }, } = this; const { children, style } = props; // We allow to pass-in all props we don't know so you can hook to DOM events. const rest = {}; Object.keys(props).forEach((key) => { if (allSurfaceProps.indexOf(key) === -1) { rest[key] = props[key]; } }); return ( <GLView key={rebootId} debug={debug} ref={this._onRef} onContextCreate={this._onContextCreate} onContextFailure={this._onContextFailure} onContextLost={this._onContextLost} onContextRestored={this._onContextRestored} style={style} {...rest} > {ready ? children : null} </GLView> ); } rebootForDebug() { // FIXME: there is a bug somewhere that breaks rendering if this is called at startup time. this._stopLoop(); this._destroyGL(); this.setState(({ rebootId }) => ({ rebootId: rebootId + 1, ready: false, debug: true, })); } getVisitors(): Array<VisitorLike> { return Visitors.get().concat(this.props.visitor || []); } getGLSize(): [number, number] { const { gl } = this; return [gl ? gl.drawingBufferWidth : 0, gl ? gl.drawingBufferHeight : 0]; } getGLName(): string { return `Surface#${this.id}`; } getGLShortName(): string { return "Surface"; } /** * see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL * @param {string} mimeType (optional) the image MimeType * @param {number} quality (optional) the image quality * @memberof Surface * @instance */ captureAsDataURL(...args: any): string { const { glView } = this; invariant(glView, "GLView is mounted"); invariant( glView.captureAsDataURL, "captureAsDataURL is not defined in %s", GLView.displayName || GLView.name ); return glView.captureAsDataURL(...args); } /** * see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob * @param {string} mimeType (optional) the image MimeType * @param {number} quality (optional) the image quality * @memberof Surface * @instance */ captureAsBlob(...args: any): Promise<Blob> { const { glView } = this; invariant(glView, "GLView is mounted"); invariant( glView.captureAsBlob, "captureAsBlob is not defined in %s", GLView.displayName || GLView.name ); return glView.captureAsBlob(...args); } /** * capture the root Node pixels. Make sure you have set `preserveDrawingBuffer: true` in `webglContextAttributes` prop. * @memberof Surface * @instance */ capture(x?: number, y?: number, w?: number, h?: number): NDArray { invariant( this.root, "Surface#capture: surface is not yet ready or don't have any root Node" ); return this.root.capture(x, y, w, h); } /** * Schedule a redraw of the Surface. * @memberof Surface * @instance * @function */ redraw = (): void => { this._needsRedraw = true; }; /** * Force the redraw (if any) to happen now, synchronously. * @memberof Surface * @instance * @function */ flush = (): void => { this._draw(); }; glIsAvailable(): boolean { return !!this.gl; } _emptyTexture: ?WebGLTexture; getEmptyTexture(): WebGLTexture { let { gl, _emptyTexture } = this; invariant(gl, "getEmptyTexture called while gl was not defined"); if (!_emptyTexture) { this._emptyTexture = _emptyTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, _emptyTexture); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) ); } return _emptyTexture; } _onContextCreate = (gl: WebGLRenderingContext): void => { const onSuccess = () => { this.setState( { ready: true, }, () => { try { this._handleLoad(); } catch (e) { this._handleError(e); } } ); }; this._prepareGL(gl, onSuccess, this._handleError); }; _onContextFailure = (e: Error): void => { this._handleError(e); }; _onContextLost = (): void => { if (this.props.onContextLost) this.props.onContextLost(); this._stopLoop(); this._destroyGL(); if (this.root) this.root._onContextLost(); }; _onContextRestored = (gl: WebGLRenderingContext) => { if (this.root) this.root._onContextRestored(gl); this._prepareGL( gl, this._handleRestoredSuccess, this._handleRestoredFailure ); }; _destroyGL() { const { gl } = this; if (gl) { this.gl = null; if (this._emptyTexture) { gl.deleteTexture(this._emptyTexture); this._emptyTexture = null; } if (this.loaderResolver) { this.loaderResolver.dispose(); } for (let k in this.shaders) { this.shaders[k].dispose(); } this.shaders = {}; gl.deleteBuffer(this.buffer); this.getVisitors().map((v) => v.onSurfaceGLContextChange(this, null)); } } _prepareGL( gl: WebGLRenderingContext, onSuccess: () => void, onError: (e: Error) => void ) { this.gl = gl; this.getVisitors().map((v) => v.onSurfaceGLContextChange(this, gl)); this.loaderResolver = new LoaderResolver(gl); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 4, 4, -1]), // see a-big-triangle gl.STATIC_DRAW ); this.buffer = buffer; const { preload } = this.props; const all: Array<Promise<*>> = []; (preload || []).forEach((raw) => { if (!raw) { console.warn("Can't preload value", raw); return; } const { loader, input } = this._resolveTextureLoader(raw); if (!loader) { console.warn("Can't preload input", raw, input); return; } const loadedAlready = loader.get(input); if (loadedAlready) return; all.push(loader.load(input)); }); this._preparingGL = all; if (all.length > 0) { Promise.all(all).then(onSuccess, onError); // FIXME make sure this never finish if _prepareGL is called again. } else { onSuccess(); } } _onRef = (ref: ?GLView): void => { this.glView = ref; }; _addGLNodeChild(node: Node): void { invariant( !this.root, "Surface can only contains a single root. Got: %s", this.root && this.root.getGLName() ); this.root = node; node._addDependent(this); this.redraw(); } _removeGLNodeChild(node: Node): void { this.root = null; this.redraw(); } _handleError = (e: Error): void => { const { onLoadError } = this.props; if (onLoadError) onLoadError(e); else { console.error(e); } }; _handleRestoredFailure = (): void => { // there is nothing we can do. it's a dead end. }; _handleRestoredSuccess = (): void => { this.redraw(); this.flush(); this._startLoop(); if (this.props.onContextRestored) this.props.onContextRestored(); }; _handleLoad = (): void => { if (!this.root) { console.warn( this.getGLName() + " children does not contain any discoverable Node" ); } const { onLoad } = this.props; this.redraw(); this.flush(); this._startLoop(); if (onLoad) onLoad(); }; _resolveTextureLoader( raw: mixed ): { loader: ?WebGLTextureLoader<*>, input: any } { let input = raw; let loader: ?WebGLTextureLoader<*> = this.loaderResolver && this.loaderResolver.resolve(input); return { loader, input }; } _makeShader({ frag, vert }: ShaderInfo, name?: string): Shader { const { gl } = this; invariant(gl, "gl is not available"); const shader = createShader(gl, vert, frag); for (let key in shader.attributes) { shader.attributes[key].pointer(); } return shader; } _getShader(shaderId: ShaderIdentifier): Shader { const { shaders } = this; return ( shaders[shaderId.id] || (shaders[shaderId.id] = this._makeShader( Shaders.get(shaderId), Shaders.getName(shaderId) )) ); } _bindRootNode(): void { const { gl } = this; invariant(gl, "gl context not available"); gl.bindFramebuffer(gl.FRAMEBUFFER, null); const [width, height] = this.getGLSize(); gl.viewport(0, 0, width, height); } _loopRaf: any; _startLoop(): void { cancelFrame(this._loopRaf); const loop = () => { this._loopRaf = requestFrame(loop); if (this._needsRedraw) this._draw(); }; this._loopRaf = requestFrame(loop); } _stopLoop(): void { cancelFrame(this._loopRaf); } _draw(): void { const { gl, root, glView } = this; invariant(glView, "GLView is mounted"); const visitors = this.getVisitors(); if (!gl || !root || !this._needsRedraw) { visitors.forEach((v) => v.onSurfaceDrawSkipped(this)); return; } this._needsRedraw = false; visitors.forEach((v) => v.onSurfaceDrawStart(this)); if (glView.beforeDraw) glView.beforeDraw(gl); try { root._draw(); } catch (e) { let silent = false; visitors.forEach((v) => { silent = v.onSurfaceDrawError(e) || silent; }); if (!silent) { if ( __DEV__ && glView.debugError && e.longMessage /* duck typing an "interesting" GLError (from lib gl-shader) */ ) { glView.debugError(e); } else { console.warn(e); throw e; } } return; } if (glView.afterDraw) glView.afterDraw(gl); visitors.forEach((v) => v.onSurfaceDrawEnd(this)); } }; };