├── .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 | [](https://npmjs.com/package/react-curtains)
6 | [](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 |
--------------------------------------------------------------------------------