├── .gitignore ├── LICENSE.txt ├── README.md ├── curtains.md ├── fxaa-pass.md ├── package-lock.json ├── package.json ├── ping-pong-plane.md ├── plane.md ├── render-target.md ├── rollup.config.js ├── shader-pass.md └── src ├── components ├── Curtains.js ├── FXAAPass.js ├── PingPongPlane.js ├── Plane.js ├── RenderTarget.js └── ShaderPass.js ├── hooks.js ├── index.js └── store └── curtainsStore.js /.gitignore: -------------------------------------------------------------------------------- 1 | # misc 2 | /.idea 3 | /node_modules 4 | /dist 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Martin Laxenaire 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

react-curtains

2 | 3 | react-curtains is an attempt at converting curtains.js WebGL classes into reusable React components. 4 | 5 | [![Version](https://img.shields.io/npm/v/react-curtains?style=flat&colorA=f5f5f5&colorB=f5f5f5)](https://npmjs.com/package/react-curtains) 6 | [![Twitter](https://img.shields.io/twitter/follow/webdesign_ml?label=%40webdesign_ml&style=flat&colorA=f5f5f5&colorB=f5f5f5&logo=twitter&logoColor=000000)](https://twitter.com/webdesign_ml) 7 | 8 | ## Getting started 9 | 10 | ### Installation 11 | 12 | Of course you'll need to create a React app first. Then, just add react-curtains into your project by installing the npm package: 13 | 14 | ```bash 15 | npm install react-curtains 16 | ``` 17 | 18 | ### Components 19 | 20 | react-curtains introduces a bunch of components based on curtains.js classes: 21 | 22 | - [Curtains](curtains.md) 23 | - [Plane](plane.md) 24 | - [RenderTarget](render-target.md) 25 | - [ShaderPass](shader-pass.md) 26 | - [PingPongPlane](ping-pong-plane.md) 27 | - [FXAAPass](fxaa-pass.md) 28 | 29 | In order for it to work, you'll need to wrap your `App` into the `Curtains` component. You'll be then able to use the other components to add WebGL objects to your scene. 30 | 31 | ### Hooks 32 | 33 | Inside your `` component, you'll have access to a couple useful custom React hooks: 34 | 35 | ##### useCurtains 36 | 37 | ```javascript 38 | useCurtains(callback, dependencies); 39 | ``` 40 | 41 | This hook is called once the curtains WebGL context has been created and each time one of the dependencies changed after that. Note that you'll have access to the `curtains` object in your callback. 42 | As with a traditional React hook, you can return a function to perform a cleanup. 43 | 44 | ```javascript 45 | useCurtains((curtains) => { 46 | // get curtains bounding box for example... 47 | const curtainsBBox = curtains.getBoundingRect(); 48 | }); 49 | ``` 50 | 51 | ##### useCurtainsEvent 52 | 53 | ```javascript 54 | useCurtainsEvent(event, callback, dependencies); 55 | ``` 56 | 57 | This hook lets you subscribe to any of your curtains instance events, so you can use those events from any component in your app. 58 | 59 | ```javascript 60 | useCurtainsEvent("onScroll", (curtains) => { 61 | // get the scroll values... 62 | const scrollValues = curtains.getScrollValues(); 63 | }); 64 | ``` 65 | 66 | ### Examples 67 | 68 | #### Explore 69 | 70 | Here are codesandboxes ports of some of the official documentation examples: 71 | 72 | - Basic plane 73 | - Vertex coordinates helper 74 | - Simple plane 75 | - Simple video plane 76 | - Slideshow using GSAP 77 | - Multiple planes 78 | - Multiple planes with post processing 79 | - Selective render targets 80 | - Flowmap 81 | 82 | #### Basic example 83 | 84 | This is the port of curtains.js documentation basic example: 85 | 86 | ##### index.css 87 | 88 | ```css 89 | /* curtains canvas container */ 90 | 91 | .curtains-canvas { 92 | position: fixed; 93 | top: 0; 94 | left: 0; 95 | width: 100vw; 96 | height: 100vh; 97 | } 98 | 99 | /* basic plane */ 100 | 101 | .BasicPlane { 102 | width: 100vw; 103 | height: 100vh; 104 | } 105 | 106 | .BasicPlane img { 107 | display: none; 108 | } 109 | ``` 110 | 111 | ##### index.js 112 | 113 | ```jsx 114 | import ReactDOM from 'react-dom'; 115 | import React from 'react'; 116 | import {Curtains, Plane} from 'react-curtains'; 117 | 118 | import './index.css'; 119 | 120 | const basicVs = ` 121 | precision mediump float; 122 | 123 | attribute vec3 aVertexPosition; 124 | attribute vec2 aTextureCoord; 125 | 126 | uniform mat4 uMVMatrix; 127 | uniform mat4 uPMatrix; 128 | 129 | uniform mat4 uTextureMatrix0; 130 | 131 | varying vec3 vVertexPosition; 132 | varying vec2 vTextureCoord; 133 | 134 | void main() { 135 | gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); 136 | 137 | // varyings 138 | vVertexPosition = aVertexPosition; 139 | vTextureCoord = (uTextureMatrix0 * vec4(aTextureCoord, 0.0, 1.0)).xy; 140 | } 141 | `; 142 | 143 | 144 | const basicFs = ` 145 | precision mediump float; 146 | 147 | varying vec3 vVertexPosition; 148 | varying vec2 vTextureCoord; 149 | 150 | uniform sampler2D uSampler0; 151 | 152 | uniform float uTime; 153 | 154 | void main() { 155 | vec2 textureCoord = vTextureCoord; 156 | // displace our pixels along the X axis based on our time uniform 157 | // textures coords are ranging from 0.0 to 1.0 on both axis 158 | textureCoord.x += sin(textureCoord.y * 25.0) * cos(textureCoord.x * 25.0) * (cos(uTime / 50.0)) / 25.0; 159 | 160 | gl_FragColor = texture2D(uSampler0, textureCoord); 161 | } 162 | `; 163 | 164 | function BasicPlane({children}) { 165 | const basicUniforms = { 166 | time: { 167 | name: "uTime", 168 | type: "1f", 169 | value: 0 170 | } 171 | }; 172 | 173 | const onRender = (plane) => { 174 | plane.uniforms.time.value++; 175 | }; 176 | 177 | return ( 178 | 189 | {children} 190 | 191 | ) 192 | } 193 | 194 | ReactDOM.render( 195 | 196 | 197 | 198 | 199 | , 200 | document.getElementById('root') 201 | ); 202 | ``` 203 | -------------------------------------------------------------------------------- /curtains.md: -------------------------------------------------------------------------------- 1 |

Curtains

2 | 3 | [Back to readme](README.md) 4 | 5 | The `` component is responsible for the creation of the WebGL context. It will act as a wrapper for the curtains.js Curtains class. 6 | 7 | This component will create a React context that will be used in the custom `useCurtains` and `useCurtainsEvent` hooks onto which the other components rely. 8 | 9 | **Do not try to create a `react-curtains` component or use one of those hooks outside this component or your app will crash.** 10 | 11 | For all those reasons, you should always wrap your application, including additional context providers and routing inside the `` component. 12 | 13 | #### Usage 14 | 15 | ```jsx 16 | import ReactDOM from 'react-dom'; 17 | import React from 'react'; 18 | import {Curtains} from 'react-curtains'; 19 | import App from 'App'; 20 | 21 | ReactDOM.render( 22 | 23 | 24 | , 25 | document.getElementById('root') 26 | ); 27 | ``` 28 | 29 | #### Properties 30 | 31 | Except for the container, which will be set internally, you can pass any of the Curtains class parameters as a React prop to your component. 32 | Also note that the `production` property is set to `false` on development and `true` on production environments by default. 33 | 34 | You can also use React props and events like `className` or `onClick`. They can be used to style your canvas container and listen to events: 35 | 36 | ```jsx 37 | 42 | 43 | 44 | ``` 45 | 46 | #### Events 47 | 48 | You can also pass as props a function to execute for each corresponding Curtains class events. You'll have access to your `curtains` instance inside all of them. 49 | 50 | ```jsx 51 | function MainCurtains() { 52 | 53 | const onCurtainsError = (curtains) => { 54 | console.log("on error!", curtains); 55 | }; 56 | 57 | const onCurtainsRender = (curtains) => { 58 | console.log("on render!", curtains); 59 | }; 60 | 61 | return ( 62 | 70 | 71 | 72 | ); 73 | } 74 | ``` 75 | 76 | #### Unmounting 77 | 78 | Even tho this should not happen in most use case, the WebGL context will be disposed each time this component will unmount. 79 | -------------------------------------------------------------------------------- /fxaa-pass.md: -------------------------------------------------------------------------------- 1 |

FXAAPass

2 | 3 | [Back to readme](README.md) 4 | 5 | The `` component will create a WebGL FXAAPass (using a ShaderPass object), acting as a wrapper for the curtains.js FXAAPass class. 6 | 7 | #### Usage 8 | 9 | ```jsx 10 | import ReactDOM from 'react-dom'; 11 | import React from 'react'; 12 | import {FXAAPass} from 'react-curtains'; 13 | 14 | function BasicFXAAPass() { 15 | return ( 16 | 17 | ); 18 | } 19 | ``` 20 | 21 | #### Properties & Events 22 | 23 | You can refer to both the FXAAPass curtains.js class and [ShaderPass component](shader-pass.md) documentations. 24 | 25 | Most of the time tho, you'll just add the `` component without any props and let it automatically add anti-aliasing to your scene. 26 | 27 | #### Unmounting 28 | 29 | Each time the `` component will unmount, the corresponding WebGL shaderpass and its associated render target element will be automatically disposed. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-curtains", 3 | "version": "1.0.10", 4 | "description": "react-curtains is an attempt at converting curtains.js WebGL classes into reusable React components.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "keywords": [ 8 | "react", 9 | "webgl", 10 | "curtains.js", 11 | "curtainsjs" 12 | ], 13 | "author": { 14 | "name": "Martin Laxenaire", 15 | "email": "martin.laxenaire@gmail.com", 16 | "url": "https://twitter.com/webdesign_ml" 17 | }, 18 | "homepage": "https://github.com/martinlaxenaire/react-curtains", 19 | "bugs": { 20 | "url": "https://github.com/martinlaxenaire/react-curtains/issues" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/martinlaxenaire/react-curtains.git" 25 | }, 26 | "license": "MIT", 27 | "scripts": { 28 | "build": "rollup -c", 29 | "prepare": "npm run build" 30 | }, 31 | "dependencies": { 32 | "curtainsjs": ">=8.1.0" 33 | }, 34 | "peerDependencies": { 35 | "react": ">=16.13.0", 36 | "react-dom": ">=16.13.0" 37 | }, 38 | "devDependencies": { 39 | "react": "^16.13.0", 40 | "react-dom": "^16.13.0", 41 | "react-scripts": "3.4.0", 42 | "@babel/core": "^7.10.5", 43 | "@babel/preset-env": "^7.10.4", 44 | "@babel/preset-react": "^7.8.3", 45 | "rollup-plugin-babel": "^4.4.0", 46 | "@rollup/plugin-commonjs": "^15.1.0", 47 | "rollup-plugin-node-resolve": "^5.2.0", 48 | "rollup-plugin-peer-deps-external": "^2.2.2", 49 | "rollup": "^2.21.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ping-pong-plane.md: -------------------------------------------------------------------------------- 1 |

PingPongPlane

2 | 3 | [Back to readme](README.md) 4 | 5 | The `` component will create a WebGL PingPongPlane (using a Plane object), acting as a wrapper for the curtains.js PingPongPlane class. 6 | 7 | #### Usage 8 | 9 | ```jsx 10 | import ReactDOM from 'react-dom'; 11 | import React from 'react'; 12 | import {PingPongPlane} from 'react-curtains'; 13 | 14 | function BasicPingPongPlane() { 15 | return ( 16 | 17 | ); 18 | } 19 | ``` 20 | 21 | #### Properties & Events 22 | 23 | You can refer to both the PingPongPlane curtains.js class and [Plane component](plane.md) documentations. 24 | 25 | Compared to the `` component the only additional prop you have to pass to your `` component is the `sampler` name that you'll use in your shader (if not set, will use "uPingPongTexture"). 26 | 27 | #### Unmounting 28 | 29 | Each time the `` component will unmount, the corresponding WebGL plane element will be automatically disposed. -------------------------------------------------------------------------------- /plane.md: -------------------------------------------------------------------------------- 1 |

Plane

2 | 3 | [Back to readme](README.md) 4 | 5 | The `` component will create a WebGL Plane, acting as a wrapper for the curtains.js Plane class. 6 | 7 | #### Usage 8 | 9 | ```jsx 10 | import ReactDOM from 'react-dom'; 11 | import React from 'react'; 12 | import {Plane} from 'react-curtains'; 13 | 14 | // will draw a black rectangle 15 | // since it needs at least a custom fragment shader 16 | // to display the texture 17 | function BasicPlane() { 18 | return ( 19 | 22 | 23 | 24 | ); 25 | } 26 | ``` 27 | 28 | #### Properties 29 | 30 | ##### Regular Plane class parameters & properties 31 | 32 | You can pass any of the Plane class parameters as a React prop to your component. 33 | 34 | You can also use React props and events like `className` or `onClick`. They can be used to style your canvas container and listen to events. You can of course pass any DOM children you want to the component. 35 | 36 | ```jsx 37 | // assuming vs, fs and planeUniforms are defined above 38 | 39 | 49 |

This is the plane title!

50 | 51 |
52 | ``` 53 | 54 | All the plane properties that are not read-only are therefore reactive and will be updated each time the corresponding prop is updated! 55 | 56 | ##### Transformations 57 | 58 | You can also pass the Plane transformation values (rotation, translation, scale, transformOrigin) via props. Those are also reactive, which means you can control your Plane transformation via props only! 59 | Just pass the values as arrays to the corresponding prop. To reset a transformation, just pass an empty array: 60 | 61 | ```jsx 62 | // assuming vs, fs, planeUniforms and rotatePlane are defined above 63 | 64 | 72 |

This is the plane title!

73 | 74 |
75 | ``` 76 | 77 | #### Events 78 | 79 | ##### Regular Plane class events 80 | 81 | You can also pass as a prop a function to execute for each corresponding Plane class events. You'll have access to the corresponding `plane` instance inside all of them. 82 | 83 | ```jsx 84 | import ReactDOM from 'react-dom'; 85 | import React from 'react'; 86 | import {Plane} from 'react-curtains'; 87 | 88 | function BasicPlane() { 89 | const onPlaneReady = (plane) => { 90 | console.log("plane is ready", plane); 91 | // you can use any regular plane methods here 92 | plane.setRenderOrder(1); 93 | }; 94 | 95 | const onPlaneRender = (plane) => { 96 | console.log("on plane render!", plane); 97 | }; 98 | 99 | return ( 100 | 106 | 107 | 108 | ); 109 | } 110 | ``` 111 | 112 | 113 | ##### Additional events 114 | 115 | The component introduces 2 new events, `onBeforeCreate` and `onBeforeRemove` that will be called just before the plane is created and removed. 116 | 117 | #### Complete prop list 118 | 119 | Here's a complete prop list that you can pass to your `` component (see also curtains.js Plane class documentation): 120 | 121 | | Prop | Type | Reactive? | Description | 122 | | --- | --- | :---: | --- | 123 | | className | string | - | Plane's div element class names | 124 | | vertexShader | string | - | Plane vertex shader | 125 | | vertexShaderID | string | - | Plane vertex shader script tag ID | 126 | | fragmentShader | string | - | Plane fragment shader | 127 | | fragmentShaderID | string | - | Plane fragment shader script tag ID | 128 | | widthSegments | int | - | Number of vertices along X axis | 129 | | heightSegments | int | - | Number of vertices along Y axis | 130 | | renderOrder | int | X | Determines in which order the plane is drawn | 131 | | depthTest | bool | X | Whether the Plane should enable or disable the depth test | 132 | | transparent | bool | - | If your Plane should handle transparency | 133 | | cullFace | string | - | Which face of the plane should be culled | 134 | | alwaysDraw | bool | X | If your Plane should always be drawn or use frustum culling | 135 | | visible | bool | X | Whether to draw your Plane | 136 | | drawCheckMargins | object | X | Additional margins to add in the frustum culling calculations, in pixels. | 137 | | watchScroll | bool | X | Whether the plane should auto update its position on scroll | 138 | | autoloadSources | bool | - | If the sources should be load on init automatically | 139 | | texturesOptions | object | - | Default options to apply to the textures of the Plane | 140 | | crossOrigin | string | - | Defines the crossOrigin process to load medias | 141 | | fov | int | X | Defines the perspective field of view | 142 | | uniforms | object | - | The uniforms that will be passed to the shaders | 143 | | target | RenderTarget object | X | The render target used to render the Plane | 144 | | relativeTranslation | array | X | Additional translation applied to your Plane along X, Y and Z axis, in pixel | 145 | | rotation | array | X | Rotation applied to your Plane on X, Y and Z axis | 146 | | scale | array | X | Scale applied to your Plane on X and Y axis | 147 | | transformOrigin | array | X | Your Plane transform origin position along X, Y and Z axis | 148 | | onAfterRender | function | - | Called just after your Plane has been drawn | 149 | | onAfterResize | function | - | Called just after your plane has been resized | 150 | | onError | function | - | Called if there's an error while instancing your Plane | 151 | | onLeaveView | function | - | Called when the Plane gets frustum culled | 152 | | onReady | function | - | Called once your Plane is all set up and ready to be drawn | 153 | | onReEnterView | function | - | Called when the Plane's no longer frustum culled | 154 | | onRender | function | - | Called at each Plane's draw call | 155 | | onBeforeCreate | function | - | Called just before the Plane will be created | 156 | | onBeforeRemove | function | - | Called just before the Plane will be removed | 157 | 158 | 159 | 160 | #### Unmounting 161 | 162 | Each time the `` component will unmount, the corresponding WebGL plane element will be automatically disposed. 163 | 164 | 165 | ##### TODO: transitioning 166 | 167 | At the moment there's no way to keep a WebGL plane when the component unmounts (think about page transitions for example). 168 | Combining an `uniqueKey` property with the plane `resetPlane()` method should however do the trick. It should be implemented in an upcoming library version. -------------------------------------------------------------------------------- /render-target.md: -------------------------------------------------------------------------------- 1 |

RenderTarget

2 | 3 | [Back to readme](README.md) 4 | 5 | The `` component will create a WebGL RenderTarget (or Frame Buffer Object), acting as a wrapper for the curtains.js RenderTarget class. 6 | 7 | #### Usage 8 | 9 | ```jsx 10 | import ReactDOM from 'react-dom'; 11 | import React from 'react'; 12 | import {RenderTarget} from 'react-curtains'; 13 | 14 | function BasicRenderTarget({children}) { 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | ``` 22 | 23 | #### Properties 24 | 25 | ##### Regular parameters & properties 26 | 27 | You can pass any of the RenderTarget class parameters as a React prop to your component. 28 | 29 | ```jsx 30 | 34 | {children} 35 | 36 | ``` 37 | 38 | ##### uniqueKey property 39 | 40 | Sometimes you'll want to apply your render target to multiple planes (usually combined with a [ShaderPass](shader-pass.md)), and it may be easier to add your render target inside a loop. You can pass an additional `uniqueKey` prop to your `` component and it will be created just once: 41 | 42 | ```jsx 43 | import ReactDOM from 'react-dom'; 44 | import React from 'react'; 45 | import {RenderTarget, ShaderPass} from 'react-curtains'; 46 | import BasicPlane from './components/BasicPlane'; // a basic plane component 47 | 48 | function SelectivePlanesPass({planeElements}) { 49 | return ( 50 |
51 | {planeElements.map((planeEl) => { 52 | 55 | 58 | 61 | 63 | })} 64 |
65 | ); 66 | } 67 | ``` 68 | 69 | ##### autoDetectChildren property 70 | 71 | By default, the `` component will loop through all its children and assign itself as the `target` prop of all `` and `` children it will found. 72 | 73 | If you want to prevent this behaviour and handle this by yourself, just set its `autoDetectChildren` prop to false: 74 | 75 | ```jsx 76 | 79 | {children} 80 | 81 | ``` 82 | 83 | #### Event 84 | 85 | The `` component provides an additional `onReady` event fired once the render target has been created: 86 | 87 | ```jsx 88 | import ReactDOM from 'react-dom'; 89 | import React from 'react'; 90 | import {RenderTarget} from 'react-curtains'; 91 | 92 | function BasicRenderTarget({children}) { 93 | 94 | const onRenderTargetReady = (renderTarget) => { 95 | console.log("render target is ready!", renderTarget); 96 | // you have access to the render target method here 97 | const renderTexture = renderTarget.getTexture(); 98 | }; 99 | 100 | return ( 101 | 104 | {children} 105 | 106 | ); 107 | } 108 | ``` 109 | 110 | #### Unmounting 111 | 112 | Each time the `` component will unmount, the corresponding WebGL render target element will be automatically disposed. -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import pkg from './package.json'; 5 | 6 | const extensions = ['.js']; 7 | 8 | export default [{ 9 | input: './src/index.js', 10 | output: [ 11 | { 12 | file: pkg.main, 13 | format: 'cjs', 14 | }, 15 | { 16 | file: pkg.module, 17 | format: 'esm', 18 | } 19 | ], 20 | plugins: [ 21 | resolve({extensions}), 22 | babel({ 23 | babelrc: false, 24 | exclude: '**/node_modules/**', 25 | presets: [ 26 | ['@babel/preset-env', {loose: true, modules: false}], 27 | '@babel/preset-react' 28 | ] 29 | }), 30 | commonjs({ 31 | exclude: 'src/**' 32 | }), 33 | ], 34 | external: Object.keys(pkg.peerDependencies || {}), 35 | }]; 36 | 37 | 38 | -------------------------------------------------------------------------------- /shader-pass.md: -------------------------------------------------------------------------------- 1 |

ShaderPass

2 | 3 | [Back to readme](README.md) 4 | 5 | The `` component will create a WebGL ShaderPass (using a RenderTarget object), acting as a wrapper for the curtains.js ShaderPass class. 6 | 7 | #### Usage 8 | 9 | ```jsx 10 | import ReactDOM from 'react-dom'; 11 | import React from 'react'; 12 | import {ShaderPass} from 'react-curtains'; 13 | 14 | function BasicShaderPass() { 15 | return ( 16 | 17 | ); 18 | } 19 | ``` 20 | 21 | #### Properties 22 | 23 | ##### Regular parameters & properties 24 | 25 | You can pass any of the ShaderPass class parameters as a React prop to your component. 26 | 27 | ```jsx 28 | // assuming passVs, passFs and passUniforms are defined above 29 | 30 | 36 | ``` 37 | 38 | ##### uniqueKey property 39 | 40 | When dealing with selective passes (ie: apply a shader pass to a bunch of planes, not all of them), it may be easier to add your render target and shader pass inside a loop. Just like with the `` you can pass an additional `uniqueKey` prop to your `` component and it will be created just once. 41 | 42 | 43 | 44 | ```jsx 45 | import ReactDOM from 'react-dom'; 46 | import React from 'react'; 47 | import {RenderTarget, ShaderPass} from 'react-curtains'; 48 | import BasicPlane from './components/BasicPlane'; // a basic plane component 49 | 50 | function SelectivePlanesPass({planeElements}) { 51 | return ( 52 |
53 | {planeElements.map((planeEl) => { 54 | 57 | 60 | 63 | 65 | })} 66 |
67 | ); 68 | } 69 | ``` 70 | 71 | Note that this prop is optional: if the parent `` component has its `autoDetectChildren` prop set to true (which is by default), it can inherit from its `uniqueKey` prop as well. 72 | 73 | #### Events 74 | 75 | You can also pass as a prop a function to execute for each corresponding ShaderPass class events. You'll have access to the corresponding `shaderPass` instance inside all of them. 76 | 77 | ```jsx 78 | import ReactDOM from 'react-dom'; 79 | import React from 'react'; 80 | import {ShaderPass} from 'react-curtains'; 81 | 82 | function BasicShaderPass() { 83 | 84 | const onPassReady = (shaderPass) => { 85 | console.log("shader pass is ready", shaderPass); 86 | }; 87 | 88 | const onPassRender = (shaderPass) => { 89 | console.log("on shader pass render!", shaderPass); 90 | }; 91 | 92 | return ( 93 | 97 | ); 98 | } 99 | ``` 100 | 101 | #### Unmounting 102 | 103 | Each time the `` component will unmount, the corresponding WebGL shaderpass and its associated render target element will be automatically disposed. -------------------------------------------------------------------------------- /src/components/Curtains.js: -------------------------------------------------------------------------------- 1 | import React, {useContext, useRef, useLayoutEffect} from 'react'; 2 | import {CurtainsProvider, CurtainsContext} from "../store/curtainsStore"; 3 | import {Curtains as WebGLCurtains} from 'curtainsjs'; 4 | 5 | function CurtainsWrapper(props) { 6 | const {state, dispatch} = useContext(CurtainsContext); 7 | 8 | const container = useRef(null); 9 | const curtains = useRef(null); 10 | 11 | const { 12 | // curtains class parameters 13 | alpha, 14 | antialias, 15 | premultipliedAlpha, 16 | depth, 17 | preserveDrawingBuffer, 18 | failIfMajorPerformanceCaveat, 19 | stencil, 20 | autoRender, 21 | autoResize, 22 | pixelRatio, 23 | renderingScale, 24 | watchScroll, 25 | 26 | // production 27 | production, 28 | 29 | // curtains class events 30 | onAfterResize, 31 | onContextLost, 32 | onContextRestored, 33 | onError, 34 | onSuccess, 35 | onRender, 36 | onScroll, 37 | 38 | ...validProps 39 | } = props; 40 | 41 | // mount 42 | const useMountEffect = (callback) => useLayoutEffect(callback, []); 43 | 44 | useMountEffect(() => { 45 | // only init curtains on client side! 46 | const canUseDOM = !!( 47 | typeof window !== 'undefined' && 48 | window.document && 49 | window.document.createElement 50 | ); 51 | 52 | if(canUseDOM && !state.curtains && !curtains.current) { 53 | curtains.current = new WebGLCurtains({ 54 | container: container.current, 55 | production: production ? production : process.env.NODE_ENV === "production", 56 | 57 | alpha, 58 | antialias, 59 | premultipliedAlpha, 60 | depth, 61 | preserveDrawingBuffer, 62 | failIfMajorPerformanceCaveat, 63 | stencil, 64 | autoRender, 65 | autoResize, 66 | pixelRatio, 67 | renderingScale, 68 | watchScroll, 69 | }); 70 | 71 | dispatch({ 72 | type: "SET_CURTAINS", 73 | curtains: curtains.current, 74 | }); 75 | } 76 | 77 | const currentCurtains = curtains.current; 78 | return () => { 79 | if(currentCurtains) { 80 | currentCurtains.dispose(); 81 | } 82 | } 83 | }); 84 | 85 | const useStateEffect = (callback) => useLayoutEffect(callback, [state]); 86 | 87 | useStateEffect(() => { 88 | if(curtains.current) { 89 | curtains.current 90 | .onAfterResize(() => { 91 | onAfterResize && onAfterResize(curtains.current); 92 | 93 | // execute subscriptions hooks 94 | state.subscriptions.onAfterResize.forEach(element => { 95 | element.callback && element.callback(curtains.current) 96 | }); 97 | }) 98 | .onContextLost(() => { 99 | onContextLost && onContextLost(curtains.current); 100 | 101 | // execute subscriptions hooks 102 | state.subscriptions.onContextLost.forEach(element => { 103 | element.callback && element.callback(curtains.current) 104 | }); 105 | }) 106 | .onContextRestored(() => { 107 | onContextRestored && onContextRestored(curtains.current); 108 | 109 | // execute subscriptions hooks 110 | state.subscriptions.onContextRestored.forEach(element => { 111 | element.callback && element.callback(curtains.current) 112 | }); 113 | }) 114 | .onSuccess(() => { 115 | onSuccess && onSuccess(curtains.current); 116 | 117 | // execute subscriptions hooks 118 | state.subscriptions.onSuccess.forEach(element => { 119 | element.callback && element.callback(curtains.current) 120 | }); 121 | }) 122 | .onError(() => { 123 | onError && onError(curtains.current); 124 | 125 | // execute subscriptions hooks 126 | state.subscriptions.onError.forEach(element => { 127 | element.callback && element.callback(curtains.current) 128 | }); 129 | }) 130 | .onRender(() => { 131 | onRender && onRender(curtains.current); 132 | 133 | // execute subscriptions hooks 134 | state.subscriptions.onRender.forEach(element => { 135 | element.callback && element.callback(curtains.current) 136 | }); 137 | }) 138 | .onScroll(() => { 139 | onScroll && onScroll(curtains.current); 140 | 141 | // execute subscriptions hooks 142 | state.subscriptions.onScroll.forEach(element => { 143 | element.callback && element.callback(curtains.current) 144 | }); 145 | }); 146 | } 147 | }); 148 | 149 | validProps.className = validProps.className || "curtains-canvas"; 150 | 151 | // avoid passing children to validProps 152 | validProps.children = null; 153 | 154 | return ( 155 | <> 156 | {props.children} 157 |
158 | 159 | ); 160 | } 161 | 162 | export function Curtains(props) { 163 | return ( 164 | 165 | 166 | 167 | ); 168 | } -------------------------------------------------------------------------------- /src/components/FXAAPass.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from 'react'; 2 | import {useCurtains} from '../hooks'; 3 | import {FXAAPass as WebGLFXAAPass} from 'curtainsjs'; 4 | 5 | export function FXAAPass(props) { 6 | // extract fxaa parameters from props 7 | const { 8 | // fxaa init parameters 9 | depthTest, 10 | renderOrder, 11 | depth, 12 | clear, 13 | renderTarget, 14 | texturesOptions, 15 | 16 | // shader pass events 17 | onAfterRender, 18 | onAfterResize, 19 | onError, 20 | onLoading, 21 | onReady, 22 | onRender, 23 | 24 | // unique key if created inside a loop 25 | uniqueKey, 26 | } = props; 27 | 28 | 29 | const webglFXAAPass = useRef(); 30 | 31 | useCurtains((curtains) => { 32 | let existingPass = []; 33 | if(uniqueKey) { 34 | existingPass = curtains.shaderPasses.filter(pass => pass._uniqueKey === uniqueKey); 35 | } 36 | 37 | let currentFXAAPass; 38 | 39 | if(!webglFXAAPass.current && !existingPass.length) { 40 | 41 | webglFXAAPass.current = new WebGLFXAAPass(curtains, { 42 | depthTest, 43 | renderOrder, 44 | depth, 45 | clear, 46 | renderTarget, 47 | texturesOptions, 48 | }); 49 | 50 | webglFXAAPass.current.onAfterRender(() => { 51 | onAfterRender && onAfterRender(webglFXAAPass.current) 52 | }) 53 | .onAfterResize(() => { 54 | onAfterResize && onAfterResize(webglFXAAPass.current); 55 | }) 56 | .onError(() => { 57 | onError && onError(webglFXAAPass.current); 58 | }) 59 | .onLoading(() => { 60 | onLoading && onLoading(webglFXAAPass.current); 61 | }) 62 | .onReady(() => { 63 | onReady && onReady(webglFXAAPass.current); 64 | }) 65 | .onRender(() => { 66 | onRender && onRender(webglFXAAPass.current); 67 | }); 68 | 69 | if(uniqueKey) { 70 | webglFXAAPass.current._uniqueKey = uniqueKey; 71 | } 72 | 73 | currentFXAAPass = webglFXAAPass.current; 74 | } 75 | else if(!webglFXAAPass.current) { 76 | webglFXAAPass.current = existingPass[0]; 77 | } 78 | 79 | return () => { 80 | if(currentFXAAPass) { 81 | currentFXAAPass.remove(); 82 | } 83 | } 84 | }); 85 | 86 | // handle parameters/properties that could be changed at runtime 87 | useEffect(() => { 88 | if(webglFXAAPass.current) { 89 | if(renderOrder !== undefined) { 90 | webglFXAAPass.current.setRenderOrder(renderOrder); 91 | } 92 | } 93 | }, [renderOrder]); 94 | 95 | return props.children || null; 96 | } -------------------------------------------------------------------------------- /src/components/PingPongPlane.js: -------------------------------------------------------------------------------- 1 | import React, {useRef, useEffect} from 'react'; 2 | import {useCurtains} from '../hooks'; 3 | import {PingPongPlane as WebGLPingPongPlane, Vec3, Vec2} from 'curtainsjs'; 4 | 5 | export function PingPongPlane(props) { 6 | // extract plane parameters and events from props 7 | const { 8 | sampler, 9 | // plane init parameters 10 | vertexShader, 11 | vertexShaderID, 12 | fragmentShader, 13 | fragmentShaderID, 14 | widthSegments, 15 | heightSegments, 16 | depthTest, 17 | transparent, 18 | cullFace, 19 | shareProgram, 20 | visible, 21 | drawCheckMargins, 22 | watchScroll, 23 | texturesOptions, 24 | crossOrigin, 25 | fov, 26 | uniforms, 27 | 28 | // render target 29 | target, 30 | 31 | // plane transformations 32 | relativeTranslation, 33 | rotation, 34 | scale, 35 | transformOrigin, 36 | 37 | // plane events 38 | onAfterRender, 39 | onAfterResize, 40 | onError, 41 | onLeaveView, 42 | onLoading, 43 | onReady, 44 | onReEnterView, 45 | onRender, 46 | 47 | // custom events 48 | onBeforeCreate, 49 | onBeforeRemove, 50 | 51 | // valid react props 52 | ...validProps 53 | } = props; 54 | 55 | const planeEl = useRef(); 56 | const webglPlane = useRef(); 57 | 58 | useCurtains((curtains) => { 59 | if(!webglPlane.current) { 60 | onBeforeCreate && onBeforeCreate(); 61 | 62 | // just add the plane 63 | webglPlane.current = new WebGLPingPongPlane(curtains, planeEl.current, { 64 | sampler, 65 | vertexShader, 66 | vertexShaderID, 67 | fragmentShader, 68 | fragmentShaderID, 69 | widthSegments, 70 | heightSegments, 71 | depthTest, 72 | transparent, 73 | cullFace, 74 | shareProgram, 75 | visible, 76 | drawCheckMargins, 77 | watchScroll, 78 | texturesOptions, 79 | crossOrigin, 80 | fov, 81 | uniforms, 82 | }) 83 | .onAfterRender(() => { 84 | onAfterRender && onAfterRender(webglPlane.current) 85 | }) 86 | .onAfterResize(() => { 87 | onAfterResize && onAfterResize(webglPlane.current); 88 | }) 89 | .onError(() => { 90 | onError && onError(webglPlane.current); 91 | }) 92 | .onLeaveView(() => { 93 | onLeaveView && onLeaveView(webglPlane.current); 94 | }) 95 | .onLoading((texture) => { 96 | onLoading && onLoading(webglPlane.current, texture); 97 | }) 98 | .onReady(() => { 99 | onReady && onReady(webglPlane.current); 100 | }) 101 | .onReEnterView(() => { 102 | onReEnterView && onReEnterView(webglPlane.current); 103 | }) 104 | .onRender(() => { 105 | onRender && onRender(webglPlane.current); 106 | }); 107 | } 108 | 109 | let currentPlane = webglPlane.current; 110 | 111 | return () => { 112 | if(currentPlane) { 113 | onBeforeRemove && onBeforeRemove(currentPlane); 114 | 115 | currentPlane.remove(); 116 | } 117 | } 118 | }); 119 | 120 | 121 | 122 | // handle parameters/properties that could be changed at runtime 123 | useEffect(() => { 124 | if(webglPlane.current) { 125 | // simple properties 126 | if(cullFace !== undefined) { 127 | webglPlane.current.cullFace = cullFace; 128 | } 129 | if(drawCheckMargins !== undefined) { 130 | webglPlane.current.drawCheckMargins = drawCheckMargins; 131 | } 132 | if(visible !== undefined) { 133 | webglPlane.current.visible = visible; 134 | } 135 | if(watchScroll !== undefined) { 136 | webglPlane.current.watchScroll = watchScroll; 137 | } 138 | 139 | // other properties 140 | if(depthTest !== undefined) { 141 | webglPlane.current.enableDepthTest(depthTest); 142 | } 143 | 144 | // render target 145 | if(target !== undefined) { 146 | webglPlane.current.setRenderTarget(target); 147 | } 148 | 149 | // transformations 150 | if(relativeTranslation) { 151 | const newTranslation = new Vec3(); 152 | if(rotation.length >= 3) { 153 | newTranslation.set(relativeTranslation[0], relativeTranslation[1], relativeTranslation[2]); 154 | } 155 | 156 | webglPlane.current.setRelativeTranslation(newTranslation); 157 | } 158 | if(rotation) { 159 | const newRotation = new Vec3(); 160 | if(rotation.length >= 3) { 161 | newRotation.set(rotation[0], rotation[1], rotation[2]); 162 | } 163 | 164 | webglPlane.current.setRotation(newRotation); 165 | } 166 | if(scale) { 167 | const newScale = new Vec2(1, 1); 168 | if(scale.length >= 2) { 169 | newScale.set(scale[0], scale[1]); 170 | } 171 | 172 | webglPlane.current.setScale(newScale); 173 | } 174 | if(transformOrigin) { 175 | const newTransformOrigin = new Vec3(0.5, 0.5, 0); 176 | if(transformOrigin.length >= 3) { 177 | newTransformOrigin.set(transformOrigin[0], transformOrigin[1], transformOrigin[2]); 178 | } 179 | 180 | webglPlane.current.setTransformOrigin(newTransformOrigin); 181 | } 182 | 183 | // update camera fov only if it actually changed 184 | if(fov !== undefined && fov !== webglPlane.current.camera.fov) { 185 | webglPlane.current.setPerspective(fov); 186 | } 187 | } 188 | }, [ 189 | cullFace, 190 | drawCheckMargins, 191 | visible, 192 | watchScroll, 193 | 194 | depthTest, 195 | 196 | target, 197 | 198 | relativeTranslation, 199 | rotation, 200 | scale, 201 | transformOrigin, 202 | 203 | fov, 204 | ]); 205 | 206 | return ( 207 |
208 | {props.children} 209 |
210 | ); 211 | } -------------------------------------------------------------------------------- /src/components/Plane.js: -------------------------------------------------------------------------------- 1 | import React, {useRef, useEffect} from 'react'; 2 | import {useCurtains} from '../hooks'; 3 | import {Plane as WebGLPlane, Vec3, Vec2} from 'curtainsjs'; 4 | 5 | export function Plane(props) { 6 | // extract plane parameters and events from props 7 | const { 8 | // plane init parameters 9 | vertexShader, 10 | vertexShaderID, 11 | fragmentShader, 12 | fragmentShaderID, 13 | widthSegments, 14 | heightSegments, 15 | renderOrder, 16 | depthTest, 17 | transparent, 18 | cullFace, 19 | alwaysDraw, 20 | visible, 21 | drawCheckMargins, 22 | watchScroll, 23 | autoloadSources, 24 | texturesOptions, 25 | crossOrigin, 26 | fov, 27 | uniforms, 28 | 29 | // render target 30 | target, 31 | 32 | // plane transformations 33 | relativeTranslation, 34 | rotation, 35 | scale, 36 | transformOrigin, 37 | 38 | // plane events 39 | onAfterRender, 40 | onAfterResize, 41 | onError, 42 | onLeaveView, 43 | onLoading, 44 | onReady, 45 | onReEnterView, 46 | onRender, 47 | 48 | // custom events 49 | onBeforeCreate, 50 | onBeforeRemove, 51 | 52 | // valid react props 53 | ...validProps 54 | } = props; 55 | 56 | const planeEl = useRef(); 57 | const webglPlane = useRef(); 58 | 59 | useCurtains((curtains) => { 60 | if(!webglPlane.current) { 61 | onBeforeCreate && onBeforeCreate(); 62 | 63 | // just add the plane 64 | webglPlane.current = new WebGLPlane(curtains, planeEl.current, { 65 | vertexShader, 66 | vertexShaderID, 67 | fragmentShader, 68 | fragmentShaderID, 69 | widthSegments, 70 | heightSegments, 71 | renderOrder, 72 | depthTest, 73 | transparent, 74 | cullFace, 75 | alwaysDraw, 76 | visible, 77 | drawCheckMargins, 78 | watchScroll, 79 | autoloadSources, 80 | texturesOptions, 81 | crossOrigin, 82 | fov, 83 | uniforms, 84 | }) 85 | .onAfterRender(() => { 86 | onAfterRender && onAfterRender(webglPlane.current) 87 | }) 88 | .onAfterResize(() => { 89 | onAfterResize && onAfterResize(webglPlane.current); 90 | }) 91 | .onError(() => { 92 | onError && onError(webglPlane.current); 93 | }) 94 | .onLeaveView(() => { 95 | onLeaveView && onLeaveView(webglPlane.current); 96 | }) 97 | .onLoading((texture) => { 98 | onLoading && onLoading(webglPlane.current, texture); 99 | }) 100 | .onReady(() => { 101 | onReady && onReady(webglPlane.current); 102 | }) 103 | .onReEnterView(() => { 104 | onReEnterView && onReEnterView(webglPlane.current); 105 | }) 106 | .onRender(() => { 107 | onRender && onRender(webglPlane.current); 108 | }); 109 | } 110 | 111 | let currentPlane = webglPlane.current; 112 | 113 | return () => { 114 | if(currentPlane) { 115 | onBeforeRemove && onBeforeRemove(currentPlane); 116 | 117 | currentPlane.remove(); 118 | } 119 | } 120 | }); 121 | 122 | 123 | 124 | // handle parameters/properties that could be changed at runtime 125 | useEffect(() => { 126 | if(webglPlane.current) { 127 | // simple properties 128 | if(alwaysDraw !== undefined) { 129 | webglPlane.current.alwaysDraw = alwaysDraw; 130 | } 131 | if(cullFace !== undefined) { 132 | webglPlane.current.cullFace = cullFace; 133 | } 134 | if(drawCheckMargins !== undefined) { 135 | webglPlane.current.drawCheckMargins = drawCheckMargins; 136 | } 137 | if(visible !== undefined) { 138 | webglPlane.current.visible = visible; 139 | } 140 | if(watchScroll !== undefined) { 141 | webglPlane.current.watchScroll = watchScroll; 142 | } 143 | 144 | // other properties 145 | if(depthTest !== undefined) { 146 | webglPlane.current.enableDepthTest(depthTest); 147 | } 148 | 149 | // render target 150 | if(target !== undefined) { 151 | webglPlane.current.setRenderTarget(target); 152 | } 153 | 154 | // render order 155 | if(renderOrder !== undefined) { 156 | webglPlane.current.setRenderOrder(renderOrder); 157 | } 158 | 159 | // transformations 160 | if(relativeTranslation) { 161 | const newTranslation = new Vec3(); 162 | if(relativeTranslation.length >= 3) { 163 | newTranslation.set(relativeTranslation[0], relativeTranslation[1], relativeTranslation[2]); 164 | } 165 | 166 | webglPlane.current.setRelativeTranslation(newTranslation); 167 | } 168 | if(rotation) { 169 | const newRotation = new Vec3(); 170 | if(rotation.length >= 3) { 171 | newRotation.set(rotation[0], rotation[1], rotation[2]); 172 | } 173 | 174 | webglPlane.current.setRotation(newRotation); 175 | } 176 | if(scale) { 177 | const newScale = new Vec2(1, 1); 178 | if(scale.length >= 2) { 179 | newScale.set(scale[0], scale[1]); 180 | } 181 | 182 | webglPlane.current.setScale(newScale); 183 | } 184 | if(transformOrigin) { 185 | const newTransformOrigin = new Vec3(0.5, 0.5, 0); 186 | if(transformOrigin.length >= 3) { 187 | newTransformOrigin.set(transformOrigin[0], transformOrigin[1], transformOrigin[2]); 188 | } 189 | 190 | webglPlane.current.setTransformOrigin(newTransformOrigin); 191 | } 192 | 193 | // update camera fov only if it actually changed 194 | if(fov !== undefined && fov !== webglPlane.current.camera.fov) { 195 | webglPlane.current.setPerspective(fov); 196 | } 197 | } 198 | }, [ 199 | alwaysDraw, 200 | cullFace, 201 | drawCheckMargins, 202 | visible, 203 | watchScroll, 204 | renderOrder, 205 | 206 | depthTest, 207 | 208 | target, 209 | 210 | relativeTranslation, 211 | rotation, 212 | scale, 213 | transformOrigin, 214 | 215 | fov, 216 | ]); 217 | 218 | return ( 219 |
220 | {props.children} 221 |
222 | ); 223 | } -------------------------------------------------------------------------------- /src/components/RenderTarget.js: -------------------------------------------------------------------------------- 1 | import {Children, cloneElement, isValidElement, useState, useEffect} from 'react'; 2 | import {ShaderPass} from './ShaderPass'; 3 | import {Plane} from './Plane'; 4 | import {useCurtains} from '../hooks'; 5 | import {RenderTarget as WebGLRenderTarget} from 'curtainsjs'; 6 | 7 | export function RenderTarget(props) { 8 | // extract render target parameters from props 9 | const { 10 | // render target init parameters 11 | depth, 12 | clear, 13 | minWidth, 14 | minHeight, 15 | texturesOptions, 16 | 17 | // custom event 18 | onReady, 19 | 20 | // whether to apply this render target to all its planes and shader passes children 21 | autoDetectChildren = true, 22 | 23 | // unique key if created inside a loop 24 | uniqueKey, 25 | } = props; 26 | 27 | const [children, setChildren] = useState(null); 28 | const [renderTarget, setRenderTarget] = useState(null); 29 | 30 | useCurtains((curtains) => { 31 | let existingRenderTarget = []; 32 | if(uniqueKey) { 33 | existingRenderTarget = curtains.renderTargets.filter(target => target._uniqueKey === uniqueKey); 34 | } 35 | 36 | if(!renderTarget && !existingRenderTarget.length) { 37 | const webglRenderTarget = new WebGLRenderTarget(curtains, { 38 | depth, 39 | clear, 40 | minWidth, 41 | minHeight, 42 | texturesOptions, 43 | }); 44 | 45 | if(uniqueKey) { 46 | webglRenderTarget._uniqueKey = uniqueKey; 47 | } 48 | 49 | setRenderTarget(webglRenderTarget); 50 | 51 | onReady && onReady(webglRenderTarget); 52 | } 53 | else if(!renderTarget) { 54 | setRenderTarget(existingRenderTarget[0]); 55 | } 56 | }); 57 | 58 | useEffect(() => { 59 | // recursively map through all children and execute a callback on each react element 60 | const recursiveMap = (children, callback) => { 61 | // return null if the render target does not have any child 62 | if(!Children.count(children)) { 63 | return null; 64 | } 65 | else { 66 | return Children.map(children, child => { 67 | if(!isValidElement(child)) { 68 | return child; 69 | } 70 | 71 | if(child.props.children) { 72 | child = cloneElement(child, { 73 | children: recursiveMap(child.props.children, callback) 74 | }); 75 | } 76 | 77 | return callback(child); 78 | }); 79 | } 80 | }; 81 | 82 | if(!autoDetectChildren) { 83 | setChildren(props.children); 84 | } 85 | else if(renderTarget) { 86 | const compChildren = recursiveMap(props.children, child => { 87 | // our callback 88 | if(child.type === Plane) { 89 | return cloneElement(child, {...child.props, target: renderTarget}); 90 | } 91 | else if(child.type === ShaderPass) { 92 | let augmentedProps = {...child.props, renderTarget: renderTarget}; 93 | 94 | // add uniqueKey if needed and not set 95 | if(uniqueKey && !child.props.uniqueKey) { 96 | augmentedProps = {...augmentedProps, uniqueKey: uniqueKey} 97 | } 98 | 99 | return cloneElement(child, augmentedProps); 100 | } 101 | else { 102 | return child; 103 | } 104 | }); 105 | 106 | setChildren(compChildren); 107 | } 108 | 109 | return () => { 110 | if(renderTarget && !renderTarget._shaderPass && renderTarget.textures.length) { 111 | renderTarget.remove(); 112 | } 113 | } 114 | }, [renderTarget, autoDetectChildren]); 115 | 116 | return children; 117 | } -------------------------------------------------------------------------------- /src/components/ShaderPass.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from 'react'; 2 | import {useCurtains} from '../hooks'; 3 | import {ShaderPass as WebGLShaderPass, Vec2, Vec3} from 'curtainsjs'; 4 | 5 | export function ShaderPass(props) { 6 | // extract shader pass parameters and events from props 7 | const { 8 | // shader pass init parameters 9 | vertexShader, 10 | vertexShaderID, 11 | fragmentShader, 12 | fragmentShaderID, 13 | renderOrder, 14 | depthTest, 15 | depth, 16 | clear, 17 | renderTarget, 18 | texturesOptions, 19 | crossOrigin, 20 | uniforms, 21 | 22 | // shader pass events 23 | onAfterRender, 24 | onAfterResize, 25 | onError, 26 | onLoading, 27 | onReady, 28 | onRender, 29 | 30 | // unique key if created inside a loop 31 | uniqueKey, 32 | } = props; 33 | 34 | 35 | const webglShaderPass = useRef(); 36 | 37 | useCurtains((curtains) => { 38 | let existingPass = []; 39 | if(uniqueKey) { 40 | existingPass = curtains.shaderPasses.filter(pass => pass._uniqueKey === uniqueKey); 41 | } 42 | 43 | let currentShaderPass; 44 | 45 | if(!webglShaderPass.current && !existingPass.length) { 46 | 47 | webglShaderPass.current = new WebGLShaderPass(curtains, { 48 | vertexShader, 49 | vertexShaderID, 50 | fragmentShader, 51 | fragmentShaderID, 52 | renderOrder, 53 | depthTest, 54 | depth, 55 | clear, 56 | renderTarget, 57 | texturesOptions, 58 | crossOrigin, 59 | uniforms, 60 | }) 61 | .onAfterRender(() => { 62 | onAfterRender && onAfterRender(webglShaderPass.current) 63 | }) 64 | .onAfterResize(() => { 65 | onAfterResize && onAfterResize(webglShaderPass.current); 66 | }) 67 | .onError(() => { 68 | onError && onError(webglShaderPass.current); 69 | }) 70 | .onLoading(() => { 71 | onLoading && onLoading(webglShaderPass.current); 72 | }) 73 | .onReady(() => { 74 | onReady && onReady(webglShaderPass.current); 75 | }) 76 | .onRender(() => { 77 | onRender && onRender(webglShaderPass.current); 78 | }); 79 | 80 | if(uniqueKey) { 81 | webglShaderPass.current._uniqueKey = uniqueKey; 82 | } 83 | 84 | currentShaderPass = webglShaderPass.current; 85 | } 86 | else if(!webglShaderPass.current) { 87 | webglShaderPass.current = existingPass[0]; 88 | } 89 | 90 | 91 | return () => { 92 | if(currentShaderPass) { 93 | currentShaderPass.remove(); 94 | } 95 | } 96 | }); 97 | 98 | // handle parameters/properties that could be changed at runtime 99 | useEffect(() => { 100 | if(webglShaderPass.current) { 101 | if(renderOrder !== undefined) { 102 | webglShaderPass.current.setRenderOrder(renderOrder); 103 | } 104 | } 105 | }, [renderOrder]); 106 | 107 | return props.children || null; 108 | } -------------------------------------------------------------------------------- /src/hooks.js: -------------------------------------------------------------------------------- 1 | import {useContext, useEffect, useRef} from 'react'; 2 | import {CurtainsContext} from "./store/curtainsStore"; 3 | 4 | 5 | const generateUUID = () => { 6 | return '_' + Math.random().toString(36).substr(2, 9); 7 | }; 8 | 9 | // execute this hook once our curtains webgl context is ready 10 | // call again each time one of the dependencies change 11 | export function useCurtains(callback, dependencies = []) { 12 | const {state} = useContext(CurtainsContext); 13 | 14 | const useCustomEffect = (effectCallback) => useEffect(effectCallback, [state.curtains].concat(dependencies)); 15 | 16 | useCustomEffect(() => { 17 | let cleanUp; 18 | if(state.curtains && !state.curtains.errors && callback) { 19 | cleanUp = callback(state.curtains); 20 | } 21 | 22 | return () => { 23 | // execute cleanUp if it exists 24 | if(cleanUp && typeof cleanUp === "function") { 25 | cleanUp(); 26 | } 27 | } 28 | }); 29 | } 30 | 31 | // execute this hook when the corresponding curtains event is fired 32 | // call again each time one of the dependencies change 33 | export function useCurtainsEvent(event, callback, dependencies = []) { 34 | const availableEvents = [ 35 | "onAfterResize", 36 | "onContextLost", 37 | "onContextRestored", 38 | "onError", 39 | "onSuccess", 40 | "onRender", 41 | "onScroll", 42 | ]; 43 | 44 | // do not crash if event passed is not allowed 45 | const validEvent = availableEvents.find((availableEvent) => event === availableEvent); 46 | 47 | const {dispatch} = useContext(CurtainsContext); 48 | const eventCallback = useRef({ 49 | // curtains class events, see https://www.curtainsjs.com/curtains-class.html#events 50 | event, 51 | callback, 52 | id: generateUUID() 53 | }); 54 | 55 | useCurtains(() => { 56 | // allow dependencies to be available inside the callback 57 | eventCallback.current.callback = callback.bind(dependencies); 58 | 59 | if(validEvent) { 60 | dispatch({ 61 | type: "ADD_SUBSCRIPTION", 62 | addSubscription: eventCallback.current, 63 | }); 64 | } 65 | 66 | const currentRenderCallback = eventCallback.current; 67 | return () => { 68 | if(validEvent) { 69 | dispatch({ 70 | type: "REMOVE_SUBSCRIPTION", 71 | removeSubscription: currentRenderCallback, 72 | }); 73 | } 74 | } 75 | }, [dispatch, validEvent].concat(dependencies)); 76 | } 77 | 78 | export function useCurtainsAfterResize(callback, dependencies = []) { 79 | useCurtainsEvent("onAfterResize", callback, dependencies); 80 | } 81 | 82 | export function useCurtainsContextLost(callback, dependencies = []) { 83 | useCurtainsEvent("onContextLost", callback, dependencies); 84 | } 85 | 86 | export function useCurtainsContextRestored(callback, dependencies = []) { 87 | useCurtainsEvent("onContextRestored", callback, dependencies); 88 | } 89 | 90 | export function useCurtainsError(callback, dependencies = []) { 91 | useCurtainsEvent("onError", callback, dependencies); 92 | } 93 | 94 | export function useCurtainsSuccess(callback, dependencies = []) { 95 | useCurtainsEvent("onSuccess", callback, dependencies); 96 | } 97 | 98 | export function useCurtainsRender(callback, dependencies = []) { 99 | useCurtainsEvent("onRender", callback, dependencies); 100 | } 101 | 102 | export function useCurtainsScroll(callback, dependencies = []) { 103 | useCurtainsEvent("onScroll", callback, dependencies); 104 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {Curtains} from './components/Curtains'; 2 | export {Plane} from './components/Plane'; 3 | 4 | export {RenderTarget} from './components/RenderTarget'; 5 | export {ShaderPass} from './components/ShaderPass'; 6 | 7 | export {PingPongPlane} from './components/PingPongPlane'; 8 | export {FXAAPass} from './components/FXAAPass'; 9 | 10 | export {useCurtains, useCurtainsEvent} from './hooks'; -------------------------------------------------------------------------------- /src/store/curtainsStore.js: -------------------------------------------------------------------------------- 1 | import React, {createContext, useReducer} from "react"; 2 | 3 | const initialState = { 4 | curtains: null, 5 | // subscriptions to hook on curtains class events 6 | subscriptions: { 7 | onAfterResize: [], 8 | onContextLost: [], 9 | onContextRestored: [], 10 | onError: [], 11 | onSuccess: [], 12 | onRender: [], 13 | onScroll: [], 14 | }, 15 | }; 16 | 17 | export const CurtainsContext = createContext(initialState); 18 | 19 | export function CurtainsProvider({ children }) { 20 | const [state, dispatch] = useReducer((state, action) => { 21 | switch (action.type) { 22 | case "SET_CURTAINS": 23 | return { 24 | ...state, 25 | curtains: action.curtains 26 | }; 27 | 28 | case "ADD_SUBSCRIPTION": 29 | // get store state and subscription action 30 | const {...addSubscriptionState} = state; 31 | const {addSubscription} = action; 32 | 33 | // is it already in our subscription event array? 34 | const existingSubscription = addSubscriptionState.subscriptions[addSubscription.event].find(el => el.id === addSubscription.id); 35 | // if not we'll add it 36 | if(!existingSubscription) { 37 | addSubscriptionState.subscriptions[addSubscription.event].push(addSubscription); 38 | } 39 | 40 | // return updated store state 41 | return addSubscriptionState; 42 | 43 | case "REMOVE_SUBSCRIPTION": 44 | // get store state and subscription action 45 | const {...removeSubscriptionState} = state; 46 | const {removeSubscription} = action; 47 | 48 | // remove from our subscription event array 49 | removeSubscriptionState.subscriptions[removeSubscription.event] = removeSubscriptionState.subscriptions[removeSubscription.event].filter(el => el.id !== removeSubscription.id); 50 | 51 | // return updated store state 52 | return removeSubscriptionState; 53 | 54 | default: 55 | throw new Error(); 56 | } 57 | }, initialState); 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | ); 64 | } 65 | --------------------------------------------------------------------------------