├── .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 | [![npm version](https://img.shields.io/npm/v/midori-bg.svg)](https://npmjs.org/package/midori-bg "View this project on npm") 3 | 4 |
5 | 6 | 7 | 8 |
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 | 290 | 291 | 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 | --------------------------------------------------------------------------------