├── .eslintignore
├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── dist
├── midori.cjs
├── midori.d.ts
└── midori.js
├── docs
├── assets
│ ├── 0.jpg
│ ├── 1.jpg
│ ├── 2.jpg
│ ├── midori.1.gif
│ ├── midori.2.gif
│ └── midori.3.gif
├── dist
│ └── index.js
├── index.css
├── index.html
└── index.tsx
├── package-lock.json
├── package.json
├── src
├── background-camera-utils.ts
├── background-camera.ts
├── background-effects.ts
├── background-renderer.ts
├── background.ts
├── effects
│ ├── effect.ts
│ ├── particles.ts
│ └── shaders
│ │ ├── effect
│ │ ├── gaussian-blur-shader.ts
│ │ ├── motion-blur-shader.ts
│ │ └── vignette-blend-shader.ts
│ │ ├── particle-shader.ts
│ │ ├── shader-utils.ts
│ │ └── transition
│ │ ├── blur-shader.ts
│ │ ├── glitch-shader.ts
│ │ ├── slide-shader.ts
│ │ └── wipe-shader.ts
├── midori.esm.js
├── midori.ts
├── pipeline
│ ├── background-pass.ts
│ ├── effect-pass.ts
│ └── transition-pass.ts
├── transition.ts
└── utils.ts
├── tsconfig.json
├── webpack.config.js
├── webpack.dev.js
└── webpack.prod.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | *.js
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:import/errors",
5 | "plugin:import/warnings",
6 | "plugin:import/typescript",
7 | "plugin:@typescript-eslint/recommended",
8 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
9 | "plugin:react/recommended",
10 | "plugin:react-hooks/recommended"
11 | ],
12 | "parser": "@typescript-eslint/parser",
13 | "parserOptions": {
14 | "project": ["./tsconfig.json"]
15 | },
16 | "plugins": ["@typescript-eslint"],
17 | "env": {
18 | "es6": true,
19 | "browser": true,
20 | "node": true
21 | },
22 | "settings": {
23 | "react": {
24 | "version": "detect"
25 | }
26 | },
27 | "rules": {
28 | "max-len": "off",
29 | "prefer-destructuring": ["error", {
30 | "object": true,
31 | "array": false
32 | }],
33 | "consistent-return": "off",
34 | "global-require": "off",
35 | "guard-for-in": "off",
36 | "no-shadow": "off",
37 | "no-plusplus": "off",
38 | "no-prototype-builtins": "off",
39 | "no-underscore-dangle": "off",
40 | "no-restricted-syntax": "off",
41 | "object-curly-newline": ["error", {
42 | "ImportDeclaration": "never"
43 | }],
44 | "lines-between-class-members": "off",
45 | "no-param-reassign": "off",
46 | "no-use-before-define": "off",
47 | "no-console": "off",
48 | "linebreak-style": "off",
49 | "no-dupe-class-members": "off",
50 | "@typescript-eslint/explicit-function-return-type": "off",
51 | "@typescript-eslint/no-explicit-any": "off",
52 | "@typescript-eslint/no-dupe-class-members": ["error"],
53 | "@typescript-eslint/interface-name-prefix": "off",
54 | "@typescript-eslint/no-empty-interface": "off",
55 | "@typescript-eslint/strict-boolean-expressions": ["error", {
56 | "allowNullableObject": true
57 | }],
58 | "@typescript-eslint/no-unsafe-member-access": "off",
59 | "@typescript-eslint/no-unsafe-assignment": "off",
60 | "@typescript-eslint/no-unsafe-call": "off",
61 | "@typescript-eslint/no-unsafe-return": "off",
62 | "@typescript-eslint/restrict-template-expressions": "off",
63 | "@typescript-eslint/no-non-null-assertion": "off"
64 | }
65 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | images/
4 | launch.json
5 | settings.json
6 | *LICENSE.txt
7 | *.map
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Benjamin Pang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # midori
2 | [](https://npmjs.org/package/midori-bg "View this project on npm")
3 |
4 |
9 |
10 | **[Interactive demo available here (with credits to artists).](https://aeroheim.github.io/midori/)**
11 |
12 | ## About
13 | Library for animating image backgrounds in websites using WebGL.
14 |
15 | It support the following:
16 | * Configurable dynamic camera
17 | * Animated transitions between backgrounds
18 | * Post-processing effects & particles
19 |
20 | ## Usage / API
21 | ### Getting Started
22 | First install `midori-bg` and `three`. Three.js is required as a dependency - any version greater than or equal to `three@0.132.2` should work. (if not, please file an issue)
23 | ```console
24 | npm install --save midori-bg three
25 | ```
26 |
27 | Below is an example of how to get started with midori in an ES6 app. For an example in `React`, see the [source for the interactive demo](./docs/index.jsx).
28 |
29 | You'll want to first initialize a renderer before loading and setting images as backgrounds.
30 |
31 | ```js
32 | import { BackgroundRenderer, loadImage } from 'midori-bg';
33 |
34 | // pass in a canvas DOM element
35 | const renderer = new BackgroundRenderer(document.getElementById('canvas'));
36 |
37 | // the loadImage function returns a promise which you can use to load your images.
38 | // the path can be a url or local path to a file. Make sure to check CORS if using a url.
39 | loadImage('url/to/image')
40 | // set background
41 | .then((image) => renderer.setBackground(image))
42 | // handle errors
43 | .catch(err => console.error(err));
44 | ```
45 |
46 | The rendering can also be controlled directly if needed:
47 | ```js
48 | // the renderer can be paused if needed.
49 | renderer.pause();
50 | // the renderer can be resumed after pausing.
51 | renderer.render();
52 | ```
53 |
54 | ### Transitions
55 | When setting backgrounds, you can use an optional transition to animate the switching between backgrounds.
56 |
57 | ```js
58 | import { BackgroundRenderer, TransitionType, Easings, SlideDirection } from 'midori-bg';
59 |
60 | const renderer = new BackgroundRenderer(document.getElementById('canvas'));
61 |
62 | loadImage('url/to/image')
63 | .then((image) => {
64 | // set a new background with a slide transition.
65 | renderer.setBackground(image, {
66 | type: TransitionType.Slide,
67 | config: {
68 | slides: 2,
69 | intensity: 5,
70 | duration: 1.5,
71 | easing: Easings.Quintic.InOut,
72 | direction: SlideDirection.Right,
73 | }
74 | });
75 | })
76 | // handle errors
77 | .catch(err => console.error(err));
78 | ```
79 |
80 | The state of the transition can be queried:
81 |
82 | ```js
83 | const isTransitioning = renderer.isTransitioning();
84 | ```
85 |
86 | The configuration options for transitions:
87 | ```ts
88 | interface BlendTransitionConfig {}
89 |
90 | interface WipeTransitionConfig {
91 | // the size of the fade when wiping.
92 | gradient?: number;
93 | // the angle of the wipe in degrees.
94 | angle?: number;
95 | // the direction of the wipe.
96 | direction?: WipeDirection;
97 | }
98 |
99 | interface SlideTransitionConfig {
100 | // the number of slides to perform.
101 | slides?: number;
102 | // the intensity of the blur during slides.
103 | intensity?: number;
104 | // the number of samples for the blur - more samples result in better quality at the cost of performance.
105 | samples?: number;
106 | // the direction of the slide.
107 | direction?: SlideDirection;
108 | }
109 |
110 | interface BlurTransitionConfig {
111 | // the intensity of the blur.
112 | intensity?: number;
113 | // the number of samples for the blur - more samples result in better quality at the cost of performance.
114 | samples?: number;
115 | }
116 |
117 | interface GlitchTransitionConfig {
118 | // a random seed from 0 to 1 used to generate glitches.
119 | seed?: number;
120 | }
121 | ```
122 |
123 | ### Camera
124 | Each background comes with its own camera. The camera can be moved, swayed, and rotated independently.
125 |
126 | > **⚠️NOTE:** Be careful when storing camera references! When switching to a new background, a new camera will be created. Settings configured on the previous camera are not transferred.
127 |
128 | ```js
129 | import { BackgroundRenderer, Easings } from 'midori-bg';
130 |
131 | const renderer = new BackgroundRenderer(document.getElementById('canvas'));
132 | const { camera } = renderer.background;
133 |
134 | // move the camera to the top-left corner, fully zoomed-out.
135 | camera.move({ x: 0, y: 0, z: 1 });
136 |
137 | // move the camera to the bottom-right corner, fully zoomed-out.
138 | camera.move({ x: 1, y: 1, z: 1});
139 |
140 | // move the camera to the center, half zoomed-in.
141 | camera.move({ x: 0.5, y: 0.5, z: 0.5 });
142 |
143 | // move the camera with a transition.
144 | camera.move({ x: Math.random(), y: Math.random(), z: 0.5 + Math.random() * 0.5 }, {
145 | duration: 2.5,
146 | easing: Easings.Cubic.InOut,
147 | });
148 |
149 | // offset the camera from its current position.
150 | // x - offset to the left by 10% of the background width
151 | // y - offset to the bottom by 20% of the background height
152 | // z - offset the zoom by zooming in 20% of the maximum zoom
153 | // zr - offset the rotation by rotating 15 degrees
154 | camera.offset({ x: -0.1, y: 0.2, z: -0.2, zr: 15 });
155 |
156 | // rotate the camera by 30 degrees with a transition.
157 | camera.rotate(30, {
158 | duration: 2.5,
159 | easing: Easings.Cubic.InOut,
160 | });
161 |
162 | // sway the camera around its center with a transition.
163 | // x - up to 10% of the background width away from the center
164 | // y - up to 5% of the background height away from the center
165 | // z - up to 2% of the maximum zoom from the center
166 | // zr - up to 1 degree of rotation from the center
167 | camera.sway({ x: 0.1, y: 0.05, z: 0.02, zr: 1 }, {
168 | duration: 1.5,
169 | easing: Easings.Quadratic.InOut,
170 | loop: true,
171 | });
172 | ```
173 |
174 | The state of the camera can be queried:
175 | ```js
176 | // the current position of the camera, excluding offsets from the position offset and swaying.
177 | const position = camera.position;
178 | // the current offset of the camera.
179 | const positionOffset = camera.positionOffset;
180 |
181 | // cancel any in-progress movement
182 | if (camera.isMoving()) {
183 | camera.move(false);
184 | }
185 |
186 | // cancel any in-progress rotation
187 | if (camera.isRotating()) {
188 | camera.rotate(false);
189 | }
190 |
191 | // cancel any in-progress swaying
192 | if (camera.isSwaying()) {
193 | camera.sway(false);
194 | }
195 | ```
196 |
197 | ### Effects
198 | Each background comes with its own effects. The `BackgroundRenderer` also exposes a global `effects` object that is applied on top of all backgrounds.
199 |
200 | > **⚠️NOTE:** The global `BackgroundRenderer` effects object does not support the following effect: `EffectType.MotionBlur`
201 |
202 | > **⚠️NOTE:** Be careful when storing effect references! When switching to a new background, a new set of effects will be created for it. Previously configured effects are not transferred.
203 | >
204 | > If you don't need different effects on multiple backgrounds or do expect to switch backgrounds often, consider using the `BackgroundRenderer`'s effects instead.
205 |
206 | ```js
207 | import { BackgroundRenderer, EffectType } from 'midori-bg';
208 |
209 | const renderer = new BackgroundRenderer(document.getElementById('canvas'));
210 |
211 | // the global effects object - effects set here apply to all backgrounds
212 | const { effects: globalEffects } = renderer;
213 | globalEffects.set(EffectType.Vignette, { darkness: 1, offset: 1 });
214 |
215 | // the background effects object - effects set here apply only to the specific background
216 | const { effects } = renderer.background;
217 | effects.set(EffectType.MotionBlur, { intensity: 1, samples: 32 });
218 | effects.set(EffectType.RgbShift, { amount: 0.005, angle: 135 });
219 | effects.set(EffectType.VignetteBlur, { size: 3, radius: 1.5, passes: 2 });
220 | ```
221 |
222 | The state of the effects can be queried:
223 | ```js
224 | // get a copy of the current effects configurations
225 | const configs = effects.getConfigs();
226 |
227 | // remove an effect
228 | if (globalEffects.hasEffect(EffectType.Vignette)) {
229 | globalEffects.remove(EffectType.Vignette);
230 | }
231 |
232 | // remove all effects
233 | if (effects.hasEffects()) {
234 | effects.removeAll();
235 | }
236 | ```
237 |
238 | The configuration options for effects:
239 |
240 | > **⚠️NOTE:** Effects that involve blurring such as `EffectType.Blur`, `EffectType.VignetteBlur`, and `EffectType.MotionBlur` can potentially be expensive. It is important to balance visual quality and performance when using such effects.
241 |
242 | ```ts
243 | interface BlurEffectConfig {
244 | // the size of the blur.
245 | radius?: number;
246 | // the number of blur passes - more passes result in stronger blurs and less artifacts at the cost of performance.
247 | passes?: number;
248 | }
249 |
250 | interface MotionBlurEffectConfig {
251 | // the intensity of the blur.
252 | intensity?: number;
253 | // the number of samples for the blur - more samples result in better quality at the cost of performance.
254 | samples?: number;
255 | }
256 |
257 | interface BloomEffectConfig {
258 | // the overall brightness of the bloom.
259 | opacity?: number;
260 | // the size of the bloom.
261 | radius?: number;
262 | // the number of bloom passes - more passes result in stronger blooms and less artifacts at the cost of performance.
263 | passes?: number;
264 | }
265 |
266 | interface RgbShiftEffectConfig {
267 | // the distance of the shift.
268 | amount?: number;
269 | // the angle of the shift in degrees.
270 | angle?: number;
271 | }
272 |
273 | interface VignetteEffectConfig {
274 | // the size of the vignette.
275 | offset?: number;
276 | // the intensity of the vignette.
277 | darkness?: number;
278 | }
279 |
280 | interface VignetteBlurEffectConfig {
281 | // the size of the vignette.
282 | size?: number;
283 | // the size of the blur.
284 | radius?: number;
285 | // the number of blur passes - more passes result in stronger blurs and less artifacts at the cost of performance.
286 | passes?: number;
287 | }
288 | ```
289 |
290 | ### Particles
291 | Each background comes with its own particles. The particles can be grouped, moved, and swayed independently.
292 |
293 | > **⚠️NOTE:** Be careful when storing particle references! When switching to a new background, a new particles object will be created. Settings configured on the previous particles are not transferred.
294 |
295 | ```js
296 | import { BackgroundRenderer, Easings } from 'midori-bg';
297 |
298 | const renderer = new BackgroundRenderer(document.getElementById('canvas'));
299 |
300 | const { particles } = renderer.background;
301 |
302 | // generate two named groups of particles in the background.
303 | particles.generate([
304 | {
305 | name: 'small',
306 | amount: 200,
307 | maxSize: 5,
308 | maxOpacity: 0.8,
309 | minGradient: 0.75,
310 | maxGradient: 1.0,
311 | color: 0xffffff,
312 | smoothing: 0.6,
313 | },
314 | {
315 | name: 'large',
316 | amount: 30,
317 | minSize: 100,
318 | maxSize: 125,
319 | maxOpacity: 0.05,
320 | minGradient: 1.0,
321 | maxGradient: 1.0,
322 | color: 0xffffff,
323 | },
324 | ]);
325 |
326 | // move the particles by a distance and angle in degrees with a transition.
327 | particles.move('small', { distance: 0.5, angle: 25 }, { duration: 5, loop: true });
328 | particles.move('large', { distance: 0.4, angle: 35 }, { duration: 5, loop: true });
329 |
330 | // sway the particles up to a given distance with a transition.
331 | particles.sway('small', { x: 0.025, y: 0.025 }, { duration: 1.5, easing: Easings.Sinusoidal.InOut, loop: true });
332 | particles.sway('large', { x: 0.025, y: 0.025 }, { duration: 1.5, easing: Easings.Sinusoidal.InOut, loop: true });
333 |
334 | // removes all particles.
335 | particles.removeAll();
336 | ```
337 |
338 | The state of the particles can also be queried:
339 | ```js
340 | // get a copy of the current particles configuration.
341 | const configs = particles.getConfigs();
342 |
343 | // cancel any in-progress movement
344 | if (particles.isMoving('small')) {
345 | particles.move('small', false);
346 | }
347 |
348 | // cancel any in-progress swaying
349 | if (particles.isSwaying('large')) {
350 | camera.sway('large', false);
351 | }
352 | ```
353 |
354 | The configuration options for particles:
355 | ```ts
356 | interface ParticleGroupConfig {
357 | // the name of the particle group.
358 | name: string;
359 | // the number of particles to generate.
360 | amount: number;
361 | // the minimum size of the particles in world units. Defaults to 0.
362 | minSize?: number;
363 | // the maximum size of the particles in world units. Defaults to 0.
364 | maxSize?: number;
365 | // the minimum fade gradient of the particles in relative units (0 to 1). Defaults to 0.
366 | minGradient?: number;
367 | // the maximum fade gradient of the particles in relative units (0 to 1). Defaults to 1.
368 | maxGradient?: number;
369 | // the minimum opacity of the particles. Defaults to 0.
370 | minOpacity?: number;
371 | // the maximum opacity of the particles. Defaults to 1.
372 | maxOpacity?: number;
373 | // optional color of the particles. Defaults to 0xffffff.
374 | color?: number;
375 | // the amount of smoothing for animated values (i.e size, gradient, opacity), specified as a value between 0 and 1. Defaults to 0.5.
376 | smoothing?: number;
377 | }
378 | ```
379 |
380 | ### Animation Callbacks & Easings
381 | Callbacks can be passed in for transitions in backgrounds, cameras, and particles. Certain transitions are loopable.
382 | ```ts
383 | interface TransitionConfig {
384 | // the duration of the transition in seconds.
385 | duration?: number;
386 | // an optional delay before the transition starts in seconds.
387 | delay?: number;
388 | // an optional easing function for the transition.
389 | easing?: (k: number) => number;
390 | // an optional callback - invoked when the transition is registered, regardless of delay.
391 | onInit?: (...args: any[]) => void;
392 | // an optional callback - invoked when the transition starts after the delay has elapsed.
393 | onStart?: (...args: any[]) => void;
394 | // an optional callback - invoked for each frame that the transition runs.
395 | onUpdate?: (...args: any[]) => void;
396 | // an optional callback - invoked when the transition has finished.
397 | onComplete?: (...args: any[]) => void;
398 | // an optional callback - invoked when the transition is interrupted or stopped.
399 | onStop?: (...args: any[]) => void;
400 | }
401 |
402 | interface LoopableTransitionConfig extends TransitionConfig {
403 | // whether to loop the transition repeatedly or not.
404 | loop?: boolean;
405 | }
406 |
407 | interface BackgroundTransitionConfig extends TransitionConfig {
408 | onInit?: (prevBackground?: Background, nextBackground?: Background) => void;
409 | onStart?: (prevBackground?: Background, nextBackground?: Background) => void;
410 | onUpdate?: (prevBackground?: Background, nextBackground?: Background) => void;
411 | onComplete?: (prevBackground?: Background, nextBackground?: Background) => void;
412 | onStop?: (prevBackground?: Background, nextBackground?: Background) => void;
413 | }
414 | ```
415 |
416 | A set of easing functions are available via the `Easings` import. A custom easing function can also be provided if desired.
417 | ```js
418 | import { BackgroundRenderer, Easings } from 'midori-bg';
419 |
420 | const renderer = new BackgroundRenderer(document.getElementById('canvas'));
421 | const { camera } = renderer.background;
422 |
423 | // move the camera with a transition using a pre-defined easing.
424 | camera.move({ x: Math.random(), y: Math.random(), z: 0.5 + Math.random() * 0.5 }, {
425 | duration: 2.5,
426 | easing: Easings.Cubic.InOut,
427 | });
428 |
429 | // move the camera with a transition using a custom easing.
430 | camera.move({ x: Math.random(), y: Math.random(), z: 0.5 + Math.random() * 0.5 }, {
431 | duration: 2.5,
432 | easing: k => k * 2,
433 | });
434 | ```
435 |
436 | Optional callbacks can be utilized for more advanced transitions (e.g sequencing camera movements).
437 | ```js
438 | import { BackgroundRenderer, TransitionType, Easings, SlideDirection } from 'midori-bg';
439 |
440 | const renderer = new BackgroundRenderer(document.getElementById('canvas'));
441 |
442 | loadImage('url/to/image')
443 | .then((image) => {
444 | // set a new background with a slide transition.
445 | renderer.setBackground(image, {
446 | type: TransitionType.Slide,
447 | config: {
448 | slides: 2,
449 | intensity: 5,
450 | duration: 1.5,
451 | easing: Easings.Quintic.InOut,
452 | direction: SlideDirection.Right,
453 | },
454 | // the previous and next background are available in the optional transition callbacks
455 | // you can use transition callbacks to do more advanced transitions (e.g sequencing camera movements)
456 | onStart: (prevBackground, nextBackground) => {
457 | prevBackground.camera.move({ x: Math.random(), y: Math.random(), z: 0.3 + Math.random() * 0.7 }, {
458 | duration: 2.5,
459 | easing: Easings.Quartic.In,
460 | });
461 | prevBackground.camera.rotate(-5 + Math.random() * 10, {
462 | duration: 2.5,
463 | easing: Easings.Quartic.In,
464 | });
465 | nextBackground.camera.move({ x: Math.random(), y: Math.random(), z: 0.7 + Math.random() * 0.3 }, {
466 | duration: 2,
467 | easing: Easings.Quartic.Out,
468 | });
469 | nextBackground.camera.sway({ x: 0.1, y: 0.05, z: 0.02, zr: 1 }, {
470 | duration: 1.5,
471 | easing: Easings.Quadratic.InOut,
472 | loop: true,
473 | });
474 | nextBackground.camera.rotate(-5 + Math.random() * 10, {
475 | duration: 2,
476 | easing: Easings.Quartic.Out,
477 | });
478 | },
479 | });
480 | })
481 | // handle errors
482 | .catch(err => console.error(err));
483 | ```
484 |
485 | ### Cleanup
486 | Midori allocates resources that are not automatically disposed. Make sure to always clean-up properly when finished:
487 | ```jsx
488 | import { BackgroundRenderer } from 'midori-bg';
489 |
490 | const renderer = new BackgroundRenderer(document.getElementById('canvas'));
491 | renderer.dispose();
492 | ```
493 |
494 | ### Full API
495 | For the full API, see the [typings file](./dist/midori.d.ts).
496 |
497 | ## Contributing
498 | Contributions are welcome! Feel free to submit issues or PRs for any bugs or feature requests.
499 |
500 | To get started, run `npm run dev` and navigate to `localhost:8080` to launch the interactive demo. Any changes made to the source will be hot reloaded in the demo.
501 |
502 | ## License
503 | See the [license file](./LICENSE).
504 |
--------------------------------------------------------------------------------
/dist/midori.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by dts-bundle-generator v5.9.0
2 |
3 | import { DepthTexture, Mesh, MeshBasicMaterial, PerspectiveCamera, PlaneGeometry, Points, Texture, WebGLRenderTarget, WebGLRenderer } from 'three';
4 | import { Pass } from 'three/examples/jsm/postprocessing/Pass';
5 |
6 | export declare const Easings: {
7 | Linear: {
8 | None: (amount: number) => number;
9 | };
10 | Quadratic: {
11 | In: (amount: number) => number;
12 | Out: (amount: number) => number;
13 | InOut: (amount: number) => number;
14 | };
15 | Cubic: {
16 | In: (amount: number) => number;
17 | Out: (amount: number) => number;
18 | InOut: (amount: number) => number;
19 | };
20 | Quartic: {
21 | In: (amount: number) => number;
22 | Out: (amount: number) => number;
23 | InOut: (amount: number) => number;
24 | };
25 | Quintic: {
26 | In: (amount: number) => number;
27 | Out: (amount: number) => number;
28 | InOut: (amount: number) => number;
29 | };
30 | Sinusoidal: {
31 | In: (amount: number) => number;
32 | Out: (amount: number) => number;
33 | InOut: (amount: number) => number;
34 | };
35 | Exponential: {
36 | In: (amount: number) => number;
37 | Out: (amount: number) => number;
38 | InOut: (amount: number) => number;
39 | };
40 | Circular: {
41 | In: (amount: number) => number;
42 | Out: (amount: number) => number;
43 | InOut: (amount: number) => number;
44 | };
45 | Elastic: {
46 | In: (amount: number) => number;
47 | Out: (amount: number) => number;
48 | InOut: (amount: number) => number;
49 | };
50 | Back: {
51 | In: (amount: number) => number;
52 | Out: (amount: number) => number;
53 | InOut: (amount: number) => number;
54 | };
55 | Bounce: {
56 | In: (amount: number) => number;
57 | Out: (amount: number) => number;
58 | InOut: (amount: number) => number;
59 | };
60 | };
61 | export interface TransitionConfig {
62 | duration?: number;
63 | delay?: number;
64 | easing?: (k: number) => number;
65 | onInit?: (...args: any[]) => void;
66 | onStart?: (...args: any[]) => void;
67 | onUpdate?: (...args: any[]) => void;
68 | onComplete?: (...args: any[]) => void;
69 | onStop?: (...args: any[]) => void;
70 | }
71 | export interface LoopableTransitionConfig extends TransitionConfig {
72 | loop?: boolean;
73 | }
74 | export interface BackgroundTransitionConfig extends TransitionConfig {
75 | onInit?: (prevBackground: Background, nextBackground: Background) => void;
76 | onStart?: (prevBackground: Background, nextBackground: Background) => void;
77 | onUpdate?: (prevBackground: Background, nextBackground: Background) => void;
78 | onComplete?: (prevBackground: Background, nextBackground: Background) => void;
79 | onStop?: (prevBackground: Background, nextBackground: Background) => void;
80 | }
81 | export interface CameraPosition {
82 | x?: number;
83 | y?: number;
84 | z?: number;
85 | }
86 | export interface CameraPositionWithRotation extends CameraPosition {
87 | zr?: number;
88 | }
89 | export declare type CameraOffset = CameraPositionWithRotation;
90 | export declare class BackgroundCamera {
91 | private _plane;
92 | readonly camera: PerspectiveCamera;
93 | private readonly _position;
94 | private readonly _positionOffset;
95 | private readonly _positionWithOffset;
96 | private _positionTransition;
97 | private _rotationTransition;
98 | private readonly _swayOffset;
99 | private _swayTransition;
100 | /**
101 | * Constructs a BackgroundCamera using a Background's plane.
102 | * @param {PlaneMesh} plane - a three.js plane mesh representing the background.
103 | * @param {Number} width - the width of the camera.
104 | * @param {Number} height - the height of the camera.
105 | */
106 | constructor(plane: PlaneMesh, width: number, height: number);
107 | /**
108 | * Returns the current position of the camera.
109 | * @returns CameraPositionWithRotation
110 | */
111 | get position(): CameraPositionWithRotation;
112 | /**
113 | * Returns the current position offset of the camera.
114 | * @returns CameraPositionWithRotation
115 | */
116 | get positionOffset(): CameraPositionWithRotation;
117 | /**
118 | * Returns whether the camera is currently moving.
119 | * @returns boolean
120 | */
121 | isMoving(): boolean;
122 | /**
123 | * Returns whether the camera is currently rotating.
124 | * @returns boolean
125 | */
126 | isRotating(): boolean;
127 | /**
128 | * Returns whether the camera is currently swaying.
129 | * @returns boolean
130 | */
131 | isSwaying(): boolean;
132 | /**
133 | * Sets the size of the camera.
134 | * @param {number} width
135 | * @param {number} height
136 | */
137 | setSize(width: number, height: number): void;
138 | /**
139 | * Offsets the camera position.
140 | * @param {CameraPositionWithRotation} offset - the offset to apply.
141 | */
142 | offset(offset: CameraPositionWithRotation): void;
143 | /**
144 | * Sways the camera around its position. Cancels any in-progress sways.
145 | * @param {CameraOffset | boolean} offset - the offset to sway on each axis in relative units from 0 to 1.
146 | * The rotation offset (zr) must be specified in units of degrees.
147 | * The x/y offsets should be set based off a z of 1 and will be scaled down appropriately based on the camera's current z position.
148 | * If a boolean is passed in instead then the sway will either continue or stop based on the value.
149 | * @param {LoopableTransitionConfig} transition - optional configuration for a transition.
150 | */
151 | sway(offset: CameraOffset | boolean, transition?: LoopableTransitionConfig): void;
152 | /**
153 | * Rotates the camera on its z-axis. Cancels any in-progress rotations.
154 | * @param {number | boolean} angle - the angle to rotate in degrees.
155 | * If a boolean is passed in instead then the rotation will either continue or stop based on the value.
156 | * @param {TransitionConfig} transition - optional configuration for a transition.
157 | */
158 | rotate(angle: number | boolean, transition?: TransitionConfig): void;
159 | /**
160 | * Moves the camera to a relative position on the background. Cancels any in-progress moves.
161 | * @param {CameraPosition | boolean} position - the position to move towards on each axis in relative units from 0 to 1.
162 | * If a boolean is passed in instead then the move will either continue or stop based on the value.
163 | * @param {TransitionConfig} transition - optional configuration for a transition.
164 | */
165 | move(position: CameraPosition | boolean, transition?: TransitionConfig): void;
166 | /**
167 | * Updates the camera position. Should be called on every render frame.
168 | */
169 | update(): void;
170 | /**
171 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
172 | */
173 | dispose(): void;
174 | }
175 | export declare type Uniforms = {
176 | [uniform: string]: any;
177 | };
178 | export declare enum EffectType {
179 | Blur = "Blur",
180 | Bloom = "Bloom",
181 | RgbShift = "RgbShift",
182 | Vignette = "Vignette",
183 | VignetteBlur = "VignetteBlur",
184 | MotionBlur = "MotionBlur",
185 | Glitch = "Glitch"
186 | }
187 | export interface IEffect {
188 | render(...args: any[]): any;
189 | setSize?(width: number, height: number): any;
190 | getUniforms(): Uniforms;
191 | updateUniforms(uniforms: Uniforms): any;
192 | clearUniforms(): any;
193 | dispose(): any;
194 | }
195 | export declare type EffectTypeConfig> = {
196 | [EffectType.Blur]: BlurEffectConfig;
197 | [EffectType.Bloom]: BloomEffectConfig;
198 | [EffectType.RgbShift]: RgbShiftEffectConfig;
199 | [EffectType.Vignette]: VignetteEffectConfig;
200 | [EffectType.VignetteBlur]: VignetteBlurEffectConfig;
201 | [EffectType.Glitch]: GlitchEffectConfig;
202 | }[T];
203 | export declare type EffectConfigs = {
204 | [T in Exclude]?: EffectTypeConfig;
205 | };
206 | export declare type EffectMap = Partial>;
207 | export interface BlurEffectConfig {
208 | radius?: number;
209 | passes?: number;
210 | }
211 | export interface BloomEffectConfig {
212 | opacity?: number;
213 | radius?: number;
214 | passes?: number;
215 | }
216 | export interface RgbShiftEffectConfig {
217 | amount?: number;
218 | angle?: number;
219 | }
220 | export interface VignetteEffectConfig {
221 | offset?: number;
222 | darkness?: number;
223 | }
224 | export interface VignetteBlurEffectConfig {
225 | size?: number;
226 | radius?: number;
227 | passes?: number;
228 | }
229 | export interface GlitchEffectConfig {
230 | amount?: number;
231 | seed?: number;
232 | }
233 | export declare class EffectPass extends Pass {
234 | private _width;
235 | private _height;
236 | private _readBuffer;
237 | private _writeBuffer;
238 | private _copyShader;
239 | protected _effects: EffectMap;
240 | /**
241 | * Constructs an EffectPass.
242 | * @param {number} width
243 | * @param {number} height
244 | */
245 | constructor(width: number, height: number);
246 | /**
247 | * Sets the size of the EffectPass.
248 | * @param {number} width
249 | * @param {number} height
250 | */
251 | setSize(width: number, height: number): void;
252 | /**
253 | * Returns the configurations for the currently set effects.
254 | * @returns EffectConfigs
255 | */
256 | getConfigs(): EffectConfigs;
257 | /**
258 | * Returns whether a specified effect is currently set.
259 | * @param {EffectType} type
260 | * @returns boolean
261 | */
262 | hasEffect(type: EffectType): boolean;
263 | /**
264 | * Returns whether any effects are currently set.
265 | * @returns boolean
266 | */
267 | hasEffects(): boolean;
268 | /**
269 | * Returns the current effect for the specified type.
270 | * If no effect is currently set for the type, creates a new effect for the type and returns it.
271 | * @param {EffectType} type
272 | * @param {EffectConfig} config
273 | * @returns IEffect
274 | */
275 | protected _getEffect(type: EffectType): IEffect;
276 | /**
277 | * Sets an effect. If an effect is already set, updates the set effect.
278 | * @param {EffectType} type - the effect to set.
279 | * @param {Object} config - configuration specific to the effect specified.
280 | */
281 | set>(type: T, config?: EffectTypeConfig): void;
282 | /**
283 | * Removes a set effect. Returns true if the effect was removed, otherwise false.
284 | * @param {EffectType} type - the type of the effect.
285 | * @returns boolean
286 | */
287 | remove(type: EffectType): boolean;
288 | /**
289 | * Removes all set effects.
290 | */
291 | removeAll(): void;
292 | /**
293 | * Swaps the internal read and write buffers. Should be called each time after rendering an effect.
294 | */
295 | private _swapBuffers;
296 | /**
297 | * Renders the effects.
298 | * @param {WebGLRenderer} renderer - the renderer to use.
299 | * @param {WebGLRenderTarget | null} writeBuffer - the buffer to render to, or null to render directly to screen.
300 | * @param {WebGLRenderTarget} readBuffer - the buffer to read from.
301 | */
302 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget | null, readBuffer: WebGLRenderTarget): void;
303 | /**
304 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
305 | */
306 | dispose(): void;
307 | }
308 | export declare type BackgroundEffectTypeConfig = {
309 | [EffectType.Blur]: BlurEffectConfig;
310 | [EffectType.Bloom]: BloomEffectConfig;
311 | [EffectType.RgbShift]: RgbShiftEffectConfig;
312 | [EffectType.Vignette]: VignetteEffectConfig;
313 | [EffectType.VignetteBlur]: VignetteBlurEffectConfig;
314 | [EffectType.MotionBlur]: MotionBlurEffectConfig;
315 | [EffectType.Glitch]: GlitchEffectConfig;
316 | }[T];
317 | export interface BackgroundEffectConfigs extends EffectConfigs {
318 | [EffectType.MotionBlur]?: MotionBlurEffectConfig;
319 | }
320 | export interface MotionBlurEffectConfig {
321 | intensity?: number;
322 | samples?: number;
323 | }
324 | export declare class BackgroundEffects extends EffectPass {
325 | private _camera;
326 | private _depthTexture;
327 | /**
328 | * Constructs a BackgroundEffects object.
329 | * @param {number} width
330 | * @param {number} height
331 | * @param {PerspectiveCamera} camera - a camera for motion blur support
332 | * @param {DepthTexture} depthTexture - a depth texture for motion blur support
333 | */
334 | constructor(width: number, height: number, camera: PerspectiveCamera, depthTexture: DepthTexture);
335 | /**
336 | * Returns the configurations for the currently set effects.
337 | * @returns BackgroundEffectConfigs
338 | */
339 | getConfigs(): BackgroundEffectConfigs;
340 | /**
341 | * Returns the current effect for the specified type.
342 | * If no effect is currently set for the type, creates a new effect for the type and returns it.
343 | * @param {EffectType} type
344 | * @param {EffectConfig} config
345 | * @returns IEffect
346 | */
347 | protected _getEffect(type: EffectType): IEffect;
348 | /**
349 | * Sets an effect. If an effect is already set, updates the set effect.
350 | * @param {EffectType} type - the effect to set.
351 | * @param {Object} config - configuration specific to the effect specified.
352 | */
353 | set(type: T, config?: BackgroundEffectTypeConfig): void;
354 | }
355 | export interface ParticleMoveOffset {
356 | distance: number;
357 | angle: number;
358 | }
359 | export interface ParticleSwayOffset {
360 | x: number;
361 | y: number;
362 | }
363 | export declare type ParticleGroupConfigs = {
364 | [name: string]: ParticleGroupConfig;
365 | };
366 | export interface ParticleGroupConfig {
367 | name: string;
368 | amount: number;
369 | minSize?: number;
370 | maxSize?: number;
371 | minGradient?: number;
372 | maxGradient?: number;
373 | minOpacity?: number;
374 | maxOpacity?: number;
375 | color?: number;
376 | smoothing?: number;
377 | }
378 | export declare class Particles {
379 | private _width;
380 | private _height;
381 | private _maxDepth;
382 | private _groups;
383 | private _particles;
384 | private _positions;
385 | /**
386 | * Constructs a Particles object.
387 | * @param {number} width
388 | * @param {number} height
389 | * @param {number} maxDepth - the maximum depth of the particles in world units.
390 | */
391 | constructor(width: number, height: number, maxDepth: number);
392 | /**
393 | * Returns the configurations for the currently set particle groups.
394 | * @returns ParticleGroupDefinitionMap
395 | */
396 | getConfigs(): ParticleGroupConfigs;
397 | /**
398 | * Returns whether a group of particles is currently moving.
399 | * @param {string} name - the name of the particle group.
400 | * @returns boolean
401 | */
402 | isMoving(name: string): boolean;
403 | /**
404 | * Returns whether a group of particles is currently swaying.
405 | * @param {string} name - the name of the particle group.
406 | * @returns boolean
407 | */
408 | isSwaying(name: string): boolean;
409 | /**
410 | * Generates particles based on a given set of configurations.
411 | * @param {ParticleGroupConfig | ParticleGroupConfig[]} config - a single or array of particle group configurations.
412 | */
413 | generate(configs: ParticleGroupConfig | ParticleGroupConfig[]): void;
414 | /**
415 | * Removes all particle groups.
416 | */
417 | removeAll(): void;
418 | /**
419 | * Calculates a new position based off an existing position and optional offset. Will wrap around boundaries.
420 | * @param {Vector2} position - the current position.
421 | * @param {Vector2} offset - the offset from the current position.
422 | * @returns Vector2
423 | */
424 | private _getNewPosition;
425 | /**
426 | * Updates the internal positions for particles. This does NOT update the attributes of the BufferGeometry.
427 | * @param {number} index - the index to start at.
428 | * @param {number} amount - the number of particles.
429 | * @param {number[]} positions - an array containing the position values to use.
430 | * @param {Vector2} offset - an optional offset to apply to all new position values.
431 | */
432 | private _updatePositions;
433 | /**
434 | * Moves a group of particles. Cancels any in-progress moves.
435 | * @param {string} name - the name of the group to move.
436 | * @param {ParticleMoveOffset | boolean} offset - the distance and angle in radians to move.
437 | * If a boolean is passed in instead then the move will either continue or stop based on the value.
438 | * @param {LoopableTransitionConfig} transition - an optional transition configuration.
439 | */
440 | move(name: string, offset: ParticleMoveOffset | boolean, transition?: LoopableTransitionConfig): void;
441 | /**
442 | * Sways a group of particles around their current positions. Cancels any in-progress sways.
443 | * @param {string} name - the name of the group to sway.
444 | * @param {ParticleSwayOffset | boolean} offset - the distances in world units allowed on each axis for swaying.
445 | * If a boolean is passed in instead then the sway will either continue or stop based on the value.
446 | * @param {LoopableTransitionConfig} transition - optional configuration for a transition.
447 | */
448 | sway(name: string, offset: ParticleSwayOffset | boolean, transition?: LoopableTransitionConfig): void;
449 | /**
450 | * Generates a new random averaged value based off a given value and its range.
451 | * @param {number} prevValue - the previous value.
452 | * @param {number} minValue - the minimum value for the given value.
453 | * @param {number} maxValue - the maximum value for the given value.
454 | * @param {number} smoothing - optional amount of smoothing to use as a value between 0 and 1. Defaults to 0.5.
455 | * @returns number
456 | */
457 | private _generateNewRandomAveragedValue;
458 | /**
459 | * Updates the positions of the particles. Should be called on every render frame.
460 | */
461 | update(): void;
462 | /**
463 | * Returns a three.js object containing the particles.
464 | * To use the particles, add this object into a three.js scene.
465 | * @returns Points
466 | */
467 | get object(): Points;
468 | /**
469 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
470 | */
471 | dispose(): void;
472 | }
473 | export interface PlaneMesh extends Mesh {
474 | geometry: PlaneGeometry;
475 | material: MeshBasicMaterial;
476 | }
477 | export declare class Background {
478 | private readonly _buffer;
479 | private readonly _plane;
480 | private readonly _scene;
481 | readonly camera: BackgroundCamera;
482 | readonly particles: Particles;
483 | readonly effects: BackgroundEffects;
484 | /**
485 | * Constructs a background.
486 | * @param {Texture | null} texture
487 | * @param {number} width
488 | * @param {number} height
489 | */
490 | constructor(texture: Texture | null, width: number, height: number);
491 | /**
492 | * Returns the texture of the background.
493 | * @returns {Texture | null}
494 | */
495 | get texture(): Texture | null;
496 | /**
497 | * Sets the size of the background.
498 | * @param {number} width
499 | * @param {number} height
500 | */
501 | setSize(width: number, height: number): void;
502 | /**
503 | * Renders the background.
504 | * @param {WebGLRenderer} renderer - the renderer to use.
505 | * @param {WebGLRenderTarget | null} writeBuffer - the buffer to render to, or null to render directly to screen.
506 | */
507 | render(renderer: WebGLRenderer, writeBuffer?: WebGLRenderTarget | null): void;
508 | /**
509 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
510 | */
511 | dispose(): void;
512 | }
513 | /**
514 | * @author aeroheim / http://aeroheim.moe/
515 | */
516 | export declare enum WipeDirection {
517 | Left = 0,
518 | Right = 1,
519 | Top = 2,
520 | Bottom = 3
521 | }
522 | /**
523 | * @author aeroheim / http://aeroheim.moe/
524 | */
525 | export declare enum SlideDirection {
526 | Left = 0,
527 | Right = 1,
528 | Top = 2,
529 | Bottom = 3
530 | }
531 | export declare enum TransitionType {
532 | None = "None",
533 | Blend = "Blend",
534 | Blur = "Blur",
535 | Wipe = "Wipe",
536 | Slide = "Slide",
537 | Glitch = "Glitch"
538 | }
539 | export interface BlendTransitionConfig extends BackgroundTransitionConfig {
540 | }
541 | export interface WipeTransitionConfig extends BackgroundTransitionConfig {
542 | gradient?: number;
543 | angle?: number;
544 | direction?: WipeDirection;
545 | }
546 | export interface SlideTransitionConfig extends BackgroundTransitionConfig {
547 | slides?: number;
548 | intensity?: number;
549 | samples?: number;
550 | direction?: SlideDirection;
551 | }
552 | export interface BlurTransitionConfig extends BackgroundTransitionConfig {
553 | intensity?: number;
554 | samples?: number;
555 | }
556 | export interface GlitchTransitionConfig extends BackgroundTransitionConfig {
557 | seed?: number;
558 | }
559 | export declare type Transition = BlendTransition | WipeTransition | SlideTransition | BlurTransition | GlitchTransition;
560 | export interface BlendTransition extends BackgroundTransitionConfig {
561 | type: TransitionType.Blend;
562 | config: BlendTransitionConfig;
563 | }
564 | export interface WipeTransition extends BackgroundTransitionConfig {
565 | type: TransitionType.Wipe;
566 | config: WipeTransitionConfig;
567 | }
568 | export interface SlideTransition extends BackgroundTransitionConfig {
569 | type: TransitionType.Slide;
570 | config: SlideTransitionConfig;
571 | }
572 | export interface BlurTransition extends BackgroundTransitionConfig {
573 | type: TransitionType.Blur;
574 | config: BlurTransitionConfig;
575 | }
576 | export interface GlitchTransition extends BackgroundTransitionConfig {
577 | type: TransitionType.Glitch;
578 | config: GlitchTransitionConfig;
579 | }
580 | /**
581 | * Returns whether WebGL support is available.
582 | * @returns boolean
583 | */
584 | export declare function isWebGLSupported(): boolean;
585 | /**
586 | * Loads an image as a texture.
587 | * @param {string} path - path to the image file.
588 | * @return Promise - texture on success, error on failure.
589 | */
590 | export declare function loadImage(path: string): Promise;
591 | export interface BackgroundRendererOptions {
592 | autoRender?: boolean;
593 | }
594 | export declare class BackgroundRenderer {
595 | private _renderer;
596 | private _composer;
597 | private _background;
598 | private _backgroundPass;
599 | private _transitionPass;
600 | private _effectPass;
601 | private _clock;
602 | private _renderAnimationFrame?;
603 | private _paused;
604 | private _disposed;
605 | /**
606 | * Constructs a renderer.
607 | * @param {HTMLCanvasElement} canvas - the canvas element to use.
608 | * @param {BackgroundRendererOptions} options - options for the renderer.
609 | */
610 | constructor(canvas: HTMLCanvasElement, options?: BackgroundRendererOptions);
611 | /**
612 | * Returns the global effects.
613 | * Effects set on this will apply to all backgrounds.
614 | * @returns EffectPass
615 | */
616 | get effects(): EffectPass;
617 | /**
618 | * Returns the current background.
619 | * @returns Background
620 | */
621 | get background(): Background;
622 | /**
623 | * Returns whether the background is currently transitioning.
624 | * @returns boolean
625 | */
626 | isTransitioning(): boolean;
627 | /**
628 | * Sets the current background.
629 | * @param {Texture} texture - the image to use for the background.
630 | * @param {Transition} transition - optional configuration for a transition.
631 | */
632 | setBackground(texture: Texture, transition?: Transition): void;
633 | /**
634 | * Resizes the canvas if necessary. Should be called on every render frame.
635 | */
636 | private _resizeCanvas;
637 | /**
638 | * Begins rendering the background.
639 | */
640 | render(): void;
641 | /**
642 | * Pauses rendering of the background.
643 | */
644 | pause(): void;
645 | /**
646 | * Returns whether the renderer is paused.
647 | * @returns {boolean}
648 | */
649 | get isPaused(): boolean;
650 | /**
651 | * Renders the background, transitions, and effects. Should be called on every frame.
652 | */
653 | private _render;
654 | /**
655 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
656 | */
657 | dispose(): void;
658 | }
659 |
660 | export {};
661 |
--------------------------------------------------------------------------------
/dist/midori.js:
--------------------------------------------------------------------------------
1 | import midori from"./midori.cjs";export const{BackgroundRenderer,loadImage,isWebGLSupported,Background,BackgroundCamera,BackgroundEffects,EffectPass,Particles,TransitionType,EffectType,SlideDirection,WipeDirection,Easings}=midori.midori;
--------------------------------------------------------------------------------
/docs/assets/0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aeroheim/midori/dce2770f5969a36f7f9a19a37fdcd4364dde1adb/docs/assets/0.jpg
--------------------------------------------------------------------------------
/docs/assets/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aeroheim/midori/dce2770f5969a36f7f9a19a37fdcd4364dde1adb/docs/assets/1.jpg
--------------------------------------------------------------------------------
/docs/assets/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aeroheim/midori/dce2770f5969a36f7f9a19a37fdcd4364dde1adb/docs/assets/2.jpg
--------------------------------------------------------------------------------
/docs/assets/midori.1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aeroheim/midori/dce2770f5969a36f7f9a19a37fdcd4364dde1adb/docs/assets/midori.1.gif
--------------------------------------------------------------------------------
/docs/assets/midori.2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aeroheim/midori/dce2770f5969a36f7f9a19a37fdcd4364dde1adb/docs/assets/midori.2.gif
--------------------------------------------------------------------------------
/docs/assets/midori.3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aeroheim/midori/dce2770f5969a36f7f9a19a37fdcd4364dde1adb/docs/assets/midori.3.gif
--------------------------------------------------------------------------------
/docs/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400&family=Manrope&display=swap');
2 |
3 | html, body {
4 | height: 100%;
5 | border: none;
6 |
7 | font-family: 'Inter', sans-serif;
8 | font-size: 16px;
9 | color: white;
10 | }
11 |
12 | body {
13 | position: relative;
14 | margin: 0;
15 | padding: 0;
16 | }
17 |
18 | span {
19 | font-weight: 300;
20 | }
21 |
22 | .canvas {
23 | z-index: -1;
24 | position: absolute;
25 |
26 | width: 100vw;
27 | height: 100vh;
28 | margin: 0;
29 | padding: 0;
30 | }
31 |
32 | .content {
33 | z-index: 0;
34 | position: absolute;
35 | box-sizing: border-box;
36 | top: 0;
37 | left: 0;
38 |
39 | width: 500px;
40 | min-width: 350px;
41 | max-width: 50%;
42 | height: 100%;
43 |
44 | padding: 30px;
45 |
46 | background: rgba(0, 0, 0, 0.8);
47 | overflow-y: auto;
48 | user-select: none;
49 | }
50 |
51 | .rule {
52 | margin-top: 15px;
53 | margin-bottom: 15px;
54 | opacity: 0.1;
55 | }
56 |
57 | .section-header {
58 | margin: 0;
59 |
60 | font-size: 32px;
61 | font-family: Manrope;
62 | }
63 |
64 | .page-header > h1 {
65 | font-size: 48px;
66 | }
67 |
68 | .content-header {
69 | padding-left: 12px;
70 | padding-right: 12px;
71 | border-left: 4px solid chartreuse;
72 | }
73 |
74 | .github-ref {
75 | margin-left: 10px;
76 |
77 | color: white;
78 | text-decoration: none;
79 | }
80 |
81 | .github-icon {
82 | width: 32px;
83 | height: 32px;
84 | }
85 |
86 | .images-layout {
87 | display: grid;
88 | grid-template-rows: 1fr 1fr 1fr;
89 | grid-template-columns: minmax(164px, auto) 1fr;
90 | grid-template-areas:
91 | "index title"
92 | "index artist"
93 | "index nav";
94 |
95 | width: fit-content;
96 | }
97 |
98 | .index {
99 | grid-area: index;
100 |
101 | margin: 0;
102 |
103 | font-size: 96px;
104 | font-weight: 300;
105 | }
106 |
107 | .link {
108 | width: fit-content;
109 |
110 | color: white;
111 |
112 | white-space: nowrap;
113 | overflow-x: hidden;
114 | text-overflow: ellipsis;
115 |
116 | text-decoration: underline;
117 | text-decoration-color: transparent;
118 | text-underline-offset: 2px;
119 | text-decoration-thickness: 1px;
120 |
121 | opacity: 0.5;
122 | transition: all 0.15s ease-out;
123 | }
124 |
125 | .link:hover {
126 | text-decoration-color: white;
127 | opacity: 1;
128 | }
129 |
130 | .title {
131 | grid-area: title;
132 | align-self: flex-end;
133 |
134 | margin: 0;
135 | }
136 |
137 | .artist {
138 | grid-area: artist;
139 |
140 | margin: 0;
141 | }
142 |
143 | .nav {
144 | grid-area: nav;
145 |
146 | display: grid;
147 | grid-template-columns: auto auto 1fr;
148 | grid-auto-flow: column;
149 | column-gap: 10px;
150 |
151 | width: fit-content;
152 | }
153 |
154 | .nav-icon {
155 | cursor: pointer;
156 | fill: white;
157 |
158 | opacity: 0.5;
159 | transition: opacity 0.2s ease;
160 | }
161 |
162 | .nav-icon:hover {
163 | opacity: 1;
164 | }
165 |
166 | .options-layout {
167 | display: grid;
168 | grid-template-columns: repeat(auto-fill, 120px);
169 | row-gap: 5px;
170 | column-gap: 5px;
171 |
172 | margin-top: 15px;
173 | width: 100%;
174 | }
175 |
176 | .select-item {
177 | cursor: pointer;
178 |
179 | min-width: 50px;
180 | padding: 10px;
181 |
182 | text-align: center;
183 | font-size: 14px;
184 |
185 | background-color: rgba(0, 0, 0, 0.3);
186 | border-radius: 12px;
187 |
188 | transition: all 0.2s ease;
189 | }
190 |
191 | .select-item:hover {
192 | color: black;
193 | background-color: rgba(255, 255, 255, 0.5);
194 | }
195 |
196 | .select-item-active {
197 | color: black;
198 | background-color: rgba(255, 255, 255) !important;
199 | }
200 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | midori
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement, useEffect, useRef, useState } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Texture } from 'three';
4 | import { BackgroundRenderer, Background, loadImage, TransitionType, EffectType, Easings, WipeDirection, SlideDirection } from '../dist/midori';
5 |
6 | function getEffectTypeString(effectType: EffectType) {
7 | switch (effectType) {
8 | case EffectType.RgbShift:
9 | return 'RGB Shift';
10 | case EffectType.MotionBlur:
11 | return 'Motion Blur';
12 | case EffectType.VignetteBlur:
13 | return 'Vignette Blur';
14 | default:
15 | return effectType;
16 | }
17 | }
18 |
19 | function getTransitionConfig(type: TransitionType) {
20 | switch (type) {
21 | case TransitionType.Blend:
22 | return {
23 | duration: 1.5,
24 | easing: Easings.Quartic.InOut,
25 | };
26 | case TransitionType.Wipe:
27 | return {
28 | duration: 1.5,
29 | easing: Easings.Quartic.InOut,
30 | gradient: 0.5,
31 | angle: 15,
32 | direction: WipeDirection[Object.keys(WipeDirection)[Math.floor(Math.random() * Object.keys(WipeDirection).length)]],
33 | };
34 | case TransitionType.Blur:
35 | return {
36 | duration: 1,
37 | easing: Easings.Quintic.InOut,
38 | intensity: 1.5,
39 | };
40 | case TransitionType.Slide:
41 | return {
42 | duration: 1.5,
43 | easing: Easings.Quintic.InOut,
44 | slides: 2,
45 | intensity: 5,
46 | direction: SlideDirection[Object.keys(SlideDirection)[Math.floor(Math.random() * Object.keys(SlideDirection).length)]],
47 | };
48 | case TransitionType.Glitch:
49 | return {
50 | seed: Math.random(),
51 | duration: 1.5,
52 | easing: Easings.Cubic.InOut,
53 | };
54 | default:
55 | return {};
56 | }
57 | }
58 |
59 | function setBackgroundEffects(background: Background, effects: EffectType[]) {
60 | const { effects: backgroundEffects } = background;
61 | backgroundEffects.removeAll();
62 | for (const effect of effects) {
63 | switch (effect) {
64 | case EffectType.Blur:
65 | backgroundEffects.set(EffectType.Blur, { radius: 1.5, passes: 2 });
66 | break;
67 | case EffectType.MotionBlur:
68 | backgroundEffects.set(EffectType.MotionBlur, { intensity: 1, samples: 32 });
69 | break;
70 | case EffectType.Bloom:
71 | backgroundEffects.set(EffectType.Bloom, { radius: 1, passes: 2 });
72 | break;
73 | case EffectType.RgbShift:
74 | backgroundEffects.set(EffectType.RgbShift, { amount: 0.005, angle: 135 });
75 | break;
76 | case EffectType.Vignette:
77 | backgroundEffects.set(EffectType.Vignette, { darkness: 1, offset: 1 });
78 | break;
79 | case EffectType.VignetteBlur:
80 | backgroundEffects.set(EffectType.VignetteBlur, { size: 3, radius: 1.5, passes: 2 });
81 | break;
82 | }
83 | }
84 | }
85 |
86 | function setBackgroundParticles(background: Background) {
87 | const { particles } = background;
88 | particles.generate([
89 | {
90 | name: 'small',
91 | amount: 200,
92 | maxSize: 5,
93 | maxOpacity: 0.8,
94 | minGradient: 0.75,
95 | maxGradient: 1.0,
96 | },
97 | {
98 | name: 'medium',
99 | amount: 50,
100 | maxSize: 12,
101 | maxOpacity: 0.8,
102 | minGradient: 0.75,
103 | maxGradient: 1.0,
104 | smoothing: 0.8,
105 | },
106 | {
107 | name: 'large',
108 | amount: 30,
109 | minSize: 100,
110 | maxSize: 125,
111 | maxOpacity: 0.04,
112 | minGradient: 1.0,
113 | maxGradient: 1.0,
114 | smoothing: 0.65,
115 | },
116 | ]);
117 | particles.move('small', { distance: 0.5, angle: 25 }, { duration: 5, loop: true });
118 | particles.move('medium', { distance: 0.3, angle: 45 }, { duration: 5, loop: true });
119 | particles.move('large', { distance: 0.4, angle: 35 }, { duration: 5, loop: true });
120 | particles.sway('small', { x: 0.025, y: 0.025 }, { duration: 1.5, easing: Easings.Sinusoidal.InOut, loop: true });
121 | particles.sway('medium', { x: 0.025, y: 0.025 }, { duration: 1.5, easing: Easings.Sinusoidal.InOut, loop: true });
122 | particles.sway('large', { x: 0.025, y: 0.025 }, { duration: 1.5, easing: Easings.Sinusoidal.InOut, loop: true });
123 | }
124 |
125 | function setRendererBackground(renderer: BackgroundRenderer, background: Texture, transition: TransitionType) {
126 | const delay = 1.25;
127 | renderer.setBackground(background, {
128 | type: transition,
129 | config: {
130 | ...getTransitionConfig(transition),
131 | delay,
132 | onInit: (prevBackground, nextBackground) => {
133 | prevBackground.camera.move({ x: Math.random(), y: Math.random(), z: 0.3 + Math.random() * 0.7 }, {
134 | duration: 2.5,
135 | easing: Easings.Quartic.In,
136 | });
137 | prevBackground.camera.rotate(-5 + Math.random() * 10, {
138 | duration: 2.5,
139 | easing: Easings.Quartic.In,
140 | });
141 | nextBackground.camera.move({ x: Math.random(), y: Math.random(), z: 0.7 + Math.random() * 0.3 }, {
142 | duration: 2,
143 | delay,
144 | easing: Easings.Quartic.Out,
145 | });
146 | nextBackground.camera.sway({ x: 0.1, y: 0.05, z: 0.02, zr: 1 }, {
147 | duration: 3,
148 | easing: Easings.Quadratic.InOut,
149 | loop: true,
150 | });
151 | nextBackground.camera.rotate(-5 + Math.random() * 10, {
152 | duration: 2,
153 | delay,
154 | easing: Easings.Quartic.Out,
155 | });
156 | },
157 | }
158 | });
159 |
160 | setBackgroundParticles(renderer.background);
161 | }
162 |
163 | interface SectionProps {
164 | className?: string;
165 | label: string;
166 | icon?: ReactElement;
167 | rule?: boolean;
168 | children: ReactElement | ReactElement[];
169 | }
170 |
171 | function Section(props: SectionProps): ReactElement {
172 | const {
173 | className = '',
174 | label, icon,
175 | rule = false,
176 | children
177 | } = props;
178 |
179 | return (
180 | <>
181 |
182 |
183 | {label}
184 | {icon}
185 |
186 | {children}
187 |
188 | {rule ?
: null}
189 | >
190 | );
191 | }
192 |
193 | interface Images {
194 | image: Texture;
195 | title: string;
196 | artist: string;
197 | profile: string;
198 | source: string;
199 | }
200 |
201 | interface ExampleProps {
202 | images: Images[];
203 | }
204 |
205 | function Example(props: ExampleProps): ReactElement {
206 | const { images } = props;
207 |
208 | const [canvasRef, setCanvasRef] = useState(null);
209 | const [renderer, setRenderer] = useState();
210 | useEffect(() => {
211 | if (canvasRef !== null) {
212 | const backgroundRenderer = new BackgroundRenderer(canvasRef);
213 | setRenderer(backgroundRenderer);
214 | return () => backgroundRenderer.dispose();
215 | }
216 | }, [images, canvasRef]);
217 |
218 | const transitionRef = useRef(TransitionType.Wipe);
219 | const [transition, setTransition] = useState(transitionRef.current);
220 | useEffect(() => {
221 | transitionRef.current = transition;
222 | }, [transition]);
223 |
224 | const [index, setIndex] = useState(0);
225 | useEffect(() => {
226 | if (renderer !== undefined) {
227 | setRendererBackground(renderer, images[index].image, transitionRef.current);
228 | }
229 | }, [images, index, renderer]);
230 |
231 | const [effects, setEffects] = useState([ EffectType.Bloom, EffectType.MotionBlur, EffectType.Vignette, EffectType.VignetteBlur ]);
232 | useEffect(() => {
233 | if (renderer !== undefined) {
234 | setBackgroundEffects(renderer.background, effects);
235 | }
236 | }, [effects, index, renderer]);
237 |
238 | const onNextBackground = () => {
239 | if (renderer !== undefined && !renderer.isTransitioning()) {
240 | setIndex((index + 1) % images.length);
241 | }
242 | };
243 |
244 | const onPrevBackground = () => {
245 | if (renderer !== undefined && !renderer.isTransitioning()) {
246 | setIndex(index - 1 < 0 ? images.length - 1 : index - 1);
247 | }
248 | };
249 |
250 | const onCameraMove = () => {
251 | if (renderer !== undefined) {
252 | const { camera } = renderer.background;
253 | if (!camera.isMoving() && !camera.isRotating()) {
254 | camera.move({ x: Math.random(), y: Math.random(), z: 0.5 + Math.random() * 0.5 }, {
255 | duration: 2.5,
256 | easing: Easings.Cubic.InOut,
257 | });
258 | camera.rotate(-5 + Math.random() * 10, {
259 | duration: 2.5,
260 | easing: Easings.Cubic.InOut,
261 | });
262 | }
263 | }
264 | };
265 |
266 | const onTransitionSet = (transitionType: TransitionType) => {
267 | setTransition(transitionType);
268 | }
269 |
270 | const onEffectSet = (effectType: EffectType) => {
271 | if (effects.includes(effectType)) {
272 | setEffects(effects.filter(x => x !== effectType));
273 | } else {
274 | setEffects([ ...effects, effectType ]);
275 | }
276 | };
277 |
278 | const { source, title, artist, profile } = images[index];
279 | return (
280 | <>
281 |
282 |
283 |
289 |
292 |
293 | }>
294 | library for animated image backgrounds
295 |
296 |
297 | example image backgrounds
298 |
299 |
{`${index + 1}/${images.length}`}
300 |
{title}
301 |
{artist}
302 |
314 |
315 |
316 |
317 | animated transitions between backgrounds
318 |
319 | {[ TransitionType.Blend, TransitionType.Wipe, TransitionType.Blur, TransitionType.Slide, TransitionType.Glitch ].map(transitionType => (
320 |
onTransitionSet(transitionType)}
324 | >
325 | {transitionType}
326 |
327 | ))}
328 |
329 |
330 |
331 | post-processing effects for backgrounds
332 |
333 | {[ EffectType.Bloom, EffectType.Blur, EffectType.MotionBlur, EffectType.RgbShift, EffectType.Vignette, EffectType.VignetteBlur ].map(effectType => (
334 |
onEffectSet(effectType)}
338 | >
339 | {getEffectTypeString(effectType)}
340 |
341 | ))}
342 |
343 |
344 |
345 | >
346 | );
347 | }
348 |
349 | Promise.all([
350 | loadImage('assets/0.jpg').then(image => ({
351 | image,
352 | title: '夜を歩いて',
353 | artist: 'みふる',
354 | profile: 'https://www.pixiv.net/en/users/488766',
355 | source: 'https://www.pixiv.net/en/artworks/71306825',
356 | })),
357 | loadImage('assets/1.jpg').then(image => ({
358 | image,
359 | title: '「何考えてるんです?」',
360 | artist: 'ちた',
361 | profile: 'https://www.pixiv.net/en/users/6437284',
362 | source: 'https://www.pixiv.net/en/artworks/78237071',
363 | })),
364 | loadImage('assets/2.jpg').then(image => ({
365 | image,
366 | title: 'Midnight Stroll',
367 | artist: 'Wenqing Yan',
368 | profile: 'https://www.yuumeiart.com/',
369 | source: 'https://www.yuumeiart.com/#/midnight-stroll/',
370 | })),
371 | ])
372 | .then(images => {
373 | ReactDOM.render(, document.getElementById('root'));
374 | })
375 | .catch(e => console.error(`Failed to load assets: ${e}`));
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "midori-bg",
3 | "version": "1.0.3",
4 | "description": "Animated image backgrounds",
5 | "license": "MIT",
6 | "keywords": [
7 | "background",
8 | "image",
9 | "animation",
10 | "webgl",
11 | "three",
12 | "threejs",
13 | "canvas",
14 | "javascript"
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/aeroheim/midori.git"
19 | },
20 | "homepage": "https://github.com/aeroheim/midori",
21 | "bugs": "https://github.com/aeroheim/midori/issues",
22 | "author": {
23 | "name": "Benjamin Pang",
24 | "email": "bp7936@gmail.com",
25 | "url": "https://aeroheim.moe/"
26 | },
27 | "main": "./dist/midori.cjs",
28 | "module": "./dist/midori.js",
29 | "types": "./dist/midori.d.ts",
30 | "files": [
31 | "/dist/midori.cjs",
32 | "/dist/midori.js",
33 | "/dist/midori.d.ts"
34 | ],
35 | "scripts": {
36 | "prepublishOnly": "npm run build-prod",
37 | "dev": "npm run clean && npm run types && concurrently \"webpack -w --config webpack.dev.js --config-name lib\" \"webpack-dev-server --config webpack.dev.js --config-name docs\"",
38 | "prod": "npm run clean && npm run types && concurrently \"webpack -w --config webpack.prod.js --config-name lib\" \"webpack-dev-server --config webpack.prod.js --config-name docs\"",
39 | "build-dev": "npm run clean && npm run types && webpack --config webpack.dev.js --config-name lib && webpack --config webpack.dev.js --config-name docs",
40 | "build-prod": "npm run clean && npm run types && webpack --config webpack.prod.js --config-name lib && webpack --config webpack.prod.js",
41 | "clean": "rimraf ./dist && rimraf ./docs/dist",
42 | "types": "dts-bundle-generator --project tsconfig.json ./src/midori.ts -o ./dist/midori.d.ts --external-imports three --external-types",
43 | "lint": "tsc --project tsconfig.json --noEmit && eslint src/*/**.ts"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.13.8",
47 | "@babel/plugin-proposal-class-properties": "^7.13.0",
48 | "@babel/plugin-transform-runtime": "^7.13.9",
49 | "@babel/preset-env": "^7.13.9",
50 | "@babel/preset-react": "^7.12.13",
51 | "@babel/preset-typescript": "^7.13.0",
52 | "@types/react": "^17.0.19",
53 | "@types/react-dom": "^17.0.9",
54 | "@types/three": "^0.131.0",
55 | "@typescript-eslint/eslint-plugin": "^5.5.0",
56 | "@typescript-eslint/parser": "^5.5.0",
57 | "babel-loader": "^8.2.2",
58 | "concurrently": "^6.2.1",
59 | "copy-webpack-plugin": "^9.0.1",
60 | "dts-bundle-generator": "^5.7.0",
61 | "eslint": "^8.3.0",
62 | "eslint-plugin-import": "^2.22.1",
63 | "eslint-plugin-react": "^7.25.1",
64 | "eslint-plugin-react-hooks": "^4.2.0",
65 | "eslint-webpack-plugin": "^3.0.1",
66 | "react": "^17.0.1",
67 | "react-dom": "^17.0.1",
68 | "rimraf": "^3.0.2",
69 | "three": "^0.132.2",
70 | "typescript": "^4.2.3",
71 | "webpack": "^5.51.2",
72 | "webpack-cli": "^4.8.0",
73 | "webpack-dev-server": "^4.1.0",
74 | "webpack-merge": "^5.8.0"
75 | },
76 | "dependencies": {
77 | "@babel/runtime": "^7.13.9",
78 | "@tweenjs/tween.js": "^18.6.4"
79 | },
80 | "peerDependencies": {
81 | "three": ">=0.132.2"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/background-camera-utils.ts:
--------------------------------------------------------------------------------
1 | import { PerspectiveCamera, MathUtils, Vector4 } from 'three';
2 | import { PlaneMesh } from './background';
3 |
4 | /**
5 | * Returns the visible height at the given depth in world units.
6 | * @param {number} absoluteZ - the depth in absolute world units.
7 | * @param {PerspectiveCamera} camera - a three.js PerspectiveCamera.
8 | * @returns number
9 | */
10 | function getVisibleHeightAtDepth(absoluteZ: number, camera: PerspectiveCamera): number {
11 | // fov is vertical fov in radians
12 | return 2 * Math.tan(MathUtils.degToRad(camera.fov) / 2) * absoluteZ;
13 | }
14 |
15 | /**
16 | * Returns the visible width at the given depth in world units.
17 | * @param {number} absoluteZ - the depth in absolute world units.
18 | * @param {PerspectiveCamera} camera - a three.js PerspectiveCamera.
19 | * @returns number
20 | */
21 | function getVisibleWidthAtDepth(absoluteZ: number, camera: PerspectiveCamera): number {
22 | return getVisibleHeightAtDepth(absoluteZ, camera) * camera.aspect;
23 | }
24 |
25 | /**
26 | * Adapted from https://stackoverflow.com/questions/16702966/rotate-image-and-crop-out-black-borders/16778797#16778797.
27 | *
28 | * Given a rectangle of size w x h that has been rotated by 'angle' (in
29 | * radians), computes and returns the width and height of the largest possible
30 | * axis-aligned rectangle (maximal area) within the rotated rectangle.
31 | *
32 | * @param {number} width - the width of the rectangle.
33 | * @param {number} height - the height of the rectangle.
34 | * @param {number} angleInRadians - the angle to rotate in radians.
35 | * @returns Object - { width: number; height: number }
36 | */
37 | function getInnerBoundedBoxForRect(width: number, height: number, angleInRadians = 0): { width: number; height: number } {
38 | const widthIsLonger = width >= height;
39 | const longSide = widthIsLonger ? width : height;
40 | const shortSide = widthIsLonger ? height : width;
41 | const sinAngle = Math.abs(Math.sin(angleInRadians));
42 | const cosAngle = Math.abs(Math.cos(angleInRadians));
43 |
44 | // since the solutions for angle, -angle and 180-angle are all the same,
45 | // it suffices to look at the first quadrant and the absolute values of sin,cos:
46 | if ((shortSide <= 2 * sinAngle * cosAngle * longSide) || (Math.abs(sinAngle - cosAngle) < 1e-10)) {
47 | // half constrained case: two crop corners touch the longer side,
48 | // the other two corners are on the mid-line parallel to the longer line
49 | const x = 0.5 * shortSide;
50 | return {
51 | width: widthIsLonger ? x / sinAngle : x / cosAngle,
52 | height: widthIsLonger ? x / cosAngle : x / sinAngle,
53 | };
54 | }
55 |
56 | // fully constrained case: crop touches all 4 sides
57 | const cosDoubleAngle = cosAngle * cosAngle - sinAngle * sinAngle;
58 | return {
59 | width: (width * cosAngle - height * sinAngle) / cosDoubleAngle,
60 | height: (height * cosAngle - width * sinAngle) / cosDoubleAngle,
61 | };
62 | }
63 |
64 | /**
65 | * Returns the maximum depth for a plane such that it is still fullscreen.
66 | * @param {PlaneMesh} plane - a three.js plane mesh.
67 | * @param {PerspectiveCamera} camera - a three.js PerspectiveCamera.
68 | * @param {number} rotateZ - the z-axis rotation angle of the camera in radians.
69 | * @returns number
70 | */
71 | function getMaxFullScreenDepthForPlane(plane: PlaneMesh, camera: PerspectiveCamera, rotateZ: number): number {
72 | // When the camera is rotated, we treat the object as if it were rotated instead and
73 | // use the width/height of the maximal inner bounded box that fits within the object.
74 | // This ensures that the maximum depth calculated will always allow for the object to be
75 | // fullscreen even if rotated.
76 | // NOTE: if there is no rotation (i.e 0 degs) then the object's width and height will be used as normal.
77 | const { width: rectWidth, height: rectHeight } = plane.geometry.parameters;
78 | const { width, height } = getInnerBoundedBoxForRect(rectWidth, rectHeight, rotateZ);
79 |
80 | const verticalFovConstant = 2 * Math.tan(MathUtils.degToRad(camera.fov) / 2);
81 | const maxDepthForWidth = width / (verticalFovConstant * camera.aspect);
82 | const maxDepthForHeight = height / verticalFovConstant;
83 |
84 | // NOTE: this depth assumes the camera is centered on the object.
85 | return Math.min(maxDepthForWidth, maxDepthForHeight) + plane.position.z;
86 | }
87 |
88 | /**
89 | * Returns the visible width and height at the given depth in world units.
90 | * @param {PlaneMesh} plane - a three.js plane mesh.
91 | * @param {PerspectiveCamera} camera - a three.js PerspectiveCamera.
92 | * @param {number} relativeZ - value between 0 (max zoom-in) and 1 (max zoom-out) that represents the z position.
93 | * @param {number} rotateZ - the z-axis rotation angle of the camera in radians.
94 | * @returns Object - { width: number; height: number }
95 | */
96 | function getViewBox(plane: PlaneMesh, camera: PerspectiveCamera, relativeZ: number, rotateZ: number): { width: number; height: number } {
97 | const maxDepth = getMaxFullScreenDepthForPlane(plane, camera, rotateZ);
98 | const absoluteDepth = relativeZ * maxDepth;
99 | return {
100 | width: getVisibleWidthAtDepth(absoluteDepth, camera),
101 | height: getVisibleHeightAtDepth(absoluteDepth, camera),
102 | };
103 | }
104 |
105 | /**
106 | * Returns the available x and y distance a camera can be panned at the given depth in world units.
107 | * @param {PlaneMesh} plane - a three.js plane mesh.
108 | * @param {PerspectiveCamera} camera - a three.js PerspectiveCamera.
109 | * @param {number} relativeZ - value between 0 (max zoom-in) and 1 (max zoom-out) that represents the z position.
110 | * @param {number} rotateZ - the z-axis rotation angle of the camera in radians.
111 | * @returns Object - { width: number, height: number }
112 | */
113 | function getAvailablePanDistance(plane: PlaneMesh, camera: PerspectiveCamera, relativeZ: number, rotateZ: number) {
114 | const { width: rectWidth, height: rectHeight } = plane.geometry.parameters;
115 | const { width, height } = getInnerBoundedBoxForRect(rectWidth, rectHeight, rotateZ);
116 | const viewBox = getViewBox(plane, camera, relativeZ, rotateZ);
117 | return {
118 | width: width - viewBox.width,
119 | height: height - viewBox.height,
120 | };
121 | }
122 |
123 | /**
124 | * Converts a relative vector to an absolute vector for a given plane and camera.
125 | * @param {PlaneMesh} plane - a three.js plane mesh.
126 | * @param {PerspectiveCamera} camera - a three.js PerspectiveCamera.
127 | * @param {Vector4} relativePosition - a vector that represents the relative camera position to convert from.
128 | * The rotation component of the vector MUST be in units of radians.
129 | * @returns Vector4
130 | */
131 | function toAbsolutePosition(plane: PlaneMesh, camera: PerspectiveCamera, relativePosition: Vector4): Vector4 {
132 | const { x, y, z, w: zr } = relativePosition;
133 |
134 | const panDistance = getAvailablePanDistance(plane, camera, z, zr);
135 | // offset the viewbox's position so that it starts at the top-left corner, then move it
136 | // based on the relative proportion to the available x and y distance the viewbox can be moved.
137 | const absoluteX = -(panDistance.width / 2) + (x * panDistance.width);
138 | const absoluteY = (panDistance.height / 2) - (y * panDistance.height);
139 | const absoluteDepth = getMaxFullScreenDepthForPlane(plane, camera, zr) * z;
140 |
141 | return new Vector4(
142 | // Make sure to rotate the x/y positions to get the actual correct positions relative to the camera rotation.
143 | absoluteX * Math.cos(zr) - absoluteY * Math.sin(zr),
144 | absoluteX * Math.sin(zr) + absoluteY * Math.cos(zr),
145 | absoluteDepth,
146 | zr,
147 | );
148 | }
149 |
150 | export {
151 | getMaxFullScreenDepthForPlane,
152 | toAbsolutePosition,
153 | }
--------------------------------------------------------------------------------
/src/background-camera.ts:
--------------------------------------------------------------------------------
1 |
2 | import { PerspectiveCamera, Vector4, MathUtils } from 'three';
3 | import { Tween, Easing } from '@tweenjs/tween.js';
4 | import { getMaxFullScreenDepthForPlane, toAbsolutePosition } from './background-camera-utils';
5 | import { PlaneMesh } from './background';
6 | import { TransitionConfig, LoopableTransitionConfig } from './transition';
7 | import { clamp } from './utils';
8 |
9 | interface CameraPosition {
10 | // the x postion of the camera from 0 to 1, or the left to right-most position respectively.
11 | x?: number;
12 | // the y position of the camera from 0 to 1, or the top to bottom-most position respectively.
13 | y?: number;
14 | // the z position of the camera from 0 to 1, or the closest to farther position respectively.
15 | z?: number;
16 | }
17 |
18 | interface CameraPositionWithRotation extends CameraPosition {
19 | // the z-axis rotation of the camera in degrees.
20 | zr?: number;
21 | }
22 |
23 | type CameraOffset = CameraPositionWithRotation;
24 |
25 | interface CameraPositionTween {
26 | x: number,
27 | y: number,
28 | z: number
29 | }
30 |
31 | interface CameraRotationTween {
32 | zr: number;
33 | }
34 |
35 | interface CameraSwayTween {
36 | offsetX: number;
37 | offsetY: number;
38 | offsetZ: number;
39 | offsetZR: number;
40 | }
41 |
42 | // Max camera zoom range - this ensures the camera doesn't exceed the near plane of its frustum.
43 | const CameraZoomRange = 0.9;
44 |
45 | class BackgroundCamera {
46 | private _plane: PlaneMesh;
47 | public readonly camera: PerspectiveCamera;
48 |
49 | // the relative position of the camera
50 | // NOTE: the w component is used as the z-axis rotation component of the vector (also aliased as zr)
51 | private readonly _position: Vector4 = new Vector4(0, 0, 1, 0);
52 | private readonly _positionOffset: Vector4 = new Vector4(0, 0, 0, 0);
53 | private readonly _positionWithOffset: Vector4 = this._position.clone(); // cached for re-use per render frame
54 | private _positionTransition: Tween = new Tween({ x: 0, y: 0, z: 0 });
55 | private _rotationTransition: Tween = new Tween({ zr: 0 });
56 |
57 | private readonly _swayOffset = new Vector4(0, 0, 0, 0);
58 | private _swayTransition: Tween = new Tween({ offsetX: 0, offsetY: 0, offsetZ: 0, offsetZR: 0 });
59 |
60 | /**
61 | * Constructs a BackgroundCamera using a Background's plane.
62 | * @param {PlaneMesh} plane - a three.js plane mesh representing the background.
63 | * @param {Number} width - the width of the camera.
64 | * @param {Number} height - the height of the camera.
65 | */
66 | constructor(plane: PlaneMesh, width: number, height: number) {
67 | this._plane = plane;
68 | this.camera = new PerspectiveCamera(35, width / height, 0.001);
69 | }
70 |
71 | /**
72 | * Returns the current position of the camera.
73 | * @returns CameraPositionWithRotation
74 | */
75 | get position(): CameraPositionWithRotation {
76 | // NOTE: the relative camera position is the base position and does NOT include offsets (e.g sway or offset).
77 | const { x, y, z, w: zr } = this._position;
78 | return { x, y, z, zr };
79 | }
80 |
81 | /**
82 | * Returns the current position offset of the camera.
83 | * @returns CameraPositionWithRotation
84 | */
85 | get positionOffset(): CameraPositionWithRotation {
86 | const { x, y, z, w: zr } = this._positionOffset;
87 | return { x, y, z, zr };
88 | }
89 |
90 | /**
91 | * Returns whether the camera is currently moving.
92 | * @returns boolean
93 | */
94 | isMoving(): boolean {
95 | return this._positionTransition.isPlaying();
96 | }
97 |
98 | /**
99 | * Returns whether the camera is currently rotating.
100 | * @returns boolean
101 | */
102 | isRotating(): boolean {
103 | return this._rotationTransition.isPlaying();
104 | }
105 |
106 | /**
107 | * Returns whether the camera is currently swaying.
108 | * @returns boolean
109 | */
110 | isSwaying(): boolean {
111 | return this._swayTransition.isPlaying();
112 | }
113 |
114 | /**
115 | * Sets the size of the camera.
116 | * @param {number} width
117 | * @param {number} height
118 | */
119 | setSize(width: number, height: number): void {
120 | this.camera.aspect = width / height;
121 | this.camera.updateProjectionMatrix();
122 | }
123 |
124 | /**
125 | * Offsets the camera position.
126 | * @param {CameraPositionWithRotation} offset - the offset to apply.
127 | */
128 | offset(offset: CameraPositionWithRotation): void {
129 | const { x = 0, y = 0, z = 0, zr = 0 } = offset;
130 | this._positionOffset.set(x, y, z, zr);
131 | }
132 |
133 | /**
134 | * Sways the camera around its position. Cancels any in-progress sways.
135 | * @param {CameraOffset | boolean} offset - the offset to sway on each axis in relative units from 0 to 1.
136 | * The rotation offset (zr) must be specified in units of degrees.
137 | * The x/y offsets should be set based off a z of 1 and will be scaled down appropriately based on the camera's current z position.
138 | * If a boolean is passed in instead then the sway will either continue or stop based on the value.
139 | * @param {LoopableTransitionConfig} transition - optional configuration for a transition.
140 | */
141 | sway(offset: CameraOffset | boolean, transition: LoopableTransitionConfig = {}): void {
142 | if (typeof offset === 'boolean') {
143 | if (!offset) {
144 | this._swayTransition.stop();
145 | }
146 | return;
147 | }
148 |
149 | this._swayTransition.stop();
150 | const {
151 | loop = false,
152 | duration = 0,
153 | delay = 0,
154 | easing = Easing.Linear.None,
155 | onInit = () => ({}),
156 | onStart = () => ({}),
157 | onUpdate = () => ({}),
158 | onComplete = () => ({}),
159 | onStop = () => ({}),
160 | } = transition;
161 |
162 | const { x = 0, y = 0, z = 0, zr = 0 } = offset;
163 | const zrInRadians = MathUtils.degToRad(zr);
164 |
165 | // calculate offsets within range of available positions
166 | // NOTE: this doesn't guarantee that sway values won't be clamped since position and offsets can change over time
167 | // this is still useful enough however to ensure that we won't use sway values that will obviously get clamped
168 | const xPosition = clamp(this._position.x + this._positionOffset.x, 0, 1);
169 | const xMin = Math.max(0, xPosition - x);
170 | const xMax = Math.min(1, xPosition + x);
171 | const xRange = xMax - xMin;
172 | const xOffset = (xMin + xRange * Math.random()) - xPosition;
173 |
174 | const yPosition = clamp(this._position.y + this._positionOffset.y, 0, 1);
175 | const yMin = Math.max(0, yPosition - y);
176 | const yMax = Math.min(1, yPosition + y);
177 | const yRange = yMax - yMin;
178 | const yOffset = (yMin + yRange * Math.random()) - yPosition;
179 |
180 | const zPosition = clamp(this._position.z + this._positionOffset.z, 0, 1);
181 | const zMin = Math.max(0, zPosition - z);
182 | const zMax = Math.min(1, zPosition + z);
183 | const zRange = zMax - zMin;
184 | const zOffset = (zMin + zRange * Math.random()) - zPosition;
185 |
186 | onInit();
187 | this._swayTransition = new Tween({
188 | offsetX: this._swayOffset.x,
189 | offsetY: this._swayOffset.y,
190 | offsetZ: this._swayOffset.z,
191 | offsetZR: this._swayOffset.w,
192 | })
193 | .to({
194 | offsetX: xOffset,
195 | offsetY: yOffset,
196 | offsetZ: zOffset,
197 | offsetZR: -zrInRadians + Math.random() * zrInRadians * 2,
198 | }, duration * 1000)
199 | .easing(easing)
200 | .onStart(onStart)
201 | .onUpdate(({ offsetX, offsetY, offsetZ, offsetZR }) => {
202 | this._swayOffset.set(offsetX, offsetY, offsetZ, offsetZR);
203 | onUpdate();
204 | })
205 | .onComplete(() => {
206 | if (loop) {
207 | this.sway(offset, transition);
208 | }
209 | onComplete();
210 | })
211 | .onStop(onStop)
212 | .delay(delay * 1000)
213 | .start();
214 | }
215 |
216 | /**
217 | * Rotates the camera on its z-axis. Cancels any in-progress rotations.
218 | * @param {number | boolean} angle - the angle to rotate in degrees.
219 | * If a boolean is passed in instead then the rotation will either continue or stop based on the value.
220 | * @param {TransitionConfig} transition - optional configuration for a transition.
221 | */
222 | rotate(angle: number | boolean, transition: TransitionConfig = {}): void {
223 | if (typeof angle === 'boolean') {
224 | if (!angle) {
225 | this._rotationTransition.stop();
226 | }
227 | return;
228 | }
229 |
230 | this._rotationTransition.stop();
231 | const {
232 | duration = 0,
233 | delay = 0,
234 | easing = Easing.Linear.None,
235 | onInit = () => ({}),
236 | onStart = () => ({}),
237 | onUpdate = () => ({}),
238 | onComplete = () => ({}),
239 | onStop = () => ({}),
240 | } = transition;
241 | const angleInRadians = MathUtils.degToRad(angle);
242 |
243 | onInit();
244 | if (duration > 0 || delay > 0) {
245 | this._rotationTransition = new Tween({ zr: this._position.w })
246 | .to({ zr: angleInRadians }, duration * 1000)
247 | .easing(easing)
248 | .onStart(onStart)
249 | .onUpdate(({ zr }) => {
250 | this._position.set(this._position.x, this._position.y, this._position.z, zr);
251 | onUpdate();
252 | })
253 | .onComplete(onComplete)
254 | .onStop(onStop)
255 | .delay(delay * 1000)
256 | .start();
257 | } else {
258 | this._position.set(this._position.x, this._position.y, this._position.z, angleInRadians);
259 | }
260 | }
261 |
262 | /**
263 | * Moves the camera to a relative position on the background. Cancels any in-progress moves.
264 | * @param {CameraPosition | boolean} position - the position to move towards on each axis in relative units from 0 to 1.
265 | * If a boolean is passed in instead then the move will either continue or stop based on the value.
266 | * @param {TransitionConfig} transition - optional configuration for a transition.
267 | */
268 | move(position: CameraPosition | boolean, transition: TransitionConfig = {}): void {
269 | if (typeof position === 'boolean') {
270 | if (!position) {
271 | this._positionTransition.stop();
272 | }
273 | return;
274 | }
275 |
276 | this._positionTransition.stop();
277 | const { x: currentX, y: currentY, z: currentZ } = this._position;
278 | const { x = currentX, y = currentY, z = currentZ } = position;
279 | const {
280 | duration = 0,
281 | delay = 0,
282 | easing = Easing.Linear.None,
283 | onInit = () => ({}),
284 | onStart = () => ({}),
285 | onUpdate = () => ({}),
286 | onComplete = () => ({}),
287 | onStop = () => ({}),
288 | } = transition;
289 |
290 | onInit();
291 | if (duration > 0 || delay > 0) {
292 | this._positionTransition = new Tween({ x: currentX, y: currentY, z: currentZ })
293 | .to({ x, y, z }, duration * 1000)
294 | .easing(easing)
295 | .onStart(onStart)
296 | .onUpdate(({ x, y, z }) => {
297 | this._position.set(x, y, z, this._position.w);
298 | onUpdate();
299 | })
300 | .onComplete(onComplete)
301 | .onStop(onStop)
302 | .delay(delay * 1000)
303 | .start();
304 | } else {
305 | this._position.set(x, y, z, this._position.w);
306 | }
307 | }
308 |
309 | /**
310 | * Updates the camera position. Should be called on every render frame.
311 | */
312 | update(): void {
313 | // scale sway based on the current depth to provide a consistent distance regardless of depth
314 | const swayScale = this._positionWithOffset.z / getMaxFullScreenDepthForPlane(this._plane, this.camera, this.camera.rotation.z);
315 |
316 | this._positionWithOffset.set(
317 | clamp(this._position.x + this._positionOffset.x + this._swayOffset.x * swayScale, 0, 1),
318 | clamp(this._position.y + this._positionOffset.y + this._swayOffset.y * swayScale, 0, 1),
319 | clamp((this._position.z + this._positionOffset.z + this._swayOffset.z) * CameraZoomRange + (1.0 - CameraZoomRange), 0, 1),
320 | this._position.w + MathUtils.degToRad(this._positionOffset.w) + this._swayOffset.w,
321 | );
322 |
323 | const { x: absoluteX, y: absoluteY, z: absoluteDepth } = toAbsolutePosition(
324 | this._plane,
325 | this.camera,
326 | this._positionWithOffset,
327 | );
328 |
329 | this.camera.position.set(absoluteX, absoluteY, absoluteDepth);
330 | this.camera.rotation.z = this._position.w + MathUtils.degToRad(this._positionOffset.w) + this._swayOffset.w;
331 | }
332 |
333 | /**
334 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
335 | */
336 | dispose(): void {
337 | this.sway(false);
338 | this.move(false);
339 | this.rotate(false);
340 | }
341 | }
342 |
343 | export {
344 | CameraPosition,
345 | CameraPositionWithRotation,
346 | CameraOffset,
347 | BackgroundCamera,
348 | };
349 |
350 | export default BackgroundCamera;
351 |
--------------------------------------------------------------------------------
/src/background-effects.ts:
--------------------------------------------------------------------------------
1 | import { PerspectiveCamera, DepthTexture } from 'three';
2 | import { EffectPass, EffectConfig, EffectConfigs, BlurEffectConfig, BloomEffectConfig, RgbShiftEffectConfig, VignetteEffectConfig, VignetteBlurEffectConfig, GlitchEffectConfig } from './pipeline/effect-pass';
3 | import { EffectType, IEffect, MotionBlurEffect } from './effects/effect';
4 |
5 | type BackgroundEffectTypeConfig = {
6 | [EffectType.Blur]: BlurEffectConfig;
7 | [EffectType.Bloom]: BloomEffectConfig;
8 | [EffectType.RgbShift]: RgbShiftEffectConfig;
9 | [EffectType.Vignette]: VignetteEffectConfig;
10 | [EffectType.VignetteBlur]: VignetteBlurEffectConfig;
11 | [EffectType.MotionBlur]: MotionBlurEffectConfig;
12 | [EffectType.Glitch]: GlitchEffectConfig;
13 | }[T];
14 |
15 | interface BackgroundEffectConfigs extends EffectConfigs {
16 | [EffectType.MotionBlur]?: MotionBlurEffectConfig;
17 | }
18 |
19 | type BackgroundEffectConfig = EffectConfig | MotionBlurEffectConfig;
20 |
21 | interface MotionBlurEffectConfig {
22 | // the intensity of the blur.
23 | intensity?: number;
24 | // the number of samples for the blur - more samples result in better quality at the cost of performance.
25 | samples?: number;
26 | }
27 |
28 | class BackgroundEffects extends EffectPass {
29 | // properties cached for motion blur support
30 | private _camera: PerspectiveCamera;
31 | private _depthTexture: DepthTexture;
32 |
33 | /**
34 | * Constructs a BackgroundEffects object.
35 | * @param {number} width
36 | * @param {number} height
37 | * @param {PerspectiveCamera} camera - a camera for motion blur support
38 | * @param {DepthTexture} depthTexture - a depth texture for motion blur support
39 | */
40 | constructor(width: number, height: number, camera: PerspectiveCamera, depthTexture: DepthTexture) {
41 | super(width, height);
42 | this._camera = camera;
43 | this._depthTexture = depthTexture;
44 | }
45 |
46 | /**
47 | * Returns the configurations for the currently set effects.
48 | * @returns BackgroundEffectConfigs
49 | */
50 | getConfigs(): BackgroundEffectConfigs {
51 | const configs: BackgroundEffectConfigs = super.getConfigs()
52 | const motionBlurEffect = this._effects[EffectType.MotionBlur];
53 |
54 | if (motionBlurEffect) {
55 | const { intensity, samples } = motionBlurEffect.getUniforms();
56 | configs[EffectType.MotionBlur] = { intensity, samples };
57 | }
58 |
59 | return configs;
60 | }
61 |
62 | /**
63 | * Returns the current effect for the specified type.
64 | * If no effect is currently set for the type, creates a new effect for the type and returns it.
65 | * @param {EffectType} type
66 | * @param {EffectConfig} config
67 | * @returns IEffect
68 | */
69 | protected _getEffect(type: EffectType): IEffect {
70 | if (type === EffectType.MotionBlur && !(type in this._effects)) {
71 | this._effects[EffectType.MotionBlur] = new MotionBlurEffect(this._camera, this._depthTexture);
72 | return this._effects[EffectType.MotionBlur]!;
73 | }
74 |
75 | return super._getEffect(type);
76 | }
77 |
78 | /**
79 | * Sets an effect. If an effect is already set, updates the set effect.
80 | * @param {EffectType} type - the effect to set.
81 | * @param {Object} config - configuration specific to the effect specified.
82 | */
83 | set(type: T, config: BackgroundEffectTypeConfig = {}): void {
84 | if (type === EffectType.MotionBlur) {
85 | // enable this pass when there is at least one effect.
86 | this.enabled = true;
87 |
88 | const motionBlurEffect = this._getEffect(EffectType.MotionBlur);
89 | const { intensity = 1, samples = 32 } = config as MotionBlurEffectConfig;
90 | motionBlurEffect.updateUniforms({ intensity, samples });
91 | } else {
92 | super.set(type, config as EffectConfig);
93 | }
94 | }
95 | }
96 |
97 | export {
98 | BackgroundEffectConfig,
99 | MotionBlurEffectConfig,
100 | BackgroundEffectConfigs,
101 | BackgroundEffects,
102 | }
103 |
104 | export default BackgroundEffects;
105 |
--------------------------------------------------------------------------------
/src/background-renderer.ts:
--------------------------------------------------------------------------------
1 | import { WebGLRenderer, Texture, TextureLoader, ClampToEdgeWrapping, LinearFilter, Clock } from 'three';
2 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
3 | import { WEBGL } from 'three/examples/jsm/WebGL';
4 | import { update } from '@tweenjs/tween.js';
5 | import { Background } from './background';
6 | import { BackgroundPass } from './pipeline/background-pass';
7 | import { EffectPass } from './pipeline/effect-pass';
8 | import { TransitionPass, TransitionType, BlendTransitionConfig, WipeTransitionConfig, SlideTransitionConfig, BlurTransitionConfig, GlitchTransitionConfig } from './pipeline/transition-pass';
9 | import { BackgroundTransitionConfig } from './transition';
10 |
11 | type Transition = BlendTransition | WipeTransition | SlideTransition | BlurTransition | GlitchTransition;
12 |
13 | interface BlendTransition extends BackgroundTransitionConfig {
14 | type: TransitionType.Blend;
15 | config: BlendTransitionConfig;
16 | }
17 |
18 | interface WipeTransition extends BackgroundTransitionConfig {
19 | type: TransitionType.Wipe;
20 | config: WipeTransitionConfig;
21 | }
22 |
23 | interface SlideTransition extends BackgroundTransitionConfig {
24 | type: TransitionType.Slide;
25 | config: SlideTransitionConfig;
26 | }
27 |
28 | interface BlurTransition extends BackgroundTransitionConfig {
29 | type: TransitionType.Blur;
30 | config: BlurTransitionConfig;
31 | }
32 |
33 | interface GlitchTransition extends BackgroundTransitionConfig {
34 | type: TransitionType.Glitch;
35 | config: GlitchTransitionConfig;
36 | }
37 |
38 | /**
39 | * Returns whether WebGL support is available.
40 | * @returns boolean
41 | */
42 | function isWebGLSupported(): boolean {
43 | return WEBGL.isWebGLAvailable();
44 | }
45 |
46 | /**
47 | * Loads an image as a texture.
48 | * @param {string} path - path to the image file.
49 | * @return Promise - texture on success, error on failure.
50 | */
51 | function loadImage(path: string): Promise {
52 | return new Promise((resolve, reject) => {
53 | new TextureLoader().load(path, (texture) => {
54 | // image should never wrap
55 | texture.wrapS = ClampToEdgeWrapping;
56 | texture.wrapT = ClampToEdgeWrapping;
57 |
58 | // image should be able to be UV mapped directly
59 | texture.minFilter = LinearFilter;
60 |
61 | // image should never repeat
62 | texture.repeat.set(1, 1);
63 |
64 | resolve(texture);
65 | },
66 | () => ({}),
67 | errorEvent => reject(errorEvent.error ?? new Error('Failed to load requested image. Verify CORS policy or check if the image is valid.')));
68 | });
69 | }
70 |
71 | interface BackgroundRendererOptions {
72 | // whether to automatically begin rendering - defaults to true.
73 | autoRender?: boolean;
74 | }
75 |
76 | class BackgroundRenderer {
77 | private _renderer: WebGLRenderer;
78 | private _composer: EffectComposer;
79 | private _background: Background;
80 | private _backgroundPass: BackgroundPass;
81 | private _transitionPass: TransitionPass;
82 | private _effectPass: EffectPass;
83 | private _clock: Clock = new Clock(false);
84 | private _renderAnimationFrame?: number;
85 | private _paused = true;
86 | private _disposed = false;
87 |
88 | /**
89 | * Constructs a renderer.
90 | * @param {HTMLCanvasElement} canvas - the canvas element to use.
91 | * @param {BackgroundRendererOptions} options - options for the renderer.
92 | */
93 | constructor(canvas: HTMLCanvasElement, options: BackgroundRendererOptions = {}) {
94 | const { clientWidth: width, clientHeight: height } = canvas;
95 |
96 | // renderer
97 | this._renderer = new WebGLRenderer({ canvas, powerPreference: 'high-performance' });
98 | this._renderer.setSize(width, height, false);
99 |
100 | // pipeline
101 | this._composer = new EffectComposer(this._renderer);
102 | this._background = new Background(null, width, height);
103 | this._backgroundPass = new BackgroundPass(this._background);
104 | this._transitionPass = new TransitionPass(this._background, width, height);
105 | this._effectPass = new EffectPass(width, height);
106 | this._composer.addPass(this._backgroundPass);
107 | this._composer.addPass(this._transitionPass);
108 | this._composer.addPass(this._effectPass);
109 |
110 | this._render = this._render.bind(this);
111 |
112 | const { autoRender = true } = options;
113 | if (autoRender) {
114 | this.render();
115 | }
116 | }
117 |
118 | /**
119 | * Returns the global effects.
120 | * Effects set on this will apply to all backgrounds.
121 | * @returns EffectPass
122 | */
123 | get effects(): EffectPass {
124 | return this._effectPass;
125 | }
126 |
127 | /**
128 | * Returns the current background.
129 | * @returns Background
130 | */
131 | get background(): Background {
132 | return this._background;
133 | }
134 |
135 | /**
136 | * Returns whether the background is currently transitioning.
137 | * @returns boolean
138 | */
139 | isTransitioning(): boolean {
140 | return this._transitionPass.isTransitioning();
141 | }
142 |
143 | /**
144 | * Sets the current background.
145 | * @param {Texture} texture - the image to use for the background.
146 | * @param {Transition} transition - optional configuration for a transition.
147 | */
148 | setBackground(texture: Texture, transition?: Transition): void {
149 | const { clientWidth: width, clientHeight: height } = this._renderer.domElement;
150 | this._background = new Background(texture, width, height);
151 |
152 | if (transition) {
153 | const { type, config: { onStart = () => ({}), ...transitionConfig } } = transition;
154 | this._transitionPass.transition(this._background, type, {
155 | ...transitionConfig,
156 | onStart: (prevBackground, nextBackground) => {
157 | this._backgroundPass.setBackground(nextBackground);
158 | onStart(prevBackground, nextBackground);
159 | },
160 | });
161 | } else {
162 | this._backgroundPass.setBackground(this._background);
163 | this._transitionPass.transition(this._background, TransitionType.None);
164 | }
165 | }
166 |
167 | /**
168 | * Resizes the canvas if necessary. Should be called on every render frame.
169 | */
170 | private _resizeCanvas() {
171 | const { width, height, clientWidth, clientHeight } = this._renderer.domElement;
172 | if (width !== clientWidth || height !== clientHeight) {
173 | this._renderer.setSize(clientWidth, clientHeight, false);
174 | this._composer.setSize(clientWidth, clientHeight);
175 | this._backgroundPass.setSize(clientWidth, clientHeight);
176 | this._transitionPass.setSize(clientWidth, clientHeight);
177 | this._effectPass.setSize(clientWidth, clientHeight);
178 | }
179 | }
180 |
181 | /**
182 | * Begins rendering the background.
183 | */
184 | render(): void {
185 | // cancel any previous ongoing renders
186 | if (this._renderAnimationFrame !== undefined) {
187 | cancelAnimationFrame(this._renderAnimationFrame);
188 | this._renderAnimationFrame = undefined;
189 | }
190 |
191 | this._paused = false;
192 | this._clock.start();
193 | this._render();
194 | }
195 |
196 | /**
197 | * Pauses rendering of the background.
198 | */
199 | pause(): void {
200 | this._paused = true;
201 | this._clock.stop();
202 | if (this._renderAnimationFrame !== undefined) {
203 | cancelAnimationFrame(this._renderAnimationFrame);
204 | this._renderAnimationFrame = undefined;
205 | }
206 | }
207 |
208 | /**
209 | * Returns whether the renderer is paused.
210 | * @returns {boolean}
211 | */
212 | get isPaused(): boolean {
213 | return this._paused;
214 | }
215 |
216 | /**
217 | * Renders the background, transitions, and effects. Should be called on every frame.
218 | */
219 | private _render() {
220 | update();
221 | this._resizeCanvas();
222 |
223 | if (!this._disposed) {
224 | this._composer.render(this._clock.getDelta());
225 | // eslint-disable-next-line @typescript-eslint/unbound-method
226 | this._renderAnimationFrame = requestAnimationFrame(this._render);
227 | }
228 | }
229 |
230 | /**
231 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
232 | */
233 | dispose(): void {
234 | this._disposed = true;
235 | this._renderer.dispose();
236 | this._backgroundPass.dispose();
237 | this._transitionPass.dispose();
238 | this._effectPass.dispose();
239 | this._clock.stop();
240 | }
241 | }
242 |
243 | export {
244 | isWebGLSupported,
245 | loadImage,
246 | Transition,
247 | BlendTransition,
248 | WipeTransition,
249 | SlideTransition,
250 | BlurTransition,
251 | GlitchTransition,
252 | BackgroundRenderer,
253 | };
254 |
255 | export default BackgroundRenderer;
256 |
--------------------------------------------------------------------------------
/src/background.ts:
--------------------------------------------------------------------------------
1 | import { WebGLRenderTarget, Scene, Mesh, PlaneGeometry, MeshBasicMaterial, DepthTexture, Texture, WebGLRenderer } from 'three';
2 | import { BackgroundCamera } from './background-camera';
3 | import { getMaxFullScreenDepthForPlane } from './background-camera-utils';
4 | import { BackgroundEffects } from './background-effects';
5 | import { Particles } from './effects/particles';
6 |
7 | interface PlaneMesh extends Mesh {
8 | geometry: PlaneGeometry;
9 | material: MeshBasicMaterial;
10 | }
11 |
12 | class Background {
13 | private readonly _buffer: WebGLRenderTarget;
14 | private readonly _plane: PlaneMesh;
15 | private readonly _scene: Scene;
16 | readonly camera: BackgroundCamera;
17 | readonly particles: Particles;
18 | readonly effects: BackgroundEffects;
19 |
20 | /**
21 | * Constructs a background.
22 | * @param {Texture | null} texture
23 | * @param {number} width
24 | * @param {number} height
25 | */
26 | constructor(texture: Texture | null, width: number, height: number) {
27 | // primary buffer - store depth texture for use in motion blur
28 | this._buffer = new WebGLRenderTarget(width, height);
29 | this._buffer.depthTexture = new DepthTexture(width, height);
30 |
31 | // plane using texture - dimensions are in world units
32 | const textureAspectRatio = texture && texture.image !== undefined
33 | ? texture.image.width / texture.image.height
34 | : 1;
35 | const planeWidth = 1;
36 | const planeHeight = 1/ textureAspectRatio;
37 | this._plane = new Mesh(
38 | new PlaneGeometry(planeWidth, planeHeight),
39 | new MeshBasicMaterial({ map: texture }),
40 | ) as PlaneMesh;
41 |
42 | // camera - look at plane
43 | this.camera = new BackgroundCamera(this._plane, width, height);
44 |
45 | // particles - use slightly larger boundaries to avoid sudden particle pop-ins
46 | this.particles = new Particles(
47 | planeWidth * 1.1,
48 | planeHeight * 1.1,
49 | getMaxFullScreenDepthForPlane(this._plane, this.camera.camera, 0)
50 | );
51 |
52 | // effects - configure background effects with motion blur support
53 | this.effects = new BackgroundEffects(width, height, this.camera.camera, this._buffer.depthTexture);
54 |
55 | // scene - throw everything together
56 | this._scene = new Scene();
57 | this._scene.add(this.particles.object);
58 | this._scene.add(this._plane);
59 | }
60 |
61 | /**
62 | * Returns the texture of the background.
63 | * @returns {Texture | null}
64 | */
65 | get texture(): Texture | null {
66 | return this._plane.material.map;
67 | }
68 |
69 | /**
70 | * Sets the size of the background.
71 | * @param {number} width
72 | * @param {number} height
73 | */
74 | setSize(width: number, height: number): void {
75 | this.camera.setSize(width, height);
76 | this._buffer.setSize(width, height);
77 | this._buffer.depthTexture.image.width = width;
78 | this._buffer.depthTexture.image.height = height;
79 | }
80 |
81 | /**
82 | * Renders the background.
83 | * @param {WebGLRenderer} renderer - the renderer to use.
84 | * @param {WebGLRenderTarget | null} writeBuffer - the buffer to render to, or null to render directly to screen.
85 | */
86 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget | null = null): void {
87 | this.camera.update();
88 | this.particles.update();
89 |
90 | // render to internal buffer to update depth texture
91 | renderer.setRenderTarget(this._buffer);
92 | renderer.render(this._scene, this.camera.camera);
93 |
94 | // render to the given write buffer
95 | if (this.effects.hasEffects()) {
96 | this.effects.render(renderer, writeBuffer, this._buffer);
97 | } else {
98 | renderer.setRenderTarget(writeBuffer);
99 | renderer.render(this._scene, this.camera.camera);
100 | }
101 | }
102 |
103 | /**
104 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
105 | */
106 | dispose(): void {
107 | this._buffer.dispose();
108 | this._buffer.texture.dispose();
109 | this._buffer.depthTexture.dispose();
110 | this._plane.geometry.dispose();
111 | this._plane.material.dispose();
112 | this.camera.dispose();
113 | this.effects.dispose();
114 | this.particles.dispose();
115 | }
116 | }
117 |
118 | export {
119 | PlaneMesh,
120 | Background,
121 | };
122 |
123 | export default Background;
124 |
--------------------------------------------------------------------------------
/src/effects/effect.ts:
--------------------------------------------------------------------------------
1 | import { WebGLRenderTarget, Vector2, Shader, ShaderMaterial, WebGLRenderer, PerspectiveCamera, DepthTexture } from 'three';
2 | import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass';
3 | import { BlendShader } from 'three/examples/jsm/shaders/BlendShader';
4 | import { GaussianBlurShader, GaussianBlurDirection } from './shaders/effect/gaussian-blur-shader';
5 | import { RGBShiftShader } from 'three/examples/jsm/shaders/RGBShiftShader';
6 | import { VignetteShader } from 'three/examples/jsm/shaders/VignetteShader';
7 | import { VignetteBlendShader } from './shaders/effect/vignette-blend-shader';
8 | import { MotionBlurShader } from './shaders/effect/motion-blur-shader';
9 | import { GlitchShader } from './shaders/transition/glitch-shader';
10 | import { ShaderUtils, Uniforms } from './shaders/shader-utils';
11 |
12 | enum EffectType {
13 | Blur = 'Blur',
14 | Bloom = 'Bloom',
15 | RgbShift = 'RgbShift',
16 | Vignette = 'Vignette',
17 | VignetteBlur = 'VignetteBlur',
18 | MotionBlur = 'MotionBlur',
19 | Glitch = 'Glitch',
20 | }
21 |
22 | interface IEffect {
23 | render(...args: any[]);
24 | setSize?(width: number, height: number);
25 | getUniforms(): Uniforms;
26 | updateUniforms(uniforms: Uniforms);
27 | clearUniforms();
28 | dispose();
29 | }
30 |
31 | class Effect implements IEffect {
32 | protected _quad: FullScreenQuad = new FullScreenQuad();
33 |
34 | /**
35 | * Contructs an effect.
36 | * @param {Shader} shader - a shader definition.
37 | * @param {Uniforms} uniforms - uniforms for the shader.
38 | */
39 | constructor(shader: Shader, uniforms: Uniforms = {}) {
40 | this._quad.material = ShaderUtils.createShaderMaterial(shader, uniforms);
41 | }
42 |
43 | /**
44 | * Returns the current uniforms for the effect.
45 | * @returns Uniforms
46 | */
47 | getUniforms(): Uniforms {
48 | return ShaderUtils.getUniforms(this._quad.material as ShaderMaterial);
49 | }
50 |
51 | /**
52 | * Updates the specified uniforms for the effect.
53 | * @param {Uniforms} uniforms
54 | */
55 | updateUniforms(uniforms: Uniforms = {}): void {
56 | ShaderUtils.updateUniforms(this._quad.material as ShaderMaterial, uniforms);
57 | }
58 |
59 | /**
60 | * Resets the uniforms for the effect back to its default values.
61 | */
62 | clearUniforms(): void {
63 | ShaderUtils.clearUniforms(this._quad.material as ShaderMaterial);
64 | }
65 |
66 | /**
67 | * Renders the effect.
68 | * @param {WebGLRenderer} renderer - the renderer to use.
69 | * @param {WebGLRenderTarget | null} writeBuffer - the buffer to render to, or null to render directly to screen.
70 | * @param {WebGLRenderTarget} readBuffer - the buffer to read from.
71 | * @param {Uniforms} uniforms - uniforms values to update before rendering.
72 | */
73 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget | null, readBuffer: WebGLRenderTarget, uniforms: Uniforms = {}): void {
74 | renderer.setRenderTarget(writeBuffer);
75 | this.updateUniforms({
76 | ...uniforms,
77 | tDiffuse: readBuffer.texture,
78 | });
79 | this._quad.render(renderer);
80 | }
81 |
82 | /**
83 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
84 | */
85 | dispose(): void {
86 | this._quad.material.dispose();
87 | }
88 | }
89 |
90 | class TransitionEffect extends Effect {
91 | /**
92 | * Renders the effect.
93 | * @param {WebGLRenderer} renderer - the renderer to use.
94 | * @param {WebGLRenderTarget | null} writeBuffer - the buffer to render to, or null to render directly to screen.
95 | * @param {WebGLRenderTarget} fromBuffer - the buffer to transition from.
96 | * @param {WebGLRenderTarget} toBuffer - the buffer to transition to.
97 | * @param {Uniforms} uniforms - uniform values to update before rendering.
98 | */
99 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget | null, fromBuffer: WebGLRenderTarget, toBuffer: WebGLRenderTarget, uniforms: Uniforms = {}): void {
100 | renderer.setRenderTarget(writeBuffer);
101 | this.updateUniforms({
102 | ...uniforms,
103 | tDiffuse1: fromBuffer.texture,
104 | tDiffuse2: toBuffer.texture,
105 | });
106 | this._quad.render(renderer);
107 | }
108 | }
109 |
110 | class MotionBlurEffect extends Effect {
111 | camera: PerspectiveCamera;
112 | depthTexture: DepthTexture;
113 |
114 | /**
115 | * Constructs a MotionBlurEffect.
116 | * @param {PerspectiveCamera} camera - a three.js PerspectiveCamera.
117 | * @param {DepthTexture} depthTexture - a three.js DepthTexture.
118 | * @param {Uniforms} uniforms - uniforms for the shader.
119 | */
120 | constructor(camera: PerspectiveCamera, depthTexture: DepthTexture, uniforms: Uniforms = {}) {
121 | super(MotionBlurShader, uniforms);
122 |
123 | this.camera = camera;
124 | this.depthTexture = depthTexture;
125 | }
126 |
127 | /**
128 | * Renders the effect.
129 | * @param {WebGLRenderer} renderer - the renderer to use.
130 | * @param {WebGLRenderTarget | null} writeBuffer - the buffer to render to, or null to render directly to screen.
131 | * @param {WebGLRenderTarget} readBuffer - the buffer to read from.
132 | * @param {Uniforms} uniforms - uniform values to update before rendering.
133 | */
134 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget | null, readBuffer: WebGLRenderTarget, uniforms: Uniforms = {}): void {
135 | const { clipToWorldMatrix, prevWorldToClipMatrix } = this.getUniforms();
136 |
137 | // the clip to world space matrix is calculated using the inverse projection-view matrix
138 | // NOTE: camera.matrixWorld is the inverse view matrix of the camera (instead of matrixWorldInverse)
139 | super.render(renderer, writeBuffer, readBuffer, {
140 | ...uniforms,
141 | tDepth: this.depthTexture,
142 | clipToWorldMatrix: clipToWorldMatrix.copy(this.camera.projectionMatrixInverse).multiply(this.camera.matrixWorld),
143 | });
144 |
145 | // the world to clip space matrix is calculated using the view-projection matrix
146 | prevWorldToClipMatrix.copy(this.camera.matrixWorldInverse).multiply(this.camera.projectionMatrix);
147 | }
148 | }
149 |
150 | class GaussianBlurEffect extends Effect {
151 | private _width: number;
152 | private _height: number;
153 | private _buffer: WebGLRenderTarget;
154 |
155 | // the number of blur passes to perform - more passes are expensive but result in stronger blurs and less artifacts.
156 | passes = 1;
157 |
158 | /**
159 | * Constructs a GaussianBlurEffect.
160 | * @param {number} width
161 | * @param {number} height
162 | * @param {Uniforms} uniforms - uniforms for the shader.
163 | */
164 | constructor(width: number, height: number, uniforms: Uniforms = {}) {
165 | super(GaussianBlurShader, uniforms);
166 | this._width = width;
167 | this._height = height;
168 | this._buffer = new WebGLRenderTarget(width, height);
169 | }
170 |
171 | /**
172 | * Sets the size of the effect.
173 | * @param {number} width
174 | * @param {number} height
175 | */
176 | setSize(width: number, height: number): void {
177 | this._width = width;
178 | this._height = height;
179 | this._buffer.setSize(width, height);
180 | }
181 |
182 | /**
183 | * Renders the effect.
184 | * @param {WebGLRenderer} renderer - the renderer to use.
185 | * @param {WebGLRenderTarget} writeBuffer - the buffer to render to.
186 | * @param {WebGLRenderTarget} readBuffer - the buffer to read from.
187 | * @param {Uniforms} uniforms - uniform values to update before rendering.
188 | */
189 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget, uniforms: Uniforms = {}): void {
190 | for (let i = 0; i < this.passes; ++i) {
191 | super.render(renderer, this._buffer, i === 0 ? readBuffer : writeBuffer, {
192 | ...uniforms,
193 | direction: GaussianBlurDirection.HORIZONTAL,
194 | resolution: this._width,
195 | });
196 | super.render(renderer, writeBuffer, this._buffer, {
197 | ...uniforms,
198 | direction: GaussianBlurDirection.VERTICAL,
199 | resolution: this._height,
200 | });
201 | }
202 | }
203 |
204 | /**
205 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
206 | */
207 | dispose(): void {
208 | this._buffer.dispose();
209 | super.dispose();
210 | }
211 | }
212 |
213 | class BloomEffect implements IEffect {
214 | private _blurEffect: GaussianBlurEffect;
215 | private _blendEffect: TransitionEffect;
216 | private _blendBuffer: WebGLRenderTarget;
217 |
218 | /**
219 | * Constructs a BloomEffect.
220 | * @param {number} width
221 | * @param {number} height
222 | * @param {Uniforms} uniforms - uniforms for the shader.
223 | */
224 | constructor(width: number, height: number, uniforms: Uniforms = {}) {
225 | this._blurEffect = new GaussianBlurEffect(width, height);
226 | this._blendEffect = new TransitionEffect(BlendShader, { mixRatio: 0.5 });
227 | this._blendBuffer = new WebGLRenderTarget(width, height);
228 | this.updateUniforms(uniforms);
229 | }
230 |
231 | /**
232 | * The number of blur passes to perform. More passes are expensive but result in stronger blurs and less artifacts.
233 | * @returns number
234 | */
235 | get passes(): number {
236 | return this._blurEffect.passes;
237 | }
238 |
239 | /**
240 | * @param {number} value
241 | */
242 | set passes(value: number) {
243 | this._blurEffect.passes = value;
244 | }
245 |
246 | /**
247 | * Sets the size of the effect.
248 | * @param {number} width
249 | * @param {number} height
250 | */
251 | setSize(width: number, height: number): void {
252 | this._blurEffect.setSize(width, height);
253 | this._blendBuffer.setSize(width, height);
254 | }
255 |
256 | /**
257 | * Returns the current uniforms for the effect.
258 | * @returns Uniforms
259 | */
260 | getUniforms(): Uniforms {
261 | const { opacity } = this._blendEffect.getUniforms();
262 | return { ...this._blurEffect.getUniforms(), opacity };
263 | }
264 |
265 | /**
266 | * Updates the specified uniforms for the effect.
267 | * @param {Uniforms} uniforms
268 | */
269 | updateUniforms(uniforms: Uniforms = {}): void {
270 | const blendUniforms = this._blendEffect.getUniforms();
271 | const { opacity = blendUniforms.opacity, ...blurUniforms } = uniforms;
272 | this._blurEffect.updateUniforms(blurUniforms);
273 | this._blendEffect.updateUniforms({ opacity });
274 | }
275 |
276 | /**
277 | * Resets the uniforms for the effect back to its default values.
278 | */
279 | clearUniforms(): void {
280 | this._blurEffect.clearUniforms();
281 | this._blendEffect.clearUniforms();
282 | this._blendEffect.updateUniforms({ mixRatio: 0.5 });
283 | }
284 |
285 | /**
286 | * Renders the effect.
287 | * @param {WebGLRenderer} renderer - the renderer to use.
288 | * @param {WebGLRenderTarget | null} writeBuffer - the buffer to render to, or null to render directly to screen.
289 | * @param {WebGLRenderTarget} readBuffer - the buffer to read from.
290 | * @param {Uniforms} uniforms - uniform values to update before rendering.
291 | */
292 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget | null, readBuffer: WebGLRenderTarget, uniforms: Uniforms = {}): void {
293 | this._blurEffect.render(renderer, this._blendBuffer, readBuffer, uniforms);
294 | this._blendEffect.render(renderer, writeBuffer, readBuffer, this._blendBuffer);
295 | }
296 |
297 | /**
298 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
299 | */
300 | dispose(): void {
301 | this._blendEffect.dispose();
302 | this._blendBuffer.dispose();
303 | }
304 | }
305 |
306 | class RGBShiftEffect extends Effect {
307 | /**
308 | * Contructs a VignetteEffect.
309 | * @param {Uniforms} uniforms - uniforms for the shader.
310 | */
311 | constructor(uniforms: Uniforms = {}) {
312 | super(RGBShiftShader, uniforms);
313 | }
314 | }
315 |
316 | class VignetteEffect extends Effect {
317 | /**
318 | * Contructs a VignetteEffect.
319 | * @param {Uniforms} uniforms - uniforms for the shader.
320 | */
321 | constructor(uniforms: Uniforms = {}) {
322 | super(VignetteShader, uniforms);
323 | }
324 | }
325 |
326 | class VignetteBlurEffect implements IEffect {
327 | private _blurEffect: GaussianBlurEffect;
328 | private _blendEffect: TransitionEffect;
329 | private _blendBuffer: WebGLRenderTarget;
330 |
331 | /**
332 | * Constructs a VignetteBlurEffect.
333 | * @param {number} width
334 | * @param {number} height
335 | * @param {Uniforms} uniforms - uniforms for the shader.
336 | */
337 | constructor(width: number, height: number, uniforms: Uniforms = {}) {
338 | this._blurEffect = new GaussianBlurEffect(width, height);
339 | this._blendEffect = new TransitionEffect(VignetteBlendShader);
340 | this._blendBuffer = new WebGLRenderTarget(width, height);
341 | this.updateUniforms(uniforms);
342 | }
343 |
344 | /**
345 | * The number of blur passes to perform. More passes are expensive but result in stronger blurs and less artifacts.
346 | * @returns number
347 | */
348 | get passes(): number {
349 | return this._blurEffect.passes;
350 | }
351 |
352 | /**
353 | * @param {number} value
354 | */
355 | set passes(value: number) {
356 | this._blurEffect.passes = value;
357 | }
358 |
359 | /**
360 | * Sets the size of the effect.
361 | * @param {number} width
362 | * @param {number} height
363 | */
364 | setSize(width: number, height: number): void {
365 | this._blurEffect.setSize(width, height);
366 | this._blendBuffer.setSize(width, height);
367 | }
368 |
369 | /**
370 | * Returns the current uniforms for the effect.
371 | * @returns Uniforms
372 | */
373 | getUniforms(): Uniforms {
374 | const { size } = this._blendEffect.getUniforms();
375 | return { ...this._blurEffect.getUniforms(), size };
376 | }
377 |
378 | /**
379 | * Updates the specified uniforms for the effect.
380 | * @param {Uniforms} uniforms
381 | */
382 | updateUniforms(uniforms: Uniforms = {}): void {
383 | const blendUniforms = this._blendEffect.getUniforms();
384 | const { size = blendUniforms.size, ...blurUniforms } = uniforms;
385 | this._blurEffect.updateUniforms(blurUniforms);
386 | this._blendEffect.updateUniforms({ size });
387 | }
388 |
389 | /**
390 | * Resets the uniforms for the effect back to its default values.
391 | */
392 | clearUniforms(): void {
393 | this._blurEffect.clearUniforms();
394 | this._blendEffect.clearUniforms();
395 | }
396 |
397 | /**
398 | * Renders the effect.
399 | * @param {WebGLRenderer} renderer - the renderer to use.
400 | * @param {WebGLRenderTarget | null} writeBuffer - the buffer to render to, or null to render directly to screen.
401 | * @param {WebGLRenderTarget} readBuffer - the buffer to read from.
402 | * @param {Uniforms} uniforms - uniform values to update before rendering.
403 | */
404 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget | null, readBuffer: WebGLRenderTarget, uniforms: Uniforms = {}): void {
405 | this._blurEffect.render(renderer, this._blendBuffer, readBuffer, uniforms);
406 | this._blendEffect.render(renderer, writeBuffer, readBuffer, this._blendBuffer);
407 | }
408 |
409 | /**
410 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
411 | */
412 | dispose(): void {
413 | this._blurEffect.dispose();
414 | this._blendEffect.dispose();
415 | this._blendBuffer.dispose();
416 | }
417 | }
418 |
419 | class GlitchEffect implements IEffect {
420 | private _resolution: Vector2;
421 | private _glitchEffect: TransitionEffect;
422 | private _blurEffect: GaussianBlurEffect;
423 | private _blurBuffer: WebGLRenderTarget;
424 |
425 | /**
426 | * Constructs a GlitchEffect.
427 | * @param {number} width
428 | * @param {number} height
429 | * @param {Uniforms} uniforms - uniforms for the shader.
430 | */
431 | constructor(width: number, height: number, uniforms: Uniforms = {}) {
432 | this._resolution = new Vector2(width, height);
433 | this._glitchEffect = new TransitionEffect(GlitchShader);
434 | this._blurEffect = new GaussianBlurEffect(width, height, { radius: 3 });
435 | this._blurEffect.passes = 2;
436 | this._blurBuffer = new WebGLRenderTarget(width, height);
437 | this.updateUniforms(uniforms);
438 | }
439 |
440 | /**
441 | * Sets the size for the effect.
442 | * @param {number} width
443 | * @param {number} height
444 | */
445 | setSize(width: number, height: number): void {
446 | this._resolution.set(width, height);
447 | this._blurEffect.setSize(width, height);
448 | this._blurBuffer.setSize(width, height);
449 | }
450 |
451 | /**
452 | * Returns the current uniforms for the effect.
453 | * @returns Uniforms
454 | */
455 | getUniforms(): Uniforms {
456 | return this._glitchEffect.getUniforms();
457 | }
458 |
459 | /**
460 | * Updates the specified uniforms for the effect.
461 | * @param {Uniforms} uniforms
462 | */
463 | updateUniforms(uniforms: Uniforms = {}): void {
464 | this._glitchEffect.updateUniforms(uniforms);
465 | }
466 |
467 | /**
468 | * Resets the uniforms for the effect back to its default values.
469 | */
470 | clearUniforms(): void {
471 | this._glitchEffect.clearUniforms();
472 | }
473 |
474 | /**
475 | * Renders the effect.
476 | * @param {WebGLRenderer} renderer - the renderer to use.
477 | * @param {WebGLRenderTarget | null} writeBuffer - the buffer to render to, or null to render directly to screen.
478 | * @param {WebGLRenderTarget} readBuffer - the buffer to read from.
479 | * @param {Uniforms} uniforms - uniform values to update before rendering.
480 | */
481 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget | null, readBuffer: WebGLRenderTarget, uniforms: Uniforms = {}): void {
482 | this._blurEffect.render(renderer, this._blurBuffer, readBuffer);
483 | this._glitchEffect.render(renderer, writeBuffer, readBuffer, this._blurBuffer, {
484 | ...uniforms,
485 | resolution: this._resolution,
486 | });
487 | }
488 |
489 | /**
490 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
491 | */
492 | dispose(): void {
493 | this._glitchEffect.dispose();
494 | this._blurEffect.dispose();
495 | this._blurBuffer.dispose();
496 | }
497 | }
498 |
499 | export {
500 | EffectType,
501 | IEffect,
502 | Effect,
503 | TransitionEffect,
504 | GaussianBlurEffect,
505 | BloomEffect,
506 | RGBShiftEffect,
507 | VignetteEffect,
508 | VignetteBlurEffect,
509 | MotionBlurEffect,
510 | GlitchEffect,
511 | };
512 |
--------------------------------------------------------------------------------
/src/effects/particles.ts:
--------------------------------------------------------------------------------
1 | import { BufferGeometry, Float32BufferAttribute, Points, Color, Vector2, ShaderMaterial, MathUtils, BufferAttribute } from 'three';
2 | import { Tween, Easing } from '@tweenjs/tween.js';
3 | import { ParticleShader } from './shaders/particle-shader';
4 | import { ShaderUtils } from './shaders/shader-utils';
5 | import { LoopableTransitionConfig } from '../transition';
6 |
7 | interface ParticleMoveOffset {
8 | // the distance of the offset.
9 | distance: number;
10 | // the angle of the offset in degrees.
11 | angle: number;
12 | }
13 |
14 | interface ParticleSwayOffset {
15 | // the x distance to sway.
16 | x: number;
17 | // the y distance to sway.
18 | y: number;
19 | }
20 |
21 | type ParticleGroupConfigs = {[name: string]: ParticleGroupConfig};
22 | interface ParticleGroupConfig {
23 | // the name of the particle group.
24 | name: string;
25 | // the number of particles to generate.
26 | amount: number;
27 | // the minimum size of the particles in world units. Defaults to 0.
28 | minSize?: number;
29 | // the maximum size of the particles in world units. Defaults to 0.
30 | maxSize?: number;
31 | // the minimum fade gradient of the particles in relative units (0 to 1). Defaults to 0.
32 | minGradient?: number;
33 | // the maximum fade gradient of the particles in relative units (0 to 1). Defaults to 1.
34 | maxGradient?: number;
35 | // the minimum opacity of the particles. Defaults to 0.
36 | minOpacity?: number;
37 | // the maximum opacity of the particles. Defaults to 1.
38 | maxOpacity?: number;
39 | // optional color of the particles. Defaults to 0xffffff.
40 | color?: number;
41 | // the amount of smoothing for animated values (i.e size, gradient, opacity), specified as a value between 0 and 1. Defaults to 0.5.
42 | smoothing?: number;
43 | }
44 |
45 | interface ParticleTween {
46 | offsetX: number;
47 | offsetY: number;
48 | }
49 |
50 |
51 | type ParticleGroups = {[name: string]: Required};
52 | interface ParticleGroup extends ParticleGroupConfig {
53 | index: number;
54 | swayOffset: Vector2;
55 | positionTransition: Tween;
56 | swayTransition: Tween;
57 | }
58 |
59 | class Particles {
60 | private _width: number;
61 | private _height: number;
62 | private _maxDepth: number;
63 |
64 | // groups also store the transitions related to the attributes and offsets
65 | private _groups: ParticleGroups = {};
66 | private _particles: Points;
67 | private _positions: number[] = [];
68 |
69 | /**
70 | * Constructs a Particles object.
71 | * @param {number} width
72 | * @param {number} height
73 | * @param {number} maxDepth - the maximum depth of the particles in world units.
74 | */
75 | constructor(width: number, height: number, maxDepth: number) {
76 | this._width = width;
77 | this._height = height;
78 | this._maxDepth = maxDepth;
79 |
80 | const geometry = new BufferGeometry();
81 | geometry.setAttribute('position', new Float32BufferAttribute(0, 3));
82 | geometry.setAttribute('size', new Float32BufferAttribute(0, 1));
83 | geometry.setAttribute('gradient', new Float32BufferAttribute(0, 1));
84 | geometry.setAttribute('opacity', new Float32BufferAttribute(0, 1));
85 | geometry.setAttribute('color', new Float32BufferAttribute(0, 3));
86 |
87 | this._particles = new Points(
88 | geometry,
89 | ShaderUtils.createShaderMaterial(ParticleShader),
90 | );
91 | }
92 |
93 | /**
94 | * Returns the configurations for the currently set particle groups.
95 | * @returns ParticleGroupDefinitionMap
96 | */
97 | getConfigs(): ParticleGroupConfigs {
98 | const configs: ParticleGroupConfigs = {};
99 | for (const group of Object.values(this._groups)) {
100 | const { name, amount, minSize, maxSize, minGradient, maxGradient, minOpacity, maxOpacity, color } = group;
101 | configs[name] = { name, amount, minSize, maxSize, minGradient, maxGradient, minOpacity, maxOpacity, color };
102 | }
103 | return configs;
104 | }
105 |
106 | /**
107 | * Returns whether a group of particles is currently moving.
108 | * @param {string} name - the name of the particle group.
109 | * @returns boolean
110 | */
111 | isMoving(name: string): boolean {
112 | return this._groups[name]?.positionTransition.isPlaying() ?? false;
113 | }
114 |
115 | /**
116 | * Returns whether a group of particles is currently swaying.
117 | * @param {string} name - the name of the particle group.
118 | * @returns boolean
119 | */
120 | isSwaying(name: string): boolean {
121 | return this._groups[name]?.swayTransition.isPlaying() ?? false;
122 | }
123 |
124 | /**
125 | * Generates particles based on a given set of configurations.
126 | * @param {ParticleGroupConfig | ParticleGroupConfig[]} config - a single or array of particle group configurations.
127 | */
128 | generate(configs: ParticleGroupConfig | ParticleGroupConfig[]): void {
129 | // cleanup previous configs and objects
130 | this.removeAll();
131 |
132 | configs = Array.isArray(configs) ? configs : [configs];
133 | let index = 0;
134 | for (const config of configs) {
135 | const {
136 | name,
137 | amount = 0,
138 | minSize = 0,
139 | maxSize = 0,
140 | minGradient = 0,
141 | maxGradient = 1,
142 | minOpacity = 0,
143 | maxOpacity = 1,
144 | color = 0xffffff,
145 | smoothing = 0.5,
146 | } = config;
147 |
148 | // Generate points with attributes
149 | for (let i = 0; i < amount || 0; ++i) {
150 | const x = (-this._width / 2) + Math.random() * this._width;
151 | const y = (-this._height / 2) + Math.random() * this._height;
152 | const z = (this._maxDepth / 4) * Math.random();
153 | this._positions.push(x, y, z);
154 | }
155 |
156 | // Store group config
157 | this._groups[name] = {
158 | name,
159 | index,
160 | amount,
161 | minSize,
162 | maxSize,
163 | minGradient,
164 | maxGradient,
165 | minOpacity,
166 | maxOpacity,
167 | color,
168 | smoothing,
169 | swayOffset: new Vector2(0, 0),
170 | positionTransition: new Tween({ offsetX: 0, offsetY: 0 }),
171 | swayTransition: new Tween({ offsetX: 0, offsetY: 0 }),
172 | };
173 |
174 | index += amount;
175 | }
176 |
177 | const geometry = new BufferGeometry();
178 | geometry.setAttribute('position', new Float32BufferAttribute(index * 3, 3));
179 | geometry.setAttribute('color', new Float32BufferAttribute(index * 3, 3));
180 | geometry.setAttribute('size', new Float32BufferAttribute(index, 1));
181 | geometry.setAttribute('gradient', new Float32BufferAttribute(index, 1));
182 | geometry.setAttribute('opacity', new Float32BufferAttribute(index, 1));
183 |
184 | const material = ShaderUtils.createShaderMaterial(ParticleShader);
185 | material.transparent = true;
186 |
187 | this._particles.geometry = geometry;
188 | this._particles.material = material;
189 | }
190 |
191 | /**
192 | * Removes all particle groups.
193 | */
194 | removeAll(): void {
195 | for (const group in this._groups) {
196 | // stop any ongoing transitions
197 | this.sway(group, false);
198 | this.move(group, false);
199 | }
200 |
201 | // reset particles to empty
202 | this._positions = [];
203 | this._groups = {};
204 | this._particles.geometry.dispose();
205 | (this._particles.material as ShaderMaterial).dispose();
206 | }
207 |
208 | /**
209 | * Calculates a new position based off an existing position and optional offset. Will wrap around boundaries.
210 | * @param {Vector2} position - the current position.
211 | * @param {Vector2} offset - the offset from the current position.
212 | * @returns Vector2
213 | */
214 | private _getNewPosition(position: Vector2, offset: Vector2): Vector2 {
215 | let { x: offsetX, y: offsetY } = offset;
216 | offsetX %= this._width;
217 | offsetY %= this._height;
218 |
219 | let x = position.x + offsetX;
220 | let y = position.y + offsetY;
221 | const halfWidth = this._width / 2;
222 | const halfHeight = this._height / 2;
223 |
224 | // wrap around left/right
225 | if (Math.abs(position.x + offsetX) > halfWidth) {
226 | x = offsetX > 0
227 | ? -halfWidth + (((position.x + offsetX) - halfWidth) % this._width)
228 | : halfWidth - ((Math.abs(position.x + offsetX) - halfWidth) % this._width);
229 | }
230 |
231 | // wrap around top/bottom
232 | if (Math.abs(position.y + offsetY) > halfHeight) {
233 | y = offsetY > 0
234 | ? -halfHeight + (((position.y + offsetY) - halfHeight) % this._height)
235 | : halfHeight - ((Math.abs(position.y + offsetY) - halfHeight) % this._height);
236 | }
237 |
238 | return new Vector2(x, y);
239 | }
240 |
241 | /**
242 | * Updates the internal positions for particles. This does NOT update the attributes of the BufferGeometry.
243 | * @param {number} index - the index to start at.
244 | * @param {number} amount - the number of particles.
245 | * @param {number[]} positions - an array containing the position values to use.
246 | * @param {Vector2} offset - an optional offset to apply to all new position values.
247 | */
248 | private _updatePositions(index: number, amount: number, positions: number[], offset: Vector2) {
249 | // Each vertex position is a set of 3 values, so index and amount are adjusted accordingly when iterating.
250 | for (let i = index; i < index + amount; ++i) {
251 | const { x, y } = this._getNewPosition(new Vector2(positions[i * 3], positions[i * 3 + 1]), offset);
252 | this._positions[i * 3] = x;
253 | this._positions[i * 3 + 1] = y;
254 | }
255 | }
256 |
257 | /**
258 | * Moves a group of particles. Cancels any in-progress moves.
259 | * @param {string} name - the name of the group to move.
260 | * @param {ParticleMoveOffset | boolean} offset - the distance and angle in radians to move.
261 | * If a boolean is passed in instead then the move will either continue or stop based on the value.
262 | * @param {LoopableTransitionConfig} transition - an optional transition configuration.
263 | */
264 | move(name: string, offset: ParticleMoveOffset | boolean, transition: LoopableTransitionConfig = {}): void {
265 | const group = this._groups[name];
266 | const { index, amount } = group;
267 |
268 | if (typeof offset === 'boolean') {
269 | if (!offset) {
270 | group.positionTransition.stop();
271 | }
272 | return;
273 | }
274 |
275 | // Stop ongoing position transition for group.
276 | group.positionTransition.stop();
277 |
278 | const {
279 | loop = false,
280 | duration = 0,
281 | easing = Easing.Linear.None,
282 | onStart = () => ({}),
283 | onUpdate = () => ({}),
284 | onComplete = () => ({}),
285 | onStop = () => ({}),
286 | } = transition;
287 |
288 | const { distance, angle } = offset;
289 | const offsetX = distance * Math.cos(MathUtils.degToRad(angle));
290 | const offsetY = distance * Math.sin(MathUtils.degToRad(angle));
291 | if (duration > 0) {
292 | // Each vertex position is a set of 3 values, so adjust index and amount accordingly.
293 | const startPositions = this._positions.slice();
294 | group.positionTransition = new Tween({ offsetX: 0, offsetY: 0 })
295 | .to({ offsetX, offsetY }, duration * 1000)
296 | .easing(easing)
297 | .onStart(onStart)
298 | .onUpdate(({ offsetX, offsetY }) => {
299 | this._updatePositions(index, amount, startPositions, new Vector2(offsetX, offsetY));
300 | onUpdate();
301 | })
302 | .onComplete(() => {
303 | if (loop) {
304 | // Repeat move with same config.
305 | this.move(name, offset, transition);
306 | }
307 | onComplete();
308 | })
309 | .onStop(onStop)
310 | .start();
311 | } else {
312 | this._updatePositions(index, amount, this._positions, new Vector2(offsetX, offsetY));
313 | }
314 | }
315 |
316 | /**
317 | * Sways a group of particles around their current positions. Cancels any in-progress sways.
318 | * @param {string} name - the name of the group to sway.
319 | * @param {ParticleSwayOffset | boolean} offset - the distances in world units allowed on each axis for swaying.
320 | * If a boolean is passed in instead then the sway will either continue or stop based on the value.
321 | * @param {LoopableTransitionConfig} transition - optional configuration for a transition.
322 | */
323 | sway(name: string, offset: ParticleSwayOffset | boolean, transition: LoopableTransitionConfig = {}): void {
324 | const group = this._groups[name];
325 | const { swayOffset } = group;
326 |
327 | if (typeof offset === 'boolean') {
328 | if (!offset) {
329 | group.swayTransition.stop();
330 | }
331 | return;
332 | }
333 |
334 | // Stop ongoing sway transition for group.
335 | group.swayTransition.stop();
336 |
337 | const {
338 | loop = false,
339 | duration = 0,
340 | easing = Easing.Linear.None,
341 | onStart = () => ({}),
342 | onUpdate = () => ({}),
343 | onComplete = () => ({}),
344 | onStop = () => ({}),
345 | } = transition;
346 |
347 | const { x, y } = offset;
348 | group.swayTransition = new Tween({
349 | offsetX: swayOffset.x,
350 | offsetY: swayOffset.y,
351 | })
352 | .to({
353 | offsetX: -x + Math.random() * x * 2,
354 | offsetY: -y + Math.random() * y * 2,
355 | }, duration * 1000)
356 | .easing(easing)
357 | .onStart(onStart)
358 | .onUpdate(({ offsetX, offsetY }) => {
359 | swayOffset.set(offsetX, offsetY);
360 | onUpdate();
361 | })
362 | .onComplete(() => {
363 | if (loop) {
364 | this.sway(name, offset, transition);
365 | }
366 | onComplete();
367 | })
368 | .onStop(onStop)
369 | .start();
370 | }
371 |
372 | /**
373 | * Generates a new random averaged value based off a given value and its range.
374 | * @param {number} prevValue - the previous value.
375 | * @param {number} minValue - the minimum value for the given value.
376 | * @param {number} maxValue - the maximum value for the given value.
377 | * @param {number} smoothing - optional amount of smoothing to use as a value between 0 and 1. Defaults to 0.5.
378 | * @returns number
379 | */
380 | private _generateNewRandomAveragedValue(prevValue: number, minValue: number, maxValue: number, smoothing = 0.5): number {
381 | // cap smoothing at 0.95
382 | smoothing = Math.min(smoothing, 0.95);
383 | const offset = (maxValue - minValue) / 2;
384 | const nextValue = Math.max(Math.min(prevValue + (-offset + Math.random() * offset * 2), maxValue), minValue);
385 | const smoothedValue = (prevValue * smoothing) + (nextValue * (1 - smoothing));
386 | return Math.max(Math.min(smoothedValue, maxValue), minValue);
387 | }
388 |
389 | /**
390 | * Updates the positions of the particles. Should be called on every render frame.
391 | */
392 | update(): void {
393 | const { attributes } = this._particles.geometry;
394 | const {
395 | position: positions,
396 | size: sizes,
397 | gradient: gradients,
398 | opacity: opacities,
399 | color: colors,
400 | } = attributes;
401 |
402 | for (const group of Object.values(this._groups)) {
403 | const {
404 | index,
405 | amount,
406 | minSize,
407 | maxSize,
408 | minGradient,
409 | maxGradient,
410 | minOpacity,
411 | maxOpacity,
412 | color,
413 | smoothing,
414 | swayOffset,
415 | } = group;
416 | for (let i = index; i < index + amount; ++i) {
417 | // Apply offset to current position (excluding z).
418 | const position = this._getNewPosition(new Vector2(this._positions[i * 3], this._positions[i * 3 + 1]), swayOffset);
419 | const rgb = new Color(color);
420 |
421 | positions.setXYZ(i, position.x, position.y, this._positions[i * 3 + 2]);
422 | colors.setXYZ(i, rgb.r, rgb.g, rgb.b);
423 | sizes.setX(i, this._generateNewRandomAveragedValue(sizes.getX(i), minSize, maxSize, smoothing));
424 | gradients.setX(i, this._generateNewRandomAveragedValue(gradients.getX(i), minGradient, maxGradient, smoothing));
425 | opacities.setX(i, this._generateNewRandomAveragedValue(opacities.getX(i), minOpacity, maxOpacity, smoothing));
426 | }
427 | }
428 |
429 | (attributes.position as BufferAttribute).needsUpdate = true;
430 | (attributes.size as BufferAttribute).needsUpdate = true;
431 | (attributes.gradient as BufferAttribute).needsUpdate = true;
432 | (attributes.opacity as BufferAttribute).needsUpdate = true;
433 | (attributes.color as BufferAttribute).needsUpdate = true;
434 | }
435 |
436 | /**
437 | * Returns a three.js object containing the particles.
438 | * To use the particles, add this object into a three.js scene.
439 | * @returns Points
440 | */
441 | get object(): Points {
442 | return this._particles;
443 | }
444 |
445 | /**
446 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
447 | */
448 | dispose(): void {
449 | this.removeAll();
450 | this._particles.geometry.dispose();
451 | (this._particles.material as ShaderMaterial).dispose();
452 | }
453 | }
454 |
455 | export {
456 | ParticleMoveOffset,
457 | ParticleSwayOffset,
458 | ParticleGroupConfigs,
459 | ParticleGroupConfig,
460 | Particles,
461 | };
462 |
463 | export default Particles;
464 |
--------------------------------------------------------------------------------
/src/effects/shaders/effect/gaussian-blur-shader.ts:
--------------------------------------------------------------------------------
1 | import { Shader } from 'three';
2 |
3 | /**
4 | * @author aeroheim / http://aeroheim.moe/
5 | *
6 | * A two-pass gaussian blur that uses a 17-tap filter based off of:
7 | * http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/
8 | *
9 | * Also based off of the following implementation:
10 | * https://github.com/mattdesl/lwjgl-basics/wiki/ShaderLesson5
11 | *
12 | */
13 |
14 | const GaussianBlurDirection = Object.freeze({
15 | HORIZONTAL: [1.0, 0.0],
16 | VERTICAL: [0.0, 1.0],
17 | });
18 |
19 | const GaussianBlurShader: Shader = {
20 | uniforms: {
21 | tDiffuse: { value: null },
22 | // the radius of the blur - determines the offset distance for each tap
23 | radius: { value: 1.0 },
24 | // the length of the direction to be blurred (i.e width or height of texture)
25 | resolution: { value: 0.0 },
26 | // the direction of the blur
27 | direction: { value: [0.0, 0.0] },
28 | },
29 |
30 | vertexShader: `
31 |
32 | varying vec2 vUv;
33 |
34 | void main() {
35 | vUv = uv;
36 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
37 | }
38 |
39 | `,
40 |
41 | fragmentShader: `
42 |
43 | uniform sampler2D tDiffuse;
44 | uniform float radius;
45 | uniform float resolution;
46 | uniform vec2 direction;
47 | varying vec2 vUv;
48 |
49 | void main() {
50 | float blur = radius / resolution;
51 | float h = direction.x;
52 | float v = direction.y;
53 |
54 | vec4 sum = vec4(0.0);
55 |
56 | // optimized 33-tap filter that takes advantage of bilinear filtering (effectively 17 fetches)
57 | sum += texture2D(tDiffuse, vec2(vUv.x - 15.0810810809 * blur * h, vUv.y - 15.0810810809 * blur * v)) * 1.13068382e-7;
58 | sum += texture2D(tDiffuse, vec2(vUv.x - 13.1351352551 * blur * h, vUv.y - 13.1351352551 * blur * v)) * 0.00000634313;
59 | sum += texture2D(tDiffuse, vec2(vUv.x - 11.1891891693 * blur * h, vUv.y - 11.1891891693 * blur * v)) * 0.00014981883;
60 | sum += texture2D(tDiffuse, vec2(vUv.x - 9.2432432422 * blur * h, vUv.y - 9.2432432422 * blur * v)) * 0.00181031093;
61 | sum += texture2D(tDiffuse, vec2(vUv.x - 7.29729729717 * blur * h, vUv.y - 7.29729729717 * blur * v)) * 0.01244177332;
62 | sum += texture2D(tDiffuse, vec2(vUv.x - 5.35135135135 * blur * h, vUv.y - 5.35135135135 * blur * v)) * 0.0518407222;
63 | sum += texture2D(tDiffuse, vec2(vUv.x - 3.40540540538 * blur * h, vUv.y - 3.40540540538 * blur * v)) * 0.13626704123;
64 | sum += texture2D(tDiffuse, vec2(vUv.x - 1.45945945945 * blur * h, vUv.y - 1.45945945945 * blur * v)) * 0.23145357738;
65 |
66 | sum += texture2D(tDiffuse, vUv) * 0.13206059971;
67 |
68 | sum += texture2D(tDiffuse, vec2(vUv.x + 1.45945945945 * blur * h, vUv.y + 1.45945945945 * blur * v)) * 0.23145357738;
69 | sum += texture2D(tDiffuse, vec2(vUv.x + 3.40540540538 * blur * h, vUv.y + 3.40540540538 * blur * v)) * 0.13626704123;
70 | sum += texture2D(tDiffuse, vec2(vUv.x + 5.35135135135 * blur * h, vUv.y + 5.35135135135 * blur * v)) * 0.0518407222;
71 | sum += texture2D(tDiffuse, vec2(vUv.x + 7.29729729717 * blur * h, vUv.y + 7.29729729717 * blur * v)) * 0.01244177332;
72 | sum += texture2D(tDiffuse, vec2(vUv.x + 9.2432432422 * blur * h, vUv.y + 9.2432432422 * blur * v)) * 0.00181031093;
73 | sum += texture2D(tDiffuse, vec2(vUv.x + 11.1891891693 * blur * h, vUv.y + 11.1891891693 * blur * v)) * 0.00014981883;
74 | sum += texture2D(tDiffuse, vec2(vUv.x + 13.1351352551 * blur * h, vUv.y + 13.1351352551 * blur * v)) * 0.00000634313;
75 | sum += texture2D(tDiffuse, vec2(vUv.x + 15.0810810809 * blur * h, vUv.y + 15.0810810809 * blur * v)) * 1.13068382e-7;
76 |
77 | gl_FragColor = sum;
78 | }
79 | `,
80 | };
81 |
82 | export {
83 | GaussianBlurShader,
84 | GaussianBlurDirection,
85 | };
86 |
87 | export default GaussianBlurShader;
88 |
--------------------------------------------------------------------------------
/src/effects/shaders/effect/motion-blur-shader.ts:
--------------------------------------------------------------------------------
1 | import { Matrix4, Shader } from 'three';
2 |
3 | /**
4 | * @author aeroheim / http://aeroheim.moe/
5 | *
6 | * A motion blur implemention based off of GPU Gems 3: Chapter 27. Motion Blur as a Post-Processing Effect:
7 | * https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch27.html
8 | *
9 | * Also based off of an implementation by John Chapman:
10 | * https://john-chapman-graphics.blogspot.com/2013/01/what-is-motion-blur-motion-pictures-are.html
11 | *
12 | */
13 |
14 | const MotionBlurShader: Shader = {
15 | uniforms: {
16 | tDiffuse: { value: null },
17 | // a depth buffer of the frame to be blurred
18 | tDepth: { value: null },
19 | // the clip -> world matrix of the current frame - used to calculate the velocity of the blur
20 | clipToWorldMatrix: { value: new Matrix4() },
21 | // the world -> clip matrix of the previous frame - used to calculate the velocity of the blur
22 | prevWorldToClipMatrix: { value: new Matrix4() },
23 | // a positive value that affects the intensity of the blur
24 | intensity: { value: 1.0 },
25 | // the number of samples to use (up to 128) - higher samples result in better quality at the cost of performance
26 | samples: { value: 32 },
27 | },
28 |
29 | vertexShader: `
30 |
31 | varying vec2 vUv;
32 |
33 | void main() {
34 | vUv = uv;
35 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
36 | }
37 |
38 | `,
39 |
40 | fragmentShader: `
41 |
42 | const int MAX_SAMPLES = 128;
43 |
44 | uniform sampler2D tDiffuse;
45 | uniform sampler2D tDepth;
46 | uniform mat4 clipToWorldMatrix;
47 | uniform mat4 prevWorldToClipMatrix;
48 | uniform float intensity;
49 | uniform int samples;
50 | varying vec2 vUv;
51 |
52 | void main() {
53 | float zOverW = texture2D(tDepth, vUv).x;
54 | vec4 clipPosition = vec4(vUv.x, vUv.y, zOverW, 1.0);
55 | vec4 worldPosition = clipToWorldMatrix * clipPosition;
56 | worldPosition /= worldPosition.w;
57 |
58 | vec4 prevClipPosition = prevWorldToClipMatrix * worldPosition;
59 | prevClipPosition /= prevClipPosition.w;
60 | vec2 velocity = ((clipPosition - prevClipPosition).xy + (clipPosition - prevClipPosition).zz) * intensity;
61 |
62 | vec4 texel = texture2D(tDiffuse, vUv);
63 | vec2 texelCoord = vUv;
64 | for (int i = 1; i < MAX_SAMPLES; ++i) {
65 | if (i >= samples) {
66 | // hack to allow loop comparisons against uniforms
67 | break;
68 | }
69 | // this offset calculation centers the blur which avoids unevenness favoring the direction of the velocity
70 | vec2 offset = velocity * (float(i) / float(samples - 1) - 0.5);
71 | texel += texture2D(tDiffuse, vUv + offset);
72 | }
73 |
74 | gl_FragColor = texel / max(1.0, float(samples));
75 | }
76 |
77 | `,
78 | };
79 |
80 | export {
81 | MotionBlurShader,
82 | };
83 |
84 | export default MotionBlurShader;
85 |
--------------------------------------------------------------------------------
/src/effects/shaders/effect/vignette-blend-shader.ts:
--------------------------------------------------------------------------------
1 | import { Shader } from 'three';
2 |
3 | /**
4 | * @author aeroheim / http://aeroheim.moe/
5 | */
6 |
7 | const VignetteBlendShader: Shader = {
8 | uniforms: {
9 | tDiffuse1: { value: null },
10 | tDiffuse2: { value: null },
11 | size: { value: 1.0 },
12 | },
13 |
14 | vertexShader: `
15 |
16 | varying vec2 vUv;
17 |
18 | void main() {
19 | vUv = uv;
20 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
21 | }
22 |
23 | `,
24 |
25 | fragmentShader: `
26 |
27 | uniform sampler2D tDiffuse1;
28 | uniform sampler2D tDiffuse2;
29 | uniform float size;
30 | varying vec2 vUv;
31 |
32 | void main() {
33 | vec2 uv = (vUv - vec2(0.5));
34 | float mixRatio = smoothstep(0.0, 1.0, min(dot(uv, uv) * size, 1.0));
35 | gl_FragColor = mix(texture2D(tDiffuse1, vUv), texture2D(tDiffuse2, vUv), mixRatio);
36 | }
37 |
38 | `,
39 | };
40 |
41 | export {
42 | VignetteBlendShader,
43 | };
44 |
45 | export default VignetteBlendShader;
46 |
--------------------------------------------------------------------------------
/src/effects/shaders/particle-shader.ts:
--------------------------------------------------------------------------------
1 | import { Shader } from 'three';
2 |
3 | /**
4 | * @author aeroheim / http://aeroheim.moe/
5 | */
6 |
7 | const ParticleShader: Shader = {
8 | uniforms: {},
9 |
10 | vertexShader: `
11 |
12 | attribute float size;
13 |
14 | // a value from 0 to 1 indicating the size of the blend gradient
15 | attribute float gradient;
16 | varying float v_gradient;
17 |
18 | // a value from 0 to 1 indicating the opacity of the particle
19 | attribute float opacity;
20 | varying float v_opacity;
21 |
22 | // the color of the particle
23 | attribute vec3 color;
24 | varying vec3 v_color;
25 |
26 | void main() {
27 | v_gradient = gradient;
28 | v_opacity = opacity;
29 | v_color = color;
30 |
31 | gl_PointSize = size;
32 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
33 | }
34 |
35 | `,
36 |
37 | fragmentShader: `
38 |
39 | varying float v_diameter;
40 | varying float v_gradient;
41 | varying float v_opacity;
42 | varying vec3 v_color;
43 |
44 | void main() {
45 | float radius = 0.5;
46 | float distanceFromCenter = distance(gl_PointCoord, vec2(0.5, 0.5));
47 | if (distanceFromCenter > radius) {
48 | discard;
49 | }
50 | gl_FragColor = vec4(v_color, min((radius - distanceFromCenter) / smoothstep(0.0, 1.0, v_gradient * radius), 1.0) * v_opacity);
51 | }
52 |
53 | `,
54 | };
55 |
56 | export {
57 | ParticleShader,
58 | };
59 |
60 | export default ParticleShader;
61 |
--------------------------------------------------------------------------------
/src/effects/shaders/shader-utils.ts:
--------------------------------------------------------------------------------
1 | import { ShaderMaterial, UniformsUtils, Shader } from 'three';
2 |
3 | type Uniforms = {[uniform: string]: any};
4 |
5 | /**
6 | * Returns the values of the uniforms for a given ShaderMaterial.
7 | * @param {ShaderMaterial} shader - a ShaderMaterial object.
8 | */
9 | function getUniforms(shader: ShaderMaterial): Uniforms {
10 | const uniforms: Uniforms = {};
11 | for (const uniform in shader.uniforms) {
12 | uniforms[uniform] = shader.uniforms[uniform].value;
13 | }
14 | return uniforms;
15 | }
16 |
17 | /**
18 | * Updates the uniforms for a given ShaderMaterial.
19 | * @param {ShaderMaterial} shader - a ShaderMaterial object.
20 | * @param {Uniforms} uniforms - a map that defines the values of the uniforms to be used
21 | */
22 | function updateUniforms(shader: ShaderMaterial, uniforms: Uniforms = {}): void {
23 | for (const uniform in uniforms) {
24 | if (shader.uniforms[uniform] === undefined) {
25 | throw new Error(`Uniform "${uniform}" does not exist on shader "${shader.name}"`);
26 | }
27 | shader.uniforms[uniform].value = uniforms[uniform];
28 | }
29 | }
30 |
31 | /**
32 | * Resets the uniforms for a given ShaderMaterial.
33 | * @param {ShaderMaterial} shader - a ShaderMaterial object.
34 | */
35 | function clearUniforms(shader: ShaderMaterial): void {
36 | shader.uniforms = UniformsUtils.clone(shader.uniforms);
37 | }
38 |
39 | /**
40 | * Returns a new ShaderMaterial given a shader definition and uniforms.
41 | * @param {Shader} shader - a shader definition.
42 | * @param {Uniforms} uniforms - uniforms for the shader.
43 | */
44 | function createShaderMaterial(shader: Shader, uniforms: Uniforms = {}): ShaderMaterial {
45 | const material = new ShaderMaterial({
46 | uniforms: UniformsUtils.clone(shader.uniforms),
47 | vertexShader: shader.vertexShader,
48 | fragmentShader: shader.fragmentShader,
49 | });
50 | updateUniforms(material, uniforms);
51 | return material;
52 | }
53 |
54 | const ShaderUtils = {
55 | getUniforms,
56 | updateUniforms,
57 | clearUniforms,
58 | createShaderMaterial,
59 | };
60 |
61 | export {
62 | Uniforms,
63 | ShaderUtils,
64 | };
65 |
66 | export default ShaderUtils;
67 |
--------------------------------------------------------------------------------
/src/effects/shaders/transition/blur-shader.ts:
--------------------------------------------------------------------------------
1 | import { Shader } from 'three';
2 |
3 | /**
4 | * @author aeroheim / http://aeroheim.moe/
5 | */
6 |
7 | const BlurShader: Shader = {
8 | uniforms: {
9 | tDiffuse1: { value: null },
10 | tDiffuse2: { value: null },
11 | // a value from 0 to 1 indicating the blend ratio for both textures
12 | amount: { value: 0.0 },
13 | // the amount value of the previous frame - used to calculate the velocity for the blur
14 | prevAmount: { value: 0.0 },
15 | // a positive value that affects the intensity of the blur
16 | intensity: { value: 1.0 },
17 | // the number of samples to use (up to 128) - higher samples result in better quality at the cost of performance
18 | samples: { value: 32 },
19 | },
20 |
21 | vertexShader: `
22 |
23 | varying vec2 vUv;
24 |
25 | void main() {
26 | vUv = uv;
27 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
28 | }
29 |
30 | `,
31 |
32 | fragmentShader: `
33 |
34 | const int MAX_SAMPLES = 128;
35 |
36 | uniform sampler2D tDiffuse1;
37 | uniform sampler2D tDiffuse2;
38 | uniform float amount;
39 | uniform float prevAmount;
40 | uniform float intensity;
41 | uniform int samples;
42 | varying vec2 vUv;
43 |
44 |
45 | void main() {
46 | vec4 texel = mix(texture2D(tDiffuse1, vUv), texture2D(tDiffuse2, vUv), amount);
47 | float velocity = (amount - prevAmount) * intensity;
48 | for (int i = 1; i < MAX_SAMPLES; ++i) {
49 | if (i >= samples) {
50 | // hack to allow loop comparisons against uniforms
51 | break;
52 | }
53 | float offset = velocity * (float(i) / float(samples - 1) - 0.5);
54 | texel += mix(texture2D(tDiffuse1, vec2(vUv.x + offset, vUv.y)), texture2D(tDiffuse2, vec2(vUv.x + offset, vUv.y)), amount);
55 | }
56 |
57 | gl_FragColor = texel / max(1.0, float(samples));
58 | }
59 |
60 | `,
61 | };
62 |
63 | export {
64 | BlurShader,
65 | };
66 |
67 | export default BlurShader;
68 |
--------------------------------------------------------------------------------
/src/effects/shaders/transition/glitch-shader.ts:
--------------------------------------------------------------------------------
1 | import { Shader } from 'three';
2 |
3 | /**
4 | * @author aeroheim / http://aeroheim.moe/
5 | */
6 |
7 | /*
8 | * Helper functions to generate noise.
9 | * See https://github.com/ashima/webgl-noise/wiki by Stefan Gustavson.
10 | */
11 | const noiseHelpers = `
12 |
13 | //
14 | // Description : Array and textureless GLSL 2D simplex noise function.
15 | // Author : Ian McEwan, Ashima Arts.
16 | // Maintainer : stegu
17 | // Lastmod : 20110822 (ijm)
18 | // License : Copyright (C) 2011 Ashima Arts. All rights reserved.
19 | // Distributed under the MIT License. See LICENSE file.
20 | // https://github.com/ashima/webgl-noise
21 | // https://github.com/stegu/webgl-noise
22 | //
23 |
24 | vec3 mod289(vec3 x) {
25 | return x - floor(x * (1.0 / 289.0)) * 289.0;
26 | }
27 |
28 | vec2 mod289(vec2 x) {
29 | return x - floor(x * (1.0 / 289.0)) * 289.0;
30 | }
31 |
32 | vec3 permute(vec3 x) {
33 | return mod289(((x*34.0)+1.0)*x);
34 | }
35 |
36 | float snoise(vec2 v) {
37 | const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0
38 | 0.366025403784439, // 0.5*(sqrt(3.0)-1.0)
39 | -0.577350269189626, // -1.0 + 2.0 * C.x
40 | 0.024390243902439); // 1.0 / 41.0
41 | // First corner
42 | vec2 i = floor(v + dot(v, C.yy) );
43 | vec2 x0 = v - i + dot(i, C.xx);
44 |
45 | // Other corners
46 | vec2 i1;
47 | //i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0
48 | //i1.y = 1.0 - i1.x;
49 | i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
50 | // x0 = x0 - 0.0 + 0.0 * C.xx ;
51 | // x1 = x0 - i1 + 1.0 * C.xx ;
52 | // x2 = x0 - 1.0 + 2.0 * C.xx ;
53 | vec4 x12 = x0.xyxy + C.xxzz;
54 | x12.xy -= i1;
55 |
56 | // Permutations
57 | i = mod289(i); // Avoid truncation effects in permutation
58 | vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
59 | + i.x + vec3(0.0, i1.x, 1.0 ));
60 |
61 | vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
62 | m = m*m ;
63 | m = m*m ;
64 |
65 | // Gradients: 41 points uniformly over a line, mapped onto a diamond.
66 | // The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287)
67 |
68 | vec3 x = 2.0 * fract(p * C.www) - 1.0;
69 | vec3 h = abs(x) - 0.5;
70 | vec3 ox = floor(x + 0.5);
71 | vec3 a0 = x - ox;
72 |
73 | // Normalise gradients implicitly by scaling m
74 | // Approximation of: m *= inversesqrt( a0*a0 + h*h );
75 | m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
76 |
77 | // Compute final noise value at P
78 | vec3 g;
79 | g.x = a0.x * x0.x + h.x * x0.y;
80 | g.yz = a0.yz * x12.xz + h.yz * x12.yw;
81 | return 130.0 * dot(m, g);
82 | }
83 |
84 | `;
85 |
86 | const GlitchShader: Shader = {
87 | uniforms: {
88 | tDiffuse1: { value: null },
89 | tDiffuse2: { value: null },
90 | resolution: { value: null },
91 | amount: { value: 0 },
92 | seed: { value: 1.0 },
93 | },
94 |
95 | vertexShader: `
96 |
97 | varying vec2 vUv;
98 |
99 | void main() {
100 | vUv = uv;
101 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
102 | }
103 |
104 | `,
105 |
106 | fragmentShader: `
107 |
108 | ${noiseHelpers}
109 |
110 | uniform sampler2D tDiffuse1;
111 | uniform sampler2D tDiffuse2;
112 | uniform float amount;
113 | uniform float seed;
114 | uniform vec2 resolution;
115 | varying vec2 vUv;
116 |
117 | vec2 tile(vec2 position, vec2 resolution, float size, float scale) {
118 | vec2 tileSize = vec2(size / resolution.x * scale, size / resolution.y);
119 | return tileSize * floor(position / tileSize);
120 | }
121 |
122 | float glitchNoise(vec2 position, vec2 resolution, float amount, float seed) {
123 | // the amount affects the seeds used for noise and the multipliers for each type of glitch
124 | float noise = 0.0;
125 |
126 | // large rectangular glitch blocks
127 | noise += max(snoise(tile(position, resolution, 488.0, 15.0) * (1.0 + amount * seed * 8.0)) * amount - 0.5, 0.0);
128 |
129 | // medium square glitch blocks
130 | noise += max(snoise(tile(position, resolution, 100.0, 1.0) * (4.0 + amount * seed * 2.0)) * amount - 0.3, 0.0);
131 |
132 | // medium rectangular glitch blocks
133 | noise += max(snoise(tile(position, resolution, 120.0, 8.0) * (4.0 + amount * seed * 4.0)) * amount - 0.2, 0.0);
134 | noise += max(snoise(tile(position, resolution, 125.0, 8.0) * (4.0 + amount * seed * 4.0)) * amount - 0.2, 0.0);
135 |
136 | // small rectangular glitch blocks
137 | noise += max(snoise(tile(position, resolution, 29.0, 16.0) * (4.0 + amount * seed * 2.0)) * amount - 0.2, 0.0);
138 |
139 | // small square glitch blocks
140 | noise += max(snoise(tile(position, resolution, 29.0, 1.0) * (8.0 + amount * seed * 2.0)) * amount - 0.7, 0.0);
141 |
142 | if (noise >= 0.6) {
143 | // thin glitch lines - fill existing glitch blocks
144 | noise += max(snoise(tile(position, resolution, 1.1, 1000.0) * 1000.0) * amount, 0.0);
145 | } else if (noise <= 0.0) {
146 | // thin glitch lines - fill remaining empty space
147 | float lineNoise = max(snoise(tile(position, resolution, 1.1, 500.0) * (500.0 + amount * seed * 100.0)) * amount, 0.0);
148 | lineNoise += min(snoise(tile(position, resolution, 100.0, 3.0) * (4.0 + amount * seed * 2.0)) * amount, 0.0);
149 | noise += max(lineNoise, 0.0);
150 | }
151 |
152 | // coerce to max glitch amount
153 | float glitchCoerceThreshold = 0.9;
154 | if (amount >= glitchCoerceThreshold) {
155 | float percent = (amount - glitchCoerceThreshold) / (1.0 - glitchCoerceThreshold);
156 | return noise + (1.0 * percent);
157 | }
158 |
159 | return noise;
160 | }
161 |
162 | vec4 rgbShift(sampler2D tex, vec2 position, vec3 offset) {
163 | vec4 r = texture2D(tex, position + vec2(offset.r, 0.0));
164 | vec4 g = texture2D(tex, position + vec2(offset.g, 0.0));
165 | vec4 b = texture2D(tex, position + vec2(offset.b, 0.0));
166 | return vec4(r.r, g.g, b.b, 1.0);
167 | }
168 |
169 | void main() {
170 | float glitch = glitchNoise(vUv, resolution, amount, seed);
171 |
172 | vec3 rgbShiftOffset = vec3(0.01, 0.0, -0.01);
173 | vec4 texel1 = texture2D(tDiffuse1, vUv);
174 | vec4 shiftedTexel1 = rgbShift(tDiffuse1, vUv, rgbShiftOffset);
175 | vec4 texel2 = texture2D(tDiffuse2, vUv);
176 | vec4 shiftedTexel2 = rgbShift(tDiffuse2, vUv, rgbShiftOffset);
177 |
178 | vec4 color = texel1;
179 | if (glitch >= 0.95) {
180 | // no glitching
181 | color = texel2;
182 | } else if (glitch >= 0.7) {
183 | // color-shifted new texture
184 | color = shiftedTexel2;
185 | } else if (glitch >= 0.6) {
186 | // color-shifted original texture
187 | color = shiftedTexel1;
188 | } else if (glitch >= 0.5) {
189 | // magenta glitch blocks
190 | color = texel1 * vec4(1.2, 0.0, 1.2, 0.5);
191 | } else if (glitch >= 0.4) {
192 | // cyan glitch blocks
193 | color = texel1 * vec4(0.0, 1.2, 1.2, 0.5);
194 | } else if (glitch >= 0.38) {
195 | // bright color-shifted new texture
196 | color = shiftedTexel2 * 1.5;
197 | } else if (glitch >= 0.2) {
198 | // color-shifted original texture
199 | color = shiftedTexel1;
200 | }
201 |
202 | gl_FragColor = color;
203 | }
204 |
205 | `,
206 | };
207 |
208 | export {
209 | GlitchShader,
210 | };
211 |
212 | export default GlitchShader;
213 |
--------------------------------------------------------------------------------
/src/effects/shaders/transition/slide-shader.ts:
--------------------------------------------------------------------------------
1 | import { Shader } from 'three';
2 |
3 | /**
4 | * @author aeroheim / http://aeroheim.moe/
5 | */
6 |
7 | export enum SlideDirection {
8 | Left = 0,
9 | Right = 1,
10 | Top = 2,
11 | Bottom = 3,
12 | }
13 |
14 | const SlideShader: Shader = {
15 | uniforms: {
16 | tDiffuse1: { value: null },
17 | tDiffuse2: { value: null },
18 | // the number of slides to perform
19 | slides: { value: 1.0 },
20 | // a value from 0 to 1 indicating the slide ratio
21 | amount: { value: 0.0 },
22 | // the amount value of the previous frame - used to calculate the velocity for the blur
23 | prevAmount: { value: 0.0 },
24 | // a positive value that affects the intensity of the blur
25 | intensity: { value: 1.0 },
26 | // the direction to slide to
27 | direction: { value: SlideDirection.Right },
28 | // the number of samples to use (up to 128) - higher samples result in better quality at the cost of performance
29 | samples: { value: 32 },
30 | },
31 |
32 | vertexShader: `
33 |
34 | varying vec2 vUv;
35 |
36 | void main() {
37 | vUv = uv;
38 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
39 | }
40 |
41 | `,
42 |
43 | // TODO: refactor and reduce branching for performance
44 | fragmentShader: `
45 |
46 | const int MAX_SAMPLES = 128;
47 |
48 | uniform sampler2D tDiffuse1;
49 | uniform sampler2D tDiffuse2;
50 | uniform int slides;
51 | uniform float amount;
52 | uniform float prevAmount;
53 | uniform float intensity;
54 | uniform int direction;
55 | uniform int samples;
56 | varying vec2 vUv;
57 |
58 | float getComponentForDirection(int direction, vec2 uv) {
59 | return direction < 2 ? uv.x : uv.y;
60 | }
61 |
62 | vec2 getVectorForDirection(int direction, vec2 uv, float position) {
63 | return direction < 2 ? vec2(position, uv.y) : vec2(uv.x, position);
64 | }
65 |
66 | float getOffsetPosition(int direction, float uv, float offset) {
67 | return direction == 1 || direction == 3
68 | ? mod(uv + offset, 1.0)
69 | : mod(uv + (1.0 - offset), 1.0);
70 | }
71 |
72 | void main() {
73 | vec4 texel;
74 | float offset = amount * float(slides);
75 | float position = getComponentForDirection(direction, vUv);
76 |
77 | bool isFirstSlide = direction == 1 || direction == 3
78 | ? position + offset <= 1.0
79 | : position - offset >= 0.0;
80 |
81 | if (isFirstSlide) {
82 | texel = texture2D(tDiffuse1, getVectorForDirection(direction, vUv, getOffsetPosition(direction, position, offset)));
83 | } else {
84 | texel = texture2D(tDiffuse2, getVectorForDirection(direction, vUv, getOffsetPosition(direction, position, offset)));
85 | }
86 |
87 | float velocity = (amount - prevAmount) * intensity;
88 | for (int i = 1; i < MAX_SAMPLES; ++i) {
89 | if (i >= samples) {
90 | // hack to allow loop comparisons against uniforms
91 | break;
92 | }
93 | float blurOffset = velocity * (float(i) / float(samples - 1) - 0.5);
94 | bool isFirstSlide = direction == 1 || direction == 3
95 | ? position + offset + blurOffset <= 1.0
96 | : position - offset - blurOffset >= 0.0;
97 | if (isFirstSlide) {
98 | texel += texture2D(tDiffuse1, getVectorForDirection(direction, vUv, getOffsetPosition(direction, position, offset + blurOffset)));
99 | } else {
100 | texel += texture2D(tDiffuse2, getVectorForDirection(direction, vUv, getOffsetPosition(direction, position, offset + blurOffset)));
101 | }
102 | }
103 |
104 | gl_FragColor = texel / max(1.0, float(samples));
105 | }
106 |
107 | `,
108 | };
109 |
110 | export {
111 | SlideShader,
112 | };
113 |
114 | export default SlideShader;
115 |
--------------------------------------------------------------------------------
/src/effects/shaders/transition/wipe-shader.ts:
--------------------------------------------------------------------------------
1 | import { Shader } from 'three';
2 |
3 | /**
4 | * @author aeroheim / http://aeroheim.moe/
5 | */
6 |
7 | export enum WipeDirection {
8 | Left = 0,
9 | Right = 1,
10 | Top = 2,
11 | Bottom = 3,
12 | }
13 |
14 | const WipeShader: Shader = {
15 | uniforms: {
16 | tDiffuse1: { value: null },
17 | tDiffuse2: { value: null },
18 | // a value from 0 to 1 indicating the ratio of the texture wipe
19 | amount: { value: 0.0 },
20 | // a value from 0 to 1 indicating the size of the blend gradient
21 | gradient: { value: 0.0 },
22 | // the direction to wipe to
23 | direction: { value: WipeDirection.Right },
24 | // the angle of the wipe
25 | angle: { value: 0.0 },
26 | // the aspect ratio of the texture. required using an angle
27 | aspect: { value: 1.0 },
28 | },
29 |
30 | vertexShader: `
31 |
32 | varying vec2 vUv;
33 |
34 | void main() {
35 | vUv = uv;
36 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
37 | }
38 |
39 | `,
40 |
41 | // TODO: refactor and reduce branching for performance
42 | fragmentShader: `
43 |
44 | uniform sampler2D tDiffuse1;
45 | uniform sampler2D tDiffuse2;
46 | uniform float amount;
47 | uniform float gradient;
48 | uniform int direction;
49 | uniform float angle;
50 | uniform float aspect;
51 | varying vec2 vUv;
52 |
53 | void main() {
54 | vec4 texel1 = texture2D(tDiffuse1, vUv);
55 | vec4 texel2 = texture2D(tDiffuse2, vUv);
56 |
57 | float position;
58 | if (direction == 0) {
59 | // WipeDirection.LEFT
60 | position = 1.0 - vUv.x;
61 | } else if (direction == 1) {
62 | // WipeDirection.RIGHT
63 | position = vUv.x;
64 | } else if (direction == 2) {
65 | // WipeDirection.TOP
66 | position = vUv.y;
67 | } else if (direction == 3) {
68 | // WipeDirection.BOTTOM
69 | position = 1.0 - vUv.y;
70 | }
71 |
72 | float rotationOffset;
73 | float rotatedPosition;
74 | if (direction < 2) {
75 | // rotation for horizontal wipes
76 | float slope = 1.0 / tan(angle);
77 | rotationOffset = (1.0 / slope) / aspect;
78 | rotatedPosition = (vUv.y / slope) / aspect;
79 | } else {
80 | // rotation for vertical wipes
81 | float slope = tan(angle);
82 | rotationOffset = slope / aspect;
83 | rotatedPosition = (vUv.x * slope) / aspect;
84 | }
85 |
86 | // a tween that starts from one side of the texture and ends at the other side.
87 | // this tween accounts for offsets due to the size of the blend gradient and angle of the wipe effect.
88 | float wipeOffset = (-max(0.0, rotationOffset) - gradient) + ((1.0 + abs(rotationOffset) + gradient) * amount) + rotatedPosition;
89 | if (position <= wipeOffset) {
90 | gl_FragColor = texel2;
91 | } else if (position <= wipeOffset + gradient) {
92 | gl_FragColor = mix(texel2, texel1, (position - wipeOffset) / gradient);
93 | } else {
94 | gl_FragColor = texel1;
95 | }
96 | }
97 |
98 | `,
99 | };
100 |
101 | export {
102 | WipeShader,
103 | };
104 |
105 | export default WipeShader;
106 |
--------------------------------------------------------------------------------
/src/midori.esm.js:
--------------------------------------------------------------------------------
1 | // ESM library export
2 | import midori from './midori.cjs';
3 |
4 | export const {
5 | BackgroundRenderer, loadImage, isWebGLSupported, Background,
6 | BackgroundCamera, BackgroundEffects, EffectPass, Particles,
7 | TransitionType, EffectType, SlideDirection, WipeDirection, Easings,
8 | } = midori.midori;
9 |
--------------------------------------------------------------------------------
/src/midori.ts:
--------------------------------------------------------------------------------
1 | // CommonJS library export
2 |
3 | export { BackgroundRenderer, loadImage, isWebGLSupported } from './background-renderer';
4 | export { Background } from './background';
5 | export { BackgroundCamera } from './background-camera';
6 | export { BackgroundEffects } from './background-effects';
7 | export { EffectPass } from './pipeline/effect-pass';
8 | export { Particles } from './effects/particles';
9 | export { TransitionType } from './pipeline/transition-pass';
10 | export { EffectType } from './effects/effect';
11 | export { SlideDirection } from './effects/shaders/transition/slide-shader';
12 | export { WipeDirection } from './effects/shaders/transition/wipe-shader';
13 | export { Easings } from './transition';
14 |
--------------------------------------------------------------------------------
/src/pipeline/background-pass.ts:
--------------------------------------------------------------------------------
1 | import { WebGLRenderer, WebGLRenderTarget } from 'three';
2 | import { Pass } from 'three/examples/jsm/postprocessing/Pass';
3 | import { Background } from '../background';
4 |
5 | class BackgroundPass extends Pass {
6 | private _background: Background;
7 |
8 | /**
9 | * Constructs a BackgroundPass.
10 | * @param {Background} background
11 | */
12 | constructor(background: Background) {
13 | super();
14 | this._background = background;
15 | }
16 |
17 | /**
18 | * Sets the current background.
19 | * @param {Background} background
20 | */
21 | setBackground(background: Background): void {
22 | this._background = background;
23 | }
24 |
25 | /**
26 | * Returns the current background.
27 | * @returns Background
28 | */
29 | get background(): Background {
30 | return this._background;
31 | }
32 |
33 | /**
34 | * Sets the size of the current background.
35 | * @param {number} width
36 | * @param {number} height
37 | */
38 | setSize(width: number, height: number): void {
39 | this._background.setSize(width, height);
40 | }
41 |
42 | /**
43 | * Renders the current background.
44 | * @param {WebGLRenderer} renderer - the renderer to use.
45 | * @param {WebGLRenderTarget} writeBuffer - the buffer to render to, or null to render directly to screen.
46 | */
47 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget): void {
48 | this._background.render(renderer, this.renderToScreen ? null : writeBuffer);
49 | }
50 |
51 | /**
52 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
53 | */
54 | dispose(): void {
55 | this._background.dispose();
56 | }
57 | }
58 |
59 | export {
60 | BackgroundPass,
61 | };
62 |
63 | export default BackgroundPass;
64 |
--------------------------------------------------------------------------------
/src/pipeline/effect-pass.ts:
--------------------------------------------------------------------------------
1 | import { WebGLRenderTarget, WebGLRenderer, MathUtils } from 'three';
2 | import { Pass } from 'three/examples/jsm/postprocessing/Pass';
3 | import { CopyShader } from 'three/examples/jsm/shaders/CopyShader';
4 | import { EffectType, Effect, GaussianBlurEffect, BloomEffect, VignetteBlurEffect, GlitchEffect, IEffect, RGBShiftEffect, VignetteEffect } from '../effects/effect';
5 |
6 | type EffectTypeConfig> = {
7 | [EffectType.Blur]: BlurEffectConfig;
8 | [EffectType.Bloom]: BloomEffectConfig;
9 | [EffectType.RgbShift]: RgbShiftEffectConfig;
10 | [EffectType.Vignette]: VignetteEffectConfig;
11 | [EffectType.VignetteBlur]: VignetteBlurEffectConfig;
12 | [EffectType.Glitch]: GlitchEffectConfig;
13 | }[T];
14 |
15 | type EffectConfigs = {
16 | [T in Exclude]?: EffectTypeConfig;
17 | }
18 |
19 | type EffectMap = Partial>;
20 |
21 | type EffectConfig = BlurEffectConfig | BloomEffectConfig | RgbShiftEffectConfig | VignetteEffectConfig | VignetteBlurEffectConfig | GlitchEffectConfig;
22 |
23 | interface BlurEffectConfig {
24 | // the size of the blur.
25 | radius?: number;
26 | // the number of blur passes - more passes result in stronger blurs and less artifacts at the cost of performance.
27 | passes?: number;
28 | }
29 |
30 | interface BloomEffectConfig {
31 | // the overall brightness of the bloom.
32 | opacity?: number;
33 | // the size of the bloom.
34 | radius?: number;
35 | // the number of bloom passes - more passes result in stronger blooms and less artifacts at the cost of performance.
36 | passes?: number;
37 | }
38 |
39 | interface RgbShiftEffectConfig {
40 | // the distance of the shift.
41 | amount?: number;
42 | // the angle of the shift in degrees.
43 | angle?: number;
44 | }
45 |
46 | interface VignetteEffectConfig {
47 | // the size of the vignette.
48 | offset?: number;
49 | // the intensity of the vignette.
50 | darkness?: number;
51 | }
52 |
53 | interface VignetteBlurEffectConfig {
54 | // the size of the vignette.
55 | size?: number;
56 | // the size of the blur.
57 | radius?: number;
58 | // the number of blur passes - more passes result in stronger blurs and less artifacts at the cost of performance.
59 | passes?: number;
60 | }
61 |
62 | interface GlitchEffectConfig {
63 | // the intensity of the glitch.
64 | amount?: number;
65 | // a random seed from 0 to 1 used to generate glitches.
66 | seed?: number;
67 | }
68 |
69 | class EffectPass extends Pass {
70 | private _width: number;
71 | private _height: number;
72 |
73 | private _readBuffer: WebGLRenderTarget;
74 | private _writeBuffer: WebGLRenderTarget;
75 | private _copyShader: Effect = new Effect(CopyShader);
76 |
77 | protected _effects: EffectMap = {};
78 |
79 | /**
80 | * Constructs an EffectPass.
81 | * @param {number} width
82 | * @param {number} height
83 | */
84 | constructor(width: number, height: number) {
85 | super();
86 | this._width = width;
87 | this._height = height;
88 | this._readBuffer = new WebGLRenderTarget(width, height);
89 | this._writeBuffer = new WebGLRenderTarget(width, height);
90 |
91 | // this pass only needs to render when there is at least one effect, so it should be disabled by default.
92 | this.enabled = false;
93 | }
94 |
95 | /**
96 | * Sets the size of the EffectPass.
97 | * @param {number} width
98 | * @param {number} height
99 | */
100 | setSize(width: number, height: number): void {
101 | this._width = width;
102 | this._height = height;
103 | this._readBuffer.setSize(width, height);
104 | this._writeBuffer.setSize(width, height);
105 |
106 | for (const effect of Object.values(this._effects)) {
107 | if (effect.setSize) {
108 | effect.setSize(width, height);
109 | }
110 | }
111 | }
112 |
113 | /**
114 | * Returns the configurations for the currently set effects.
115 | * @returns EffectConfigs
116 | */
117 | getConfigs(): EffectConfigs {
118 | const configs: EffectConfigs = {};
119 | for (const [type, effect] of Object.entries(this._effects)) {
120 | switch (type) {
121 | case EffectType.Blur: {
122 | const { radius } = effect.getUniforms();
123 | configs[type] = { radius, passes: (effect as GaussianBlurEffect).passes };
124 | break;
125 | }
126 | case EffectType.Bloom: {
127 | const { opacity, radius } = effect.getUniforms();
128 | configs[type] = { opacity, radius, passes: (effect as BloomEffect).passes };
129 | break;
130 | }
131 | case EffectType.RgbShift: {
132 | const { amount, angle } = effect.getUniforms();
133 | configs[type] = { amount, angle: MathUtils.radToDeg(angle as number) };
134 | break;
135 | }
136 | case EffectType.Vignette: {
137 | const { offset, darkness } = effect.getUniforms();
138 | configs[type] = { offset, darkness };
139 | break;
140 | }
141 | case EffectType.VignetteBlur: {
142 | const { size, radius } = effect.getUniforms();
143 | configs[type] = { size, radius, passes: (effect as VignetteBlurEffect).passes };
144 | break;
145 | }
146 | case EffectType.Glitch: {
147 | const { amount, seed } = effect.getUniforms();
148 | configs[type] = { amount, seed };
149 | break;
150 | }
151 | }
152 | }
153 | return configs;
154 | }
155 |
156 | /**
157 | * Returns whether a specified effect is currently set.
158 | * @param {EffectType} type
159 | * @returns boolean
160 | */
161 | hasEffect(type: EffectType): boolean {
162 | return this._effects.hasOwnProperty(type);
163 | }
164 |
165 | /**
166 | * Returns whether any effects are currently set.
167 | * @returns boolean
168 | */
169 | hasEffects(): boolean {
170 | return Object.getOwnPropertyNames(this._effects).length !== 0;
171 | }
172 |
173 | /**
174 | * Returns the current effect for the specified type.
175 | * If no effect is currently set for the type, creates a new effect for the type and returns it.
176 | * @param {EffectType} type
177 | * @param {EffectConfig} config
178 | * @returns IEffect
179 | */
180 | protected _getEffect(type: EffectType): IEffect {
181 | if (!(type in this._effects)) {
182 | switch (type) {
183 | case EffectType.Blur:
184 | this._effects[type] = new GaussianBlurEffect(this._width, this._height);
185 | break;
186 | case EffectType.Bloom:
187 | this._effects[type] = new BloomEffect(this._width, this._height);
188 | break;
189 | case EffectType.RgbShift:
190 | this._effects[type] = new RGBShiftEffect();
191 | break;
192 | case EffectType.Vignette:
193 | this._effects[type] = new VignetteEffect();
194 | break;
195 | case EffectType.VignetteBlur:
196 | this._effects[type] = new VignetteBlurEffect(this._width, this._height);
197 | break;
198 | case EffectType.Glitch:
199 | this._effects[type] = new GlitchEffect(this._width, this._height);
200 | break;
201 | }
202 | }
203 |
204 | return this._effects[type]!;
205 | }
206 |
207 | /**
208 | * Sets an effect. If an effect is already set, updates the set effect.
209 | * @param {EffectType} type - the effect to set.
210 | * @param {Object} config - configuration specific to the effect specified.
211 | */
212 | set>(type: T, config: EffectTypeConfig = {}): void {
213 | const effect = this._getEffect(type);
214 |
215 | // enable this pass when there is at least one effect.
216 | this.enabled = true;
217 |
218 | switch (type) {
219 | case EffectType.Blur: {
220 | const { radius = 1, passes = (effect as GaussianBlurEffect).passes } = config as BlurEffectConfig;
221 | (effect as GaussianBlurEffect).passes = passes;
222 | effect.updateUniforms({ radius });
223 | break;
224 | }
225 | case EffectType.Bloom: {
226 | const { opacity = 1, radius = 1, passes = (effect as BloomEffect).passes } = config as BloomEffectConfig;
227 | (effect as BloomEffect).passes = passes;
228 | effect.updateUniforms({ opacity, radius });
229 | break;
230 | }
231 | case EffectType.RgbShift: {
232 | const { amount = 0.005, angle = 0 } = config as RgbShiftEffectConfig;
233 | effect.updateUniforms({ amount, angle: MathUtils.degToRad(angle) });
234 | break;
235 | }
236 | case EffectType.Vignette: {
237 | const { offset = 1, darkness = 1 } = config as VignetteEffectConfig;
238 | effect.updateUniforms({ offset, darkness });
239 | break;
240 | }
241 | case EffectType.VignetteBlur: {
242 | const { size = 1, radius = 1, passes = (effect as VignetteBlurEffect).passes } = config as VignetteBlurEffectConfig;
243 | (effect as VignetteBlurEffect).passes = passes;
244 | effect.updateUniforms({ radius, size });
245 | break;
246 | }
247 | case EffectType.Glitch: {
248 | const { amount = 1, seed = Math.random() } = config as GlitchEffectConfig;
249 | effect.updateUniforms({ amount, seed });
250 | break;
251 | }
252 | }
253 | }
254 |
255 | /**
256 | * Removes a set effect. Returns true if the effect was removed, otherwise false.
257 | * @param {EffectType} type - the type of the effect.
258 | * @returns boolean
259 | */
260 | remove(type: EffectType): boolean {
261 | if (type in this._effects) {
262 | this._effects[type]!.dispose();
263 | delete this._effects[type];
264 |
265 | // disable this pass if there are no effects left.
266 | this.enabled = this.hasEffects();
267 | return true;
268 | }
269 |
270 | return false;
271 | }
272 |
273 | /**
274 | * Removes all set effects.
275 | */
276 | removeAll(): void {
277 | for (const type in this._effects) {
278 | this._effects[type].dispose();
279 | delete this._effects[type];
280 | }
281 | this.enabled = false;
282 | }
283 |
284 | /**
285 | * Swaps the internal read and write buffers. Should be called each time after rendering an effect.
286 | */
287 | private _swapBuffers() {
288 | const tmp = this._readBuffer;
289 | this._readBuffer = this._writeBuffer;
290 | this._writeBuffer = tmp;
291 | }
292 |
293 | /**
294 | * Renders the effects.
295 | * @param {WebGLRenderer} renderer - the renderer to use.
296 | * @param {WebGLRenderTarget | null} writeBuffer - the buffer to render to, or null to render directly to screen.
297 | * @param {WebGLRenderTarget} readBuffer - the buffer to read from.
298 | */
299 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget | null, readBuffer: WebGLRenderTarget): void {
300 | this._copyShader.render(renderer, this._readBuffer, readBuffer);
301 | for (const effect of Object.values(this._effects)) {
302 | effect.render(renderer, this._writeBuffer, this._readBuffer);
303 | this._swapBuffers();
304 | }
305 | this._copyShader.render(renderer, this.renderToScreen ? null : writeBuffer, this._readBuffer);
306 | }
307 |
308 | /**
309 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
310 | */
311 | dispose(): void {
312 | this._copyShader.dispose();
313 | this._readBuffer.dispose();
314 | this._readBuffer.texture.dispose();
315 | this._writeBuffer.dispose();
316 | this._writeBuffer.texture.dispose();
317 | Object.values(this._effects).forEach(effect => effect.dispose());
318 | }
319 | }
320 |
321 | export {
322 | EffectConfig,
323 | BlurEffectConfig,
324 | BloomEffectConfig,
325 | RgbShiftEffectConfig,
326 | VignetteEffectConfig,
327 | VignetteBlurEffectConfig,
328 | GlitchEffectConfig,
329 | EffectConfigs,
330 | EffectPass,
331 | };
332 |
333 | export default EffectPass;
334 |
--------------------------------------------------------------------------------
/src/pipeline/transition-pass.ts:
--------------------------------------------------------------------------------
1 | import { Tween, Easing } from '@tweenjs/tween.js';
2 | import { WebGLRenderTarget, Vector2, Shader, WebGLRenderer, MathUtils } from 'three';
3 | import { Pass } from 'three/examples/jsm/postprocessing/Pass';
4 | import { BlendShader } from 'three/examples/jsm/shaders/BlendShader';
5 | import { WipeShader, WipeDirection } from '../effects/shaders/transition/wipe-shader';
6 | import { SlideShader, SlideDirection } from '../effects/shaders/transition/slide-shader';
7 | import { BlurShader } from '../effects/shaders/transition/blur-shader';
8 | import { GlitchShader } from '../effects/shaders/transition/glitch-shader';
9 | import { Background } from '../background';
10 | import { TransitionEffect } from '../effects/effect';
11 | import { BackgroundTransitionConfig } from '../transition';
12 | import { Uniforms } from '../effects/shaders/shader-utils';
13 |
14 | enum TransitionType {
15 | None = 'None',
16 | Blend = 'Blend',
17 | Blur = 'Blur',
18 | Wipe = 'Wipe',
19 | Slide = 'Slide',
20 | Glitch = 'Glitch',
21 | }
22 |
23 | type TransitionTypeConfig = {
24 | [TransitionType.None]: BackgroundTransitionConfig;
25 | [TransitionType.Blend]: BlendTransitionConfig;
26 | [TransitionType.Blur]: BlurTransitionConfig;
27 | [TransitionType.Wipe]: WipeTransitionConfig;
28 | [TransitionType.Slide]: SlideTransitionConfig;
29 | [TransitionType.Glitch]: GlitchTransitionConfig;
30 | }[T];
31 |
32 | interface BlendTransitionConfig extends BackgroundTransitionConfig {}
33 |
34 | interface WipeTransitionConfig extends BackgroundTransitionConfig {
35 | // the size of the fade when wiping.
36 | gradient?: number;
37 | // the angle of the wipe in degrees.
38 | angle?: number;
39 | // the direction of the wipe.
40 | direction?: WipeDirection;
41 | }
42 |
43 | interface SlideTransitionConfig extends BackgroundTransitionConfig {
44 | // the number of slides to perform.
45 | slides?: number;
46 | // the intensity of the blur during slides.
47 | intensity?: number;
48 | // the number of samples for the blur - more samples result in better quality at the cost of performance.
49 | samples?: number;
50 | // the direction of the slide.
51 | direction?: SlideDirection;
52 | }
53 |
54 | interface BlurTransitionConfig extends BackgroundTransitionConfig {
55 | // the intensity of the blur.
56 | intensity?: number;
57 | // the number of samples for the blur - more samples result in better quality at the cost of performance.
58 | samples?: number;
59 | }
60 |
61 | interface GlitchTransitionConfig extends BackgroundTransitionConfig {
62 | // a random seed from 0 to 1 used to generate glitches.
63 | seed?: number;
64 | }
65 |
66 | interface TransitionTweenValues {
67 | amount: number;
68 | }
69 |
70 | interface TransitionTweenConfig extends Pick, 'easing' | 'delay' | 'duration'> {
71 | from: TransitionTweenValues;
72 | to: TransitionTweenValues;
73 | onInit: () => void;
74 | onStart: () => void;
75 | onUpdate: (values: TransitionTweenValues) => void;
76 | onComplete: () => void;
77 | onStop: () => void;
78 | }
79 |
80 | class TransitionPass extends Pass {
81 | private _width: number;
82 | private _height: number;
83 |
84 | private _prevBackground: Background; // the prev background to transition away from
85 | private _buffer: WebGLRenderTarget; // a buffer to render the prev background during transitions
86 |
87 | private _transition: Tween = new Tween({ amount: 0 });
88 | private _transitionEffect: TransitionEffect = new TransitionEffect(BlendShader, { mixRatio: 1 });
89 |
90 | /**
91 | * Constructs a TransitionPass.
92 | * @param {Background | null} background
93 | * @param {number} width
94 | * @param {number} height
95 | */
96 | constructor(background: Background | null, width: number, height: number) {
97 | super();
98 | this._width = width;
99 | this._height = height;
100 | this._prevBackground = background ?? new Background(null, width, height);
101 | this._buffer = new WebGLRenderTarget(width, height);
102 |
103 | // this pass only needs to render when a transition occurs, so it should be disabled by default.
104 | this.enabled = false;
105 | }
106 |
107 | /**
108 | * Sets the size of the TransitionPass.
109 | * @param {number} width
110 | * @param {number} height
111 | */
112 | setSize(width: number, height: number): void {
113 | this._width = width;
114 | this._height = height;
115 | this._prevBackground.setSize(width, height);
116 | this._buffer.setSize(width, height);
117 | }
118 |
119 | /**
120 | * Returns whether a transition is currently occurring.
121 | * @returns boolean
122 | */
123 | isTransitioning(): boolean {
124 | return this._transition.isPlaying();
125 | }
126 |
127 | /**
128 | * Renders a transition effect over the screen.
129 | * @param {Background} background - the background to transition to.
130 | * @param {TransitionType} transition - the transition to use.
131 | * @param {BackgroundTransitionConfig} config - configuration for the transition.
132 | */
133 | transition(background: Background, transition: T, config: TransitionTypeConfig = {}): void {
134 | const {
135 | from,
136 | to,
137 | duration,
138 | delay,
139 | easing,
140 | onInit,
141 | onStart,
142 | onUpdate,
143 | onComplete,
144 | onStop,
145 | } = this._getTweenConfig(background, transition, config);
146 |
147 | this._transition.stop();
148 | onInit();
149 | this._transition = new Tween(from)
150 | .to(to, duration)
151 | .easing(easing)
152 | .onStart(onStart)
153 | .onUpdate(onUpdate)
154 | .onComplete(onComplete)
155 | .onStop(onStop)
156 | .delay(delay)
157 | .start();
158 | }
159 |
160 | /**
161 | * Sets the internal transition effect to be used.
162 | * @param {Shader} shader - a shader definition.
163 | * @param {Uniforms} uniforms - a map that defines the values of the uniforms to be used.
164 | */
165 | private _setTransitionEffect(shader: Shader, uniforms: Uniforms = {}) {
166 | this._transitionEffect.dispose();
167 | this._transitionEffect = new TransitionEffect(shader, uniforms);
168 | }
169 |
170 | /**
171 | * Returns a tween configuration for the specified transition type.
172 | * @param {Background} background - the background to transition to.
173 | * @param {TransitionType} transition - the type of the transition.
174 | * @param {BackgroundTransitionConfig} config - configuration for the transition.
175 | */
176 | private _getTweenConfig(background: Background, transition: T, config: TransitionTypeConfig = {}): TransitionTweenConfig {
177 | const onTransitionStart = () => {
178 | // enable this pass when a transition starts.
179 | this.enabled = true;
180 | };
181 | const onTransitionEnd = () => {
182 | // disable this pass after a transition finishes.
183 | this.enabled = false;
184 | this._prevBackground.dispose();
185 | // cache the new background to be used for the next transition.
186 | this._prevBackground = background;
187 | };
188 |
189 | const {
190 | easing = Easing.Linear.None,
191 | duration = 0,
192 | delay = 0,
193 | onInit = () => ({}),
194 | onStart = () => ({}),
195 | onUpdate = () => ({}),
196 | onComplete = () => ({}),
197 | onStop = () => ({}),
198 | ...additionalConfig
199 | } = config;
200 |
201 | const baseTransitionConfig = {
202 | from: { amount: 0 },
203 | to: { amount: 1 },
204 | easing,
205 | duration: duration * 1000,
206 | delay: delay * 1000,
207 | onInit: () => onInit(this._prevBackground, background),
208 | onStart: () => {
209 | onStart(this._prevBackground, background);
210 | onTransitionStart();
211 | },
212 | onUpdate: () => onUpdate(this._prevBackground, background),
213 | onComplete: () => {
214 | onComplete(this._prevBackground, background);
215 | onTransitionEnd();
216 | },
217 | onStop: () => {
218 | onStop(this._prevBackground, background);
219 | onTransitionEnd();
220 | },
221 | };
222 |
223 | switch (transition) {
224 | case TransitionType.None: {
225 | const { onStart } = baseTransitionConfig;
226 | return {
227 | ...baseTransitionConfig,
228 | onStart: () => {
229 | this._setTransitionEffect(BlendShader, { mixRatio: 1 });
230 | onStart();
231 | },
232 | };
233 | }
234 | case TransitionType.Blend: {
235 | const { onStart, onUpdate } = baseTransitionConfig;
236 | return {
237 | ...baseTransitionConfig,
238 | onStart: () => {
239 | this._setTransitionEffect(BlendShader);
240 | onStart();
241 | },
242 | onUpdate: ({ amount: mixRatio }) => {
243 | this._transitionEffect.updateUniforms({ mixRatio });
244 | onUpdate();
245 | },
246 | };
247 | }
248 | case TransitionType.Wipe: {
249 | const { onStart, onUpdate } = baseTransitionConfig;
250 | const { gradient = 0, angle = 0, direction = WipeDirection.Right } = additionalConfig as WipeTransitionConfig;
251 | return {
252 | ...baseTransitionConfig,
253 | onStart: () => {
254 | this._setTransitionEffect(WipeShader, {
255 | gradient,
256 | angle: MathUtils.degToRad(angle),
257 | direction,
258 | aspect: this._width / this._height,
259 | });
260 | onStart();
261 | },
262 | onUpdate: ({ amount }) => {
263 | // update the aspect ratio incase it changes in the middle of the transition
264 | this._transitionEffect.updateUniforms({ amount, aspect: this._width / this._height });
265 | onUpdate();
266 | },
267 | };
268 | }
269 | case TransitionType.Slide: {
270 | const { onStart, onUpdate } = baseTransitionConfig;
271 | const { slides = 1, intensity = 1, samples = 32, direction = SlideDirection.Right } = additionalConfig as SlideTransitionConfig;
272 | return {
273 | ...baseTransitionConfig,
274 | onStart: () => {
275 | this._setTransitionEffect(SlideShader, {
276 | slides,
277 | intensity,
278 | samples,
279 | direction,
280 | });
281 | onStart();
282 | },
283 | onUpdate: ({ amount }) => {
284 | const { amount: prevAmount } = this._transitionEffect.getUniforms();
285 | this._transitionEffect.updateUniforms({ prevAmount, amount });
286 | onUpdate();
287 | },
288 | };
289 | }
290 | case TransitionType.Blur: {
291 | const { onStart, onUpdate } = baseTransitionConfig;
292 | const { intensity = 1, samples = 32 } = additionalConfig as BlurTransitionConfig;
293 | return {
294 | ...baseTransitionConfig,
295 | onStart: () => {
296 | this._setTransitionEffect(BlurShader, { intensity, samples });
297 | onStart();
298 | },
299 | onUpdate: ({ amount }) => {
300 | const { amount: prevAmount } = this._transitionEffect.getUniforms();
301 | this._transitionEffect.updateUniforms({ prevAmount, amount });
302 | onUpdate();
303 | },
304 | };
305 | }
306 | case TransitionType.Glitch: {
307 | const { onStart, onUpdate } = baseTransitionConfig;
308 | const { seed = Math.random() } = additionalConfig as GlitchTransitionConfig;
309 | return {
310 | ...baseTransitionConfig,
311 | onStart: () => {
312 | this._setTransitionEffect(GlitchShader, { seed, resolution: new Vector2(this._width, this._height) });
313 | onStart();
314 | },
315 | onUpdate: ({ amount }) => {
316 | // update the resolution incase it changes in the middle of the transition
317 | const { resolution } = this._transitionEffect.getUniforms();
318 | resolution.set(this._width, this._height);
319 | this._transitionEffect.updateUniforms({ amount, });
320 | onUpdate();
321 | },
322 | };
323 | }
324 | default:
325 | return baseTransitionConfig;
326 | }
327 | }
328 |
329 | /**
330 | * Renders the transition.
331 | * @param {WebGLRenderer} renderer - the renderer to use.
332 | * @param {WebGLRenderTarget} writeBuffer - the buffer to render to, or null to render directly to screen.
333 | * @param {WebGLRenderTarget} readBuffer - the buffer to read from which contains the current background.
334 | */
335 | render(renderer: WebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget): void {
336 | if (this.isTransitioning()) {
337 | this._prevBackground.render(renderer, this._buffer);
338 | this._transitionEffect.render(renderer, this.renderToScreen ? null : writeBuffer, this._buffer, readBuffer);
339 | }
340 | }
341 |
342 | /**
343 | * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
344 | */
345 | dispose(): void {
346 | this._transition.stop();
347 | this._prevBackground.dispose();
348 | this._buffer.dispose();
349 | this._transitionEffect.dispose();
350 | }
351 | }
352 |
353 | export {
354 | TransitionType,
355 | BlendTransitionConfig,
356 | WipeTransitionConfig,
357 | SlideTransitionConfig,
358 | BlurTransitionConfig,
359 | GlitchTransitionConfig,
360 | TransitionPass,
361 | };
362 |
363 | export default TransitionPass;
364 |
--------------------------------------------------------------------------------
/src/transition.ts:
--------------------------------------------------------------------------------
1 | import { Easing } from '@tweenjs/tween.js';
2 | import { Background } from './background';
3 |
4 | const Easings = Easing;
5 |
6 | interface TransitionConfig {
7 | // the duration of the transition in seconds.
8 | duration?: number;
9 | // an optional delay before the transition starts in seconds.
10 | delay?: number;
11 | // an optional easing function for the transition.
12 | easing?: (k: number) => number;
13 | // an optional callback - invoked when the transition is registered, regardless of delay.
14 | onInit?: (...args: any[]) => void;
15 | // an optional callback - invoked when the transition starts after the delay has elapsed.
16 | onStart?: (...args: any[]) => void;
17 | // an optional callback - invoked for each frame that the transition runs.
18 | onUpdate?: (...args: any[]) => void;
19 | // an optional callback - invoked when the transition has finished.
20 | onComplete?: (...args: any[]) => void;
21 | // an optional callback - invoked when the transition is interrupted or stopped.
22 | onStop?: (...args: any[]) => void;
23 | }
24 |
25 | interface LoopableTransitionConfig extends TransitionConfig {
26 | // whether to loop the transition repeatedly or not.
27 | loop?: boolean;
28 | }
29 |
30 | interface BackgroundTransitionConfig extends TransitionConfig {
31 | onInit?: (prevBackground: Background, nextBackground: Background) => void;
32 | onStart?: (prevBackground: Background, nextBackground: Background) => void;
33 | onUpdate?: (prevBackground: Background, nextBackground: Background) => void;
34 | onComplete?: (prevBackground: Background, nextBackground: Background) => void;
35 | onStop?: (prevBackground: Background, nextBackground: Background) => void;
36 | }
37 |
38 | export {
39 | TransitionConfig,
40 | LoopableTransitionConfig,
41 | BackgroundTransitionConfig,
42 | Easings,
43 | };
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 |
2 | function clamp(value: number, min: number, max: number): number {
3 | return Math.max(Math.min(value, max), min);
4 | }
5 |
6 | export {
7 | clamp,
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "module": "es6",
5 | "target": "es6",
6 | "jsx": "react",
7 | "allowSyntheticDefaultImports": true,
8 | "moduleResolution": "node",
9 | "strictNullChecks": true,
10 | },
11 | "include": [
12 | "src/**/*",
13 | "docs/**/*",
14 | ],
15 | "exclude": ["node_modules"]
16 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const CopyPlugin = require("copy-webpack-plugin");
3 | const ESLintWebpackPlugin = require('eslint-webpack-plugin');
4 |
5 | const extensions = ['.js', '.jsx', '.ts', '.tsx'];
6 |
7 | const libConfig = {
8 | name: 'lib',
9 | entry: './src/midori.ts',
10 | output: {
11 | filename: 'midori.cjs',
12 | path: path.resolve(__dirname, 'dist'),
13 | library: {
14 | name: 'midori',
15 | type: 'commonjs',
16 | },
17 | globalObject: 'this',
18 | },
19 | resolve: { extensions },
20 | externals: [
21 | 'three',
22 | /^three\/.+$/,
23 | ],
24 | module: {
25 | rules: [
26 | {
27 | test: /\.(j|t)s$/,
28 | exclude: /node_modules/,
29 | loader: 'babel-loader',
30 | options: {
31 | presets: ['@babel/preset-env', '@babel/preset-typescript'],
32 | plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-transform-runtime'],
33 | }
34 | }
35 | ]
36 | },
37 | plugins: [
38 | new CopyPlugin({ patterns: [{ from: path.resolve(__dirname, 'src/midori.esm.js'), to: path.resolve(__dirname, 'dist/midori.js') }] }),
39 | new ESLintWebpackPlugin({ files: 'src/', extensions, emitWarning: true }),
40 | ],
41 | };
42 |
43 | const docsConfig = {
44 | name: 'docs',
45 | entry: './docs/index.tsx',
46 | output: {
47 | filename: 'dist/index.js',
48 | path: path.resolve(__dirname, 'docs'),
49 | },
50 | resolve: { extensions },
51 | module: {
52 | rules: [
53 | {
54 | test: /\.(j|t)sx$/,
55 | exclude: /node_modules/,
56 | loader: 'babel-loader',
57 | options: {
58 | presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'],
59 | plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-transform-runtime'],
60 | }
61 | },
62 | ]
63 | },
64 | devServer: {
65 | static: {
66 | directory: path.resolve(__dirname, 'docs'),
67 | }
68 | },
69 | plugins: [ new ESLintWebpackPlugin({ files: 'docs/', extensions, emitWarning: true }) ],
70 | };
71 |
72 | module.exports = {
73 | libConfig,
74 | docsConfig,
75 | };
76 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const { libConfig, docsConfig } = require('./webpack.config.js');
3 |
4 | const devConfig = {
5 | mode: 'development',
6 | devtool: 'eval-source-map',
7 | };
8 |
9 | const libDevConfig = merge(libConfig, devConfig);
10 | const docsDevConfig = merge(docsConfig, devConfig);
11 |
12 | module.exports = {
13 | libConfig: libDevConfig,
14 | docsConfig: docsDevConfig,
15 | default: [
16 | libDevConfig,
17 | docsDevConfig,
18 | ]
19 | };
20 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const { libConfig, docsConfig } = require('./webpack.config.js');
3 |
4 | const prodConfig = {
5 | mode: 'production',
6 | devtool: 'source-map',
7 | };
8 |
9 | const libProdConfig = merge(libConfig, prodConfig);
10 | const docsProdConfig = merge(docsConfig, prodConfig);
11 |
12 | module.exports = {
13 | libConfig: libProdConfig,
14 | docsConfig: docsProdConfig,
15 | default: [
16 | libProdConfig,
17 | docsProdConfig,
18 | ]
19 | };
20 |
--------------------------------------------------------------------------------