├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── photos ├── p1.jpg ├── p2.jpg ├── p3.jpg ├── p4.jpg ├── render1.jpg ├── render2.jpg ├── render3.jpg └── render4.jpg ├── public ├── assets │ ├── audio │ │ └── intro-short.mp3 │ ├── font │ │ └── Domus-Regular.woff │ └── image │ │ ├── mask │ │ ├── a.png │ │ ├── b.png │ │ ├── c.png │ │ ├── d.png │ │ ├── e.png │ │ └── f.png │ │ └── tile │ │ ├── bigdot_1024_.png │ │ ├── bigdot_512_.png │ │ ├── contours_1024_.png │ │ ├── contours_512_.png │ │ ├── funkygerms_1024_.png │ │ ├── funkygerms_512_.png │ │ ├── leppard_1024_.png │ │ ├── leppard_512_.png │ │ ├── littlesticks_1024_.png │ │ ├── littlesticks_512_.png │ │ ├── smalldot_1024_.png │ │ ├── smalldot_512_.png │ │ ├── worms_1024_.png │ │ └── worms_512_.png ├── bundle.js └── index.html └── src ├── assets ├── CSS.js └── svg-shapes.json ├── createArtwork.js ├── geometry ├── Polygon2D.js ├── getBlob.js ├── getPolygon.js ├── getRectangle.js └── getSVGShape.js ├── index.js ├── material └── getShapeMaterial.js ├── object ├── BaseObject.js ├── Shape.js └── ZigZag.js ├── scene ├── MainScene.js ├── ZigZagScene.js └── presets.js ├── shader ├── example-gradient.frag ├── shape.frag ├── shape.vert └── util │ ├── background.glsl │ ├── motion.glsl │ └── surface-gradient.glsl └── util ├── circleIntersectBox.js ├── colliderCircle.js ├── createAudio.js ├── flattenVertices.js ├── getDimensions.js ├── introText.js ├── loadAssets.js ├── pickColors.js ├── polyline.js ├── query.js ├── random.js └── strokePolygon.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | dist/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "semi": true, 6 | "bracketSpacing": true 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Matt DesLauriers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsconfeu-generative-visuals 2 | 3 | The ThreeJS/WebGL and Canvas code for the real-time generative animations shown during JSConfEU 2018 in Berlin. Created by [Matt DesLauriers](http://mattdesl.com/) and [Szymon Kaliski](http://szymonkaliski.com/), based on [Silke Voigts](http://www.silkevoigts.de/)'s designs and mood boards. 4 | 5 | This was used during the opening of the event, as well as during breaks in between talks, and around the edges of speaker slides during talks. The visuals were used in a couple other select places, such as in monitors showing current schedule & speaker tracks. All using Chrome in real-time. 6 | 7 | ## Photos & Video 8 | 9 | For a video of the artwork at JSConfEU, see here: 10 | 11 | - [Vimeo: JSConf EU Generative Visuals](https://vimeo.com/273222929) 12 | 13 | A couple select photos below: 14 | 15 | ![pic1](./photos/p2.jpg) 16 | 17 | ![pic1](./photos/p3.jpg) 18 | 19 | ## Screen Shots 20 | 21 | Taken from in browser: 22 | 23 | ![pic1](./photos/render1.jpg) 24 | 25 | ![pic2](./photos/render2.jpg) 26 | 27 | ![pic2](./photos/render4.jpg) 28 | 29 | ## Live Demo 30 | 31 | To run it live in your browser, try the following links. We have only tested this on desktop Chrome. 32 | 33 | - [Generative Mode](https://mattdesl.github.io/jsconfeu-generative-visuals/public/) 34 | - [Intro Mode](https://mattdesl.github.io/jsconfeu-generative-visuals/public/?intro&autoplay=false) 35 | 36 | In Generative Mode, try pushing 1, 2 or 3 on your keyboard to transition colour palettes and states. 37 | 38 | ## How to Install 39 | 40 | Dependencies: 41 | 42 | - `node@8.10.0` 43 | - `npm@5.7.1` 44 | - `Chrome 66` 45 | 46 | Setup: 47 | 48 | ```sh 49 | git clone https://github.com/mattdesl/jsconfeu-generative-visuals 50 | cd jsconfeu-generative-visuals 51 | npm install 52 | ``` 53 | 54 | Now, you can run in development mode on [http://localhost:9966/](http://localhost:9966/) with the following: 55 | 56 | ```sh 57 | npm run start 58 | ``` 59 | 60 | Or, bundle to a static site in the `public/` folder: 61 | 62 | ```sh 63 | npm run build 64 | ``` 65 | 66 | In our final projection mapping, we ended up building this to a standalone library that was required by a larger framework to control other visualizations on the projection surface. 67 | 68 | ## Implementation Details 69 | 70 | The main projection uses an aspect ratio of 6540x1200px, four projectors connected to a single GeForce GTX 1080 Windows PC running Chrome in full-screen. 71 | 72 | The shapes are triangulated and rendered with WebGL, using vertex shaders to give them organic movement. An algorithm similar to dart throwing is used to spawn shapes in a pleasing composition, and a slow-motion physics engine repels shapes away from the centre screens if they drift too close. WebAudio and FFT analysis to the intro audio affects the dancing and shifting of the shapes. Most features of the artwork — geometry, pattern, scale, colour selection, movement, etc — are randomized with hand-tuned probabilities. 73 | 74 | ## Credits 75 | 76 | This was made possible by the entire JSConfEU team and conference, as well as the support from their sponsors, including Google Chrome. 77 | 78 | The generative visuals were made possible by the following members: 79 | 80 | - Matt DesLauriers – generative art, creative coding, video editing 81 | - Szymon Kaliski – creative coding 82 | - Martin Mostert – cinematography, film footage 83 | - Martin Schuhfuss – lights, projection mapping 84 | - Malte Ubl – curator, JSConf EU 85 | - Silke Voigts – design, brand identity 86 | - Sam Wray (2xAA) – soundtrack 87 | 88 | Also thanks to the rest of the live:js, Nested Loops, JSConf EU and CSSConf EU teams. 89 | 90 | ## License 91 | 92 | MIT, see [LICENSE.md](http://github.com/mattdesl/jsconfeu-generative-visuals/blob/master/LICENSE.md) for details. 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsconfeu-generative-visuals", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./dist/build.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com", 10 | "url": "https://github.com/mattdesl" 11 | }, 12 | "dependencies": { 13 | "analyser-frequency-average": "^1.0.0", 14 | "animejs": "^2.2.0", 15 | "chaikin-smooth": "^1.0.4", 16 | "circular-buffer": "^1.0.2", 17 | "clamp": "^1.0.1", 18 | "convex-hull": "^1.0.3", 19 | "defined": "^1.0.0", 20 | "earcut": "^2.1.3", 21 | "eases": "^1.0.8", 22 | "extrude-polyline": "^1.0.6", 23 | "glsl-aastep": "^1.0.1", 24 | "glsl-noise": "0.0.0", 25 | "glsl-pi": "^1.0.0", 26 | "glsl-random": "0.0.5", 27 | "keycode": "^2.2.0", 28 | "lerp": "^1.0.3", 29 | "load-asset": "^1.2.0", 30 | "lodash.throttle": "^4.1.1", 31 | "new-array": "^1.0.0", 32 | "normalize-path-scale": "^2.0.0", 33 | "polygon-offset": "^0.3.1", 34 | "query-string": "^6.1.0", 35 | "right-now": "^1.0.0", 36 | "seed-random": "^2.2.0", 37 | "simplex-noise": "^2.4.0", 38 | "smoothstep": "^1.0.1", 39 | "svg-mesh-3d": "^1.1.0", 40 | "three": "^0.92.0", 41 | "three-buffer-vertex-data": "^1.1.0", 42 | "three.meshline": "^1.1.0", 43 | "touches": "^1.2.2", 44 | "unlerp": "^1.0.1", 45 | "web-audio-player": "^1.3.3", 46 | "weighted-random": "^0.1.0" 47 | }, 48 | "devDependencies": { 49 | "browserify": "^16.2.2", 50 | "budo": "^11.2.1", 51 | "glslify": "^6.1.1", 52 | "glslify-hex": "^2.1.1", 53 | "surge": "^0.20.1", 54 | "uglify-es": "^3.3.9" 55 | }, 56 | "scripts": { 57 | "test": "node test.js", 58 | "deploy": "npm run build && npm run deploy:upload", 59 | "deploy:upload": "surge -p ./public -d paint-blobs.surge.sh", 60 | "build-all": "npm run build && npm run build:lib", 61 | "build": "browserify ./src/index.js | uglifyjs -cm > public/bundle.js", 62 | "dist": "npm run dist:bundle && npm run dist:min", 63 | "dist:bundle": "mkdir -p dist/ && browserify ./src/createArtwork.js --standalone artwork > dist/build.js", 64 | "dist:min": "uglifyjs ./dist/build.js -mc > dist/build.min.js", 65 | "start": "budo ./src/index.js:bundle.js --dir public/ --live" 66 | }, 67 | "browserify": { 68 | "transform": [ 69 | "glslify" 70 | ] 71 | }, 72 | "glslify": { 73 | "transform": [ 74 | "glslify-hex" 75 | ] 76 | }, 77 | "keywords": [], 78 | "repository": { 79 | "type": "git", 80 | "url": "git://github.com/mattdesl/jsconfeu-generative-visuals.git" 81 | }, 82 | "homepage": "https://github.com/mattdesl/jsconfeu-generative-visuals", 83 | "bugs": { 84 | "url": "https://github.com/mattdesl/jsconfeu-generative-visuals/issues" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /photos/p1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/photos/p1.jpg -------------------------------------------------------------------------------- /photos/p2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/photos/p2.jpg -------------------------------------------------------------------------------- /photos/p3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/photos/p3.jpg -------------------------------------------------------------------------------- /photos/p4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/photos/p4.jpg -------------------------------------------------------------------------------- /photos/render1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/photos/render1.jpg -------------------------------------------------------------------------------- /photos/render2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/photos/render2.jpg -------------------------------------------------------------------------------- /photos/render3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/photos/render3.jpg -------------------------------------------------------------------------------- /photos/render4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/photos/render4.jpg -------------------------------------------------------------------------------- /public/assets/audio/intro-short.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/audio/intro-short.mp3 -------------------------------------------------------------------------------- /public/assets/font/Domus-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/font/Domus-Regular.woff -------------------------------------------------------------------------------- /public/assets/image/mask/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/mask/a.png -------------------------------------------------------------------------------- /public/assets/image/mask/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/mask/b.png -------------------------------------------------------------------------------- /public/assets/image/mask/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/mask/c.png -------------------------------------------------------------------------------- /public/assets/image/mask/d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/mask/d.png -------------------------------------------------------------------------------- /public/assets/image/mask/e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/mask/e.png -------------------------------------------------------------------------------- /public/assets/image/mask/f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/mask/f.png -------------------------------------------------------------------------------- /public/assets/image/tile/bigdot_1024_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/bigdot_1024_.png -------------------------------------------------------------------------------- /public/assets/image/tile/bigdot_512_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/bigdot_512_.png -------------------------------------------------------------------------------- /public/assets/image/tile/contours_1024_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/contours_1024_.png -------------------------------------------------------------------------------- /public/assets/image/tile/contours_512_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/contours_512_.png -------------------------------------------------------------------------------- /public/assets/image/tile/funkygerms_1024_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/funkygerms_1024_.png -------------------------------------------------------------------------------- /public/assets/image/tile/funkygerms_512_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/funkygerms_512_.png -------------------------------------------------------------------------------- /public/assets/image/tile/leppard_1024_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/leppard_1024_.png -------------------------------------------------------------------------------- /public/assets/image/tile/leppard_512_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/leppard_512_.png -------------------------------------------------------------------------------- /public/assets/image/tile/littlesticks_1024_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/littlesticks_1024_.png -------------------------------------------------------------------------------- /public/assets/image/tile/littlesticks_512_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/littlesticks_512_.png -------------------------------------------------------------------------------- /public/assets/image/tile/smalldot_1024_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/smalldot_1024_.png -------------------------------------------------------------------------------- /public/assets/image/tile/smalldot_512_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/smalldot_512_.png -------------------------------------------------------------------------------- /public/assets/image/tile/worms_1024_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/worms_1024_.png -------------------------------------------------------------------------------- /public/assets/image/tile/worms_512_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdesl/jsconfeu-generative-visuals/1176280b8c511864ad357c234ff52eb3fc6ae67b/public/assets/image/tile/worms_512_.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jsconfeu-generative-visuals 8 | 9 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/assets/CSS.js: -------------------------------------------------------------------------------- 1 | // As a JS module for inter-play with the larger framework that is used 2 | // during projection mapping. 3 | module.exports = ` 4 | @font-face { 5 | font-family: 'Domus'; 6 | src: url(assets/font/Domus-Regular.woff); 7 | font-weight: 500; 8 | } 9 | 10 | body { 11 | overflow: hidden; 12 | background: #303f62; 13 | height: 100%; 14 | width: 100%; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .canvas-container { 21 | background: #303f62; 22 | } 23 | 24 | html { 25 | font-family: 'Domus', Helvetica, sans-serif; 26 | height: 100%; 27 | width: 100%; 28 | } 29 | 30 | .canvas-text-container { 31 | position: absolute; 32 | color: white; 33 | top: 0.25vmax; 34 | left: -0.75vmax; 35 | width: 100%; 36 | pointer-events: none; 37 | height: 100%; 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | } 42 | 43 | .canvas-text { 44 | font-size: 1.0vmax; 45 | text-align: center; 46 | 47 | user-select: none; 48 | position: absolute; 49 | } 50 | 51 | 52 | .canvas-big-text:not(:empty) + .canvas-text { 53 | top: calc(50% - 1.75vmax); 54 | } 55 | 56 | .canvas-big-text { 57 | font-size: 2.5vmax; 58 | text-align: center; 59 | width: 27vw; 60 | user-select: none; 61 | position: absolute; 62 | top: calc(50% - 1.0vmax); 63 | } 64 | 65 | canvas { 66 | /* Useful during projection mapping... */ 67 | /*cursor: none;*/ 68 | } 69 | 70 | .canvas-text p { 71 | margin: 0; 72 | line-height: 1.5; 73 | } 74 | 75 | .text-chunk { 76 | display: inline-block; 77 | position: relative; 78 | margin-right: 0.15vmax; 79 | margin-left: 0.15vmax; 80 | } 81 | `; 82 | -------------------------------------------------------------------------------- /src/assets/svg-shapes.json: -------------------------------------------------------------------------------- 1 | { 2 | "heart": 3 | "M186.476838,-60.2535768 C188.396714,-60.2025608 190.329589,-60.2725828 192.218467,-60.6146901 C201.886841,-62.3682405 214.230042,-57.7747988 224.946348,-54.1256535 C227.010214,-53.4224328 229.147076,-52.9742921 231.309936,-52.6952046 C245.935989,-50.8096127 252.523562,-43.7103846 262.036946,-37.339385 C262.675905,-36.912251 263.313864,-36.4601091 263.920824,-35.9879609 C276.406016,-26.2709111 283.115582,-21.5744371 291.17406,-13.0337565 C291.849016,-12.318532 292.473976,-11.5662959 293.074937,-10.789052 C298.973555,-3.1406515 306.014099,5.03191352 311.578739,14.0237357 C313.468616,17.0776942 314.707536,20.476761 315.461487,23.987863 C316.579415,29.1974981 318.548288,33.775935 320.660151,38.2133277 C326.657763,50.8182839 340.349876,57.9295158 354.079987,55.4407347 C357.44077,54.8315435 360.779553,54.3003768 364.118337,54.1083165 C366.519182,53.9692729 368.923026,54.1793388 371.290873,54.600471 C383.406088,56.7591485 395.108331,57.2252948 406.435597,60.8324269 C416.965916,63.7763509 429.305117,69.7802352 439.793438,76.247265 C440.639383,76.7694289 441.508327,77.2505799 442.398269,77.6947193 C456.934328,84.9569986 460.872073,90.3406883 467.042674,97.4309136 C468.726565,99.3665211 470.586444,101.163085 472.689308,102.632546 C480.834781,108.325333 486.09944,118.718595 491.168112,128.457652 C492.335036,130.699355 493.300973,133.050093 493.911934,135.502863 C496.525765,146.012161 503.869289,156.87257 507.078082,168.515224 C507.343064,169.475525 507.673043,170.41282 508.056018,171.333108 C510.970829,178.338307 511.778777,195.414667 511.384803,207.713527 C511.333806,209.292022 511.394802,210.874519 511.58679,212.442011 C513.495666,228.05091 508.378997,236.065425 504.660238,247.225928 C504.431253,247.914144 504.179269,248.607361 503.903287,249.277572 C499.017603,261.147297 495.295844,271.566567 488.992252,281.632727 C488.404291,282.572021 487.747333,283.471304 487.057378,284.338576 C478.324943,295.316021 473.42726,302.210185 465.918746,309.527482 C464.024869,311.373061 461.881008,312.942553 459.582157,314.247963 C447.281953,321.239157 440.947363,329.099625 430.18806,334.934456 C429.40811,335.356588 428.64516,335.805729 427.897208,336.279878 C417.788862,342.673885 404.001755,346.566106 390.386637,349.677083 C388.985727,349.997183 387.619816,350.443323 386.279902,350.960486 C371.524858,356.656273 354.245976,357.842646 336.946096,361.494792 C335.397197,361.821895 333.873295,362.271036 332.394391,362.834212 C308.908912,371.772018 295.319792,370.922751 276.247026,375.184088 C257.869216,378.130013 238.223488,383.506701 220.176657,387.621992 C219.117725,387.863068 218.073793,388.164162 217.043859,388.513272 C196.114214,395.614501 182.90407,396.15367 168.097028,399.350673 C167.413073,399.49872 166.716118,399.62576 166.024163,399.724791 C153.994941,401.451333 142.717672,403.817075 133.001301,406.139804 C115.587428,410.30211 98.2725491,398.930541 95.183749,381.288004 C93.6618476,372.593275 91.9219602,362.714175 89.6751057,352.227883 C89.3271282,350.602373 88.8741575,348.994869 88.2691967,347.446383 C82.4925707,332.658742 81.6426257,315.356311 77.6908816,297.66876 C73.128177,272.496859 69.6893996,261.352362 65.7076574,244.120953 C65.158693,241.743207 64.9147088,239.302441 64.9287079,236.862675 C65.0397007,217.625638 57.0302192,202.621929 52.6655018,186.199774 C52.0565412,183.912056 51.7405617,181.549315 51.6715662,179.182572 C51.0336075,157.313708 43.9120685,145.490998 40.1403127,130.718361 C39.8053344,129.404949 39.596348,128.065529 39.4443578,126.719106 C37.9784527,113.824059 34.792659,98.5862762 35.0106449,85.1450576 C35.7245986,67.1224011 37.0045158,58.7367692 40.0853163,47.5532591 C40.5852839,45.7376893 40.893264,43.879106 41.0992507,42.0065183 C42.2161783,31.8663357 48.7557549,21.697144 54.5433802,12.4372377 C55.8262972,10.3845935 56.8552306,8.18590342 57.6331802,5.89218352 C60.9149677,-3.77184961 70.7263325,-11.735349 79.0157958,-19.25771 C80.0927261,-20.236017 81.1186597,-21.268341 82.0425998,-22.3916936 C91.1270117,-33.4441625 100.296418,-37.2823671 110.682746,-42.4970038 C112.031658,-43.1742163 113.440567,-43.7203877 114.875474,-44.1875344 C127.505656,-48.302826 137.588004,-54.0236215 150.31418,-56.6734532 C165.356206,-59.2992773 175.010581,-60.5546713 186.476838,-60.2535768 Z", 4 | 5 | "feather": 6 | "M117.1,48.7 C125.062463,39.7461157 133.832855,31.5447949 143.3,24.2 C152.8,17 164.9,7.2 174.7,6.7 C185.9,2.6 199.5,-2.3 209.7,1.2 C219.2,1.8 226.8,3.7 236.8,7.7 C246.8,11.7 255.4,12.3 261.1,21.9 C268.7,27.7 276.9,34.2 282.6,41.4 C288.3,48.6 296.9,61.3 301.4,63.5 C308.3,72.3 311.2,83.6 314.9,93.4 C314.6,101.5 308.5,107.1 306.7,116 C300.6,127 293.4,135.9 288.5,148.8 C282.4,159.8 282,169.7 270.3,181.6 C268.3,185.2 269,189 266.7,193.2 C267.9,205.3 287.9,205.1 296.7,200 C307,195.2 315.5,188.8 325.5,181.6 C335.5,174.4 337.2,166.7 348.7,156.6 C359.1,162.7 359.2,176.8 360,189.1 C359.2,198.4 355.7,208.7 353.9,216.3 C352.1,223.9 348.5,232.9 341.4,241.2 C336.2,249 328.5,256 323.3,262.5 C316.5,268.8 309.1,275.4 300.6,278.7 C294.3,282.1 290.4,288.3 281.1,288.7 C274.8,292.3 264.1,293.2 265.1,302.5 C260.3,309.5 258.1,314.5 260.8,326.7 C262.5,334.9 266.4,340.1 274.3,347.2 C282.2,354.3 296.1,353.5 304.5,350.7 C314.9,348 321.2,342.6 332.7,334.5 C340.5,327.2 348.5,318.3 353.8,310.2 C359.1,302.1 365.3,293.3 375.5,286.5 C378.1,295.1 378.5,306 379.5,312.8 C379.7,321.7 379.5,329.1 376.6,339.2 C373.7,349.3 374,358.1 367.4,364.3 C363.4,372.3 358,378.9 352.7,386.6 C347.4,394.3 339.2,402 333.3,404.7 C326.1,409.9 314.8,411.8 309.8,417.2 C298,421.4 284.6,428.7 272.9,427.2 C260.7,430.6 247.6,437.1 240.1,443.7 C232.6,450.3 230.1,453.5 227,465 C224.5,473 220.7,479.6 225.7,489.9 C227.5,498.1 235.1,504.2 237.1,512.1 C242.7,518.4 250.9,522.7 257.8,526.1 C266.5,529.1 273.4,529.9 284.8,528.1 C293.9,526.7 301.7,524.8 310.9,518.9 C319,514.5 329.3,510.1 333.2,502.9 C340.2,496.8 346.2,491 353.3,483.7 C354.2,490.4 353.7,497.7 353.8,503.7 C353.9,509.7 348,514 350,523.3 C348.003318,529.70596 345.460659,535.928784 342.4,541.9 C339.3,547.9 333.9,554.9 331.6,559.4 C327.540345,564.934278 323.024581,570.119044 318.1,574.9 C313.3,579.8 309.9,585.1 302.8,588.5 C295.2,594.4 287.8,601.3 278.5,604.2 C269.9,608.7 261.9,612.5 252,615 C242.1,617.5 233.7,620.8 223.8,621.8 C214.345814,623.315332 204.771555,623.951384 195.2,623.7 C185.6,623.6 173.5,621.8 166.8,621.1 C157.4,619.4 149.8,614.5 138.8,613.7 C131.9,611.3 122.1,610.4 118.8,604.8 C112.4,601.4 105,596.6 100,593.6 C95,590.6 87.6,587.4 83,580.1 C77.6,575.3 70.9,571.5 67.4,564.4 C62.5101046,559.024144 58.0936908,553.235966 54.2,547.1 C50.1,541.1 43.7,534.5 42.9,528.2 C40.2,522.8 35,507.8 30.6,500.4 C27.5,490 27.4,481.1 38,481.8 C44.4,482.2 46.3,489 60.4,496.6 C68.6,504 75.8,514.5 85.1,513.9 C93.8,516.2 99.8,514.5 111.9,512.4 C120.2,509.1 128,504.1 130,494.7 C132.5,487.8 134.3,482.2 129.4,473.4 C126.9,466.5 120.7,462.6 117.5,454.5 C112.8,449.6 106.2,443.4 101.9,441.3 C96.3,437.3 88.1,432.2 84.6,430.6 C78.6,427.3 71.6,420.4 66.4,420.9 C60.4,417.7 53,418.5 48.2,411.2 C42.4,407.7 34.3,405.3 31.4,399.6 C26.2,395.3 18.7,391.7 17.5,385.4 C13.2,379.8 13,374.2 7.2,367.3 C4.7,360.9 1.6,355.4 1.8,347.4 C2,339.4 -0.7,331.7 0.4,326.3 C0.4,319.1 4,311.8 1.9,304.6 C2.8,297.3 3.9,291.5 5.4,282.8 C6.9,275.6 5.3,268.2 10.1,261.5 C13.9535163,266.998008 18.1978185,272.211482 22.8,277.1 C27.4,282 30.1,285.9 37.6,290.7 C42.8,294.9 47.1,300.1 54.1,302.1 C61.1,304.1 66.2,307.3 72.2,310.9 C78.2,314.5 87.6,316.4 94.7,314.1 C105.3,311.4 116.9,300.3 113.7,289.8 C114.812037,278.737533 112.39687,267.606761 106.8,258 C104,251.8 103.9,245.1 97.2,239.8 C93.6,234 91.7,230.5 85.3,223 C81,217.6 81.7,212.7 71.4,207.7 C66.5,203 62.4,196 55.7,194.4 C46.5,187.4 34.3,181.1 36.1,167.6 C36.5,162.5 38.6,160.1 42.1,153.6 C45.6,147.1 44.9,145.9 53.4,135.9 C57.2,130 64.7,119.8 64.7,118.2 C73.4,125.4 88.9,133.7 90.8,139.9 C99.6,147.1 105.7,156.8 117,161.6 C123.2,166.8 137.1,175.4 138.6,170.4 C146.2,168.6 149.9,166 150.4,152.2 C150.1,144.3 147.1,139.5 142,130.5 C136.9,121.5 125.4,109.6 120.2,101.5 C112.2,92.4 105.7,81.1 94.1,76.3 C101.5,66.9 111.1,55.7 117.1,48.7 Z", 7 | 8 | "lightning": 9 | "M0.4,178.2 C0.8,179.5 -0.9,176.1 0.7,174 C0.5,175.2 2.2,171.7 1,169.2 C2.9,168.4 1,164.1 1.4,162.7 C0.4,159.6 2.4,159.1 1.8,155 C1.2,150.9 1.3,149.1 2.5,146.4 C2.04278224,143.29234 2.2472464,140.123146 3.1,137.1 C4.4,133.8 6.8,129 3.7,127 C4.9,123.8 6,121.8 4.3,116.8 C5,115.2 5.1,110.7 5,106.7 C4.9,102.7 7.6,100.1 5.5,96.9 C3.4,93.7 4.9,92.6 6.2,87.4 C7.5,82.2 8.2,79.7 6.7,78.8 C5.2,77.9 7.4,73.9 7.3,71 C7.2,68.1 9,67.4 7.7,64.6 C8.7,61 6.3,62.2 8,59.8 C9.7,57.4 6.7,56.3 8.2,55.6 C8.4,53 9.4,50.4 12.1,49.9 C14.3,47.5 17.9,49.1 18.9,50.4 C18.3,51 20.9,52 24.9,54.6 C24.7,55.7 29.4,56 31.1,59 C32.8,62 35.1,63.1 38.2,63.9 C40.7981117,65.2110459 43.19017,66.895594 45.3,68.9 C46.4,71.7 49.1,71.9 51.5,73.2 C55.8,77.3 57.8,78.8 57.5,77.4 C57.2,76 60,80.1 63.4,78.3 C61.3,74.2 68,77.3 67.7,74.3 C69.2,72.6 68.5,72.4 69.7,69.3 C69.2086223,68.2584479 69.177948,67.0582577 69.6154791,65.9929646 C70.0530102,64.9276715 70.9184011,64.0955082 72,63.7 C74.4,61.7 73.3,58.4 74.8,56.6 C73.6,52.7 79.8,51.1 78.1,48.6 C79.2,45.6 81.6,46 81.5,40.1 C82.8,35.7 80.7,34.8 84.9,31.7 C86.2,28.6 87.3,24.7 88.1,23.7 C88.3,21 90,18.5 90.9,16.6 C91.8,14.7 92.9,12.9 93.2,11 C95.1,8.3 95.5,5.5 95.2,5.9 C95.6,2.9 95.7,1.4 98.5,2.3 C98.8,1 100.8,0.4 102.9,1.8 C105,3.2 105.6,4 106.8,4.1 C108,4.2 107.7,6.5 108.3,8.8 C105.6,7.8 108.5,9.3 108.1,12.3 C108.46532,13.6874147 108.395503,15.1535769 107.9,16.5 C108.3,17.7 106.4,18.7 107.7,21.9 C107.2,24.1 106,28.2 107.3,28.6 C106.1,30.9 105.7,34.8 107,36.1 C106.250855,38.8365607 106.11428,41.7046362 106.6,44.5 C108.5,45.8 105.3,48.8 106.2,53.3 C107.1,57.8 105.1,59.8 105.7,62.6 C105.7,67.6 106.9,70.2 105.3,72.1 C103.7,74 105.7,76.9 104.8,81.6 C107,84.6 104.6,88.7 104.4,90.8 C104.2,92.9 102,97.7 104,99.7 C106,101.7 103.5,107.9 103.6,108 C105.1,113.2 102.3,114.3 103.2,115.6 C102.4,117.4 100.9,123 102.9,122.2 C104.2,127.2 101.9,127.9 102.7,127.7 C100.8,128.6 102.5,131.8 102.5,131.9 C102.5,132 100.8,133.4 102.3,135.4 C101.3,135.6 100.5,138.7 98.7,141.1 C94.8,142.4 93.7,141.9 91.9,140.8 C93.7,136.8 91.2,137.5 90.1,139.7 C89,141.9 86.7,137.7 85.3,136.7 C86.7,134.7 82.7,138 78.6,132.4 C74.5,130.7 74.6,126.8 70.8,127.5 C71,124.3 68,125.9 63,122.7 C62,122.8 60.1,118.3 56.3,118.3 C53.8,116.2 50.9,116.2 51.5,115.3 C50,117.7 47.7,114.9 49.7,114.2 C49.5,118 45.9,112.7 44,113.5 C44,115.5 40.2,118.2 39.8,117.3 C42.3,119.8 40,119.2 37.8,122.1 C35.8,127.6 35.7,124.1 35.6,127.3 C34.6,132.1 33.3,131.7 33,133.9 C33.1,138.1 29.6,140 30,141.3 C28.4,145.1 28.2,147.4 26.6,149.3 C25,151.2 23.5,154.5 23.3,157.2 C23.1,159.9 19.9,161.8 20.3,164.6 C22.2,165.7 18.4,170.4 17.5,171.2 C17.2,175.1 15.4,174.9 15.4,176.5 C14.9,180.1 12.3,181.7 13.5,181.2 C12.9,182.2 10.2,185.2 10.1,184.6 C7.3,184 7.3,185.7 5.7,185.2 C4.1,184.7 2.5,184 1.9,182.9 C1.3,181.8 0.7,178.9 0.4,178.2 Z" 10 | } 11 | -------------------------------------------------------------------------------- /src/createArtwork.js: -------------------------------------------------------------------------------- 1 | global.THREE = require('three'); 2 | 3 | const rightNow = require('right-now'); 4 | const defined = require('defined'); 5 | const loadAssets = require('./util/loadAssets'); 6 | const MainScene = require('./scene/MainScene'); 7 | const anime = require('animejs'); 8 | const RND = require('./util/random'); 9 | const ZigZagScene = require('./scene/ZigZagScene'); 10 | const tmpVec3 = new THREE.Vector3(); 11 | const presets = require('./scene/presets'); 12 | const startIntroText = require('./util/introText'); 13 | const query = require('./util/query'); 14 | const createAudio = require('./util/createAudio'); 15 | const noop = () => {}; 16 | const CSS = require('./assets/CSS.js'); 17 | 18 | module.exports = createArtwork; 19 | 20 | function createArtwork(canvas, params = {}) { 21 | // I've been designing my code to this aspect ratio 22 | // Since it's assumed it will be the one we use 23 | const designAspect = 6540 / 1200; 24 | 25 | // But I've also been testing some other target ratios 26 | // in case the actual display is not what we have above for some reason 27 | // const targetAspect = designAspect 28 | const targetAspect = designAspect; 29 | // const targetAspect = 366 / 89 30 | // const targetAspect = 1416 / 334 31 | 32 | // You can also test full screen, it will give a different look... 33 | const useFullscreen = defined(params.fullscreen, query.fullscreen, false); 34 | const autoplay = defined(params.autoplay, query.autoplay, false); 35 | 36 | const renderer = new THREE.WebGLRenderer({ antialias: true, canvas }); 37 | renderer.sortObjects = false; 38 | 39 | const background = new THREE.Color('white'); 40 | renderer.setClearColor(background, 1); 41 | 42 | const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, -100, 100); 43 | const scene = new THREE.Scene(); 44 | 45 | const app = { 46 | camera, 47 | scene, 48 | canvas, 49 | onFinishIntro: noop, 50 | intro: false, 51 | audio: createAudio(), 52 | audioSignal: [0, 0, 0], 53 | sceneBounds: new THREE.Box2(), 54 | unitScale: new THREE.Vector2(1, 1), 55 | // Holds props for visuals 56 | preset: {} 57 | }; 58 | 59 | const tickFPS = 30; 60 | 61 | let raf; 62 | let tickFrame = 0; 63 | let lastTickTime = 0; 64 | let elapsedTime = 0; 65 | let previousTime = rightNow(); 66 | let running = false; 67 | let hasInit = false; 68 | let hasResized = false; 69 | let stoppedAnimations = []; 70 | let backgroundAnimation; 71 | let styleEl; 72 | 73 | // scene.backgroundValue = app.colorPalette.background; 74 | // scene.background = new THREE.Color(scene.backgroundValue); 75 | 76 | const isIntroDefault = false; 77 | const isInitiallyIntro = defined(params.intro, query.intro, isIntroDefault); 78 | const defaultPreset = isInitiallyIntro ? 'intro0' : 'default'; 79 | const initialPresetKey = defined(params.preset, query.preset, defaultPreset); 80 | 81 | canvas.style.visibility = 'hidden'; 82 | setPreset(initialPresetKey); 83 | draw(); 84 | 85 | const api = { 86 | resize, 87 | draw, 88 | isRunning() { 89 | return running; 90 | }, 91 | onFadeOutIntro: () => { 92 | traverse('onTrigger', 'fadeOut'); 93 | transitionBackground(presets.intro0.background, { 94 | easing: 'linear', 95 | duration: 2000 96 | }); 97 | app.audio.fadeOut(() => { 98 | app.onFinishIntro(); 99 | }); 100 | }, 101 | load() { 102 | return loadAssets({ renderer }).then(assets => { 103 | canvas.style.visibility = ''; 104 | app.assets = assets; 105 | console.log('[canvas] Loaded assets'); 106 | return assets; 107 | }); 108 | }, 109 | setPreset, 110 | transitionToPreset, 111 | start(opt = {}) { 112 | if (!app.assets) { 113 | console.error('[canvas] Assets have not yet been loaded, must await load() before start()'); 114 | } 115 | if (!hasResized) { 116 | console.error('[canvas] You must call artwork.resize() at least once before artwork.start()'); 117 | } 118 | 119 | let needsStart = false; 120 | 121 | // here we have bunch of code that we are repeating from other places, 122 | // just so we don't need to run background transition on start if we want 123 | // // different mode 124 | // app.mode = opt.mode; 125 | 126 | // if (app.mode === 'default') { 127 | // app.colorPalette = colorPalettes.light; 128 | // } else if (app.mode === 'ambient') { 129 | // app.colorPalette = colorPalettes.ambient; 130 | // } 131 | 132 | // scene.backgroundValue = app.colorPalette.background; 133 | // scene.background = new THREE.Color(scene.backgroundValue); 134 | // repeated code ends here 135 | 136 | const introMode = defined(opt.intro, isIntroDefault); 137 | app.intro = introMode; 138 | 139 | if (!hasInit) { 140 | needsStart = true; 141 | createScene(scene); 142 | hasInit = true; 143 | } 144 | 145 | const runStart = () => { 146 | resume(); 147 | if (needsStart) { 148 | traverse('onTrigger', 'start', opt); 149 | } 150 | if (introMode) { 151 | setPreset('intro0'); 152 | setBackground(presets.intro0.background); 153 | // transitionBackground(presets.intro0.background, { 154 | // easing: 'linear', 155 | // duration: 2000 156 | // }); 157 | startIntroSequence({ 158 | delay: 3000 159 | }); 160 | } 161 | }; 162 | 163 | const setupCSS = () => { 164 | const style = document.createElement('style'); 165 | style.type = 'text/css'; 166 | style.innerHTML = CSS; 167 | 168 | document.getElementsByTagName('head')[0].appendChild(style); 169 | 170 | return style; 171 | }; 172 | 173 | styleEl = setupCSS(); 174 | if (introMode) { 175 | setBackground(presets.intro0.background); 176 | draw(); 177 | if (autoplay) { 178 | runStart(); 179 | } else { 180 | setupIntroClick(runStart); 181 | } 182 | } else { 183 | runStart(); 184 | } 185 | 186 | draw(); 187 | }, 188 | getPresets: () => presets, 189 | triggerIntroSwap(ev) { 190 | traverse('onTrigger', 'introSwap', ev); 191 | }, 192 | clear, 193 | reset, 194 | stop, 195 | bounce, 196 | randomize() { 197 | traverse('onTrigger', 'randomize'); 198 | }, 199 | swapPalettes() { 200 | const newPalette = app.colorPalette === colorPalettes.light ? colorPalettes.dark : colorPalettes.light; 201 | app.colorPalette = newPalette; 202 | updatePalette(); 203 | traverse('onTrigger', 'palette'); 204 | }, 205 | hide() { 206 | // canvas.style.visibility = 'hidden'; 207 | }, 208 | show() { 209 | // canvas.style.visibility = ''; 210 | }, 211 | // set to match text position to be repelled 212 | setTextPosition(x, y, radius = 1) { 213 | traverse('onTrigger', 'colliderPosition', { x, y, radius }); 214 | } 215 | }; 216 | 217 | // so we can `api.setPreset('ambient')` from devtools 218 | window.api = api; 219 | 220 | return api; 221 | 222 | function setupIntroClick(cb) { 223 | const text = document.querySelector('.canvas-text'); 224 | if (text) text.textContent = 'Click to play'; 225 | 226 | const done = () => { 227 | if (text) text.textContent = ''; 228 | window.removeEventListener('click', done); 229 | window.removeEventListener('touchend', done); 230 | cb(); 231 | }; 232 | window.addEventListener('click', done); 233 | window.addEventListener('touchend', done); 234 | } 235 | 236 | function startIntroSequence(opts = {}) { 237 | app.audio.play(); 238 | app.audio.fadeIn(); 239 | startIntroText(api, opts); 240 | } 241 | 242 | function setBackground(color) { 243 | background.set(color); 244 | renderer.setClearColor(background, 1); 245 | } 246 | 247 | function setPreset(key) { 248 | const newPreset = presets[key] || presets.default; 249 | const oldPreset = Object.assign({}, app.preset); 250 | app.preset = Object.assign({}, newPreset); 251 | app.presetKey = key; 252 | app.intro = /intro/i.test(app.preset.mode); 253 | setBackground(app.preset.background); 254 | traverse('onPresetChanged', app.preset, oldPreset); 255 | } 256 | 257 | function transitionBackground(color, opt = {}) { 258 | if (backgroundAnimation) backgroundAnimation.pause(); 259 | const oldColor = background.clone(); 260 | const newColor = new THREE.Color().set(color); 261 | const tmpColor = new THREE.Color(); 262 | const tween = { value: 0 }; 263 | backgroundAnimation = anime({ 264 | targets: tween, 265 | value: 1, 266 | duration: defined(opt.duration, 5000), 267 | easing: defined(opt.easing, [0.12, 0.93, 0.12, 0.93]), 268 | update: () => { 269 | tmpColor.copy(oldColor).lerp(newColor, tween.value); 270 | setBackground(tmpColor); 271 | } 272 | }); 273 | } 274 | 275 | function transitionToPreset(key) { 276 | const newPreset = presets[key] || presets.default; 277 | const oldPreset = Object.assign({}, app.preset); 278 | app.preset = Object.assign({}, newPreset); 279 | app.presetKey = key; 280 | app.intro = /intro/i.test(app.preset.mode); 281 | transitionBackground(app.preset.background); 282 | traverse('onPresetTransition', app.preset, oldPreset); 283 | } 284 | 285 | function resize(width, height, pixelRatio) { 286 | // if (query.test) { 287 | width = defined(width, window.innerWidth); 288 | height = useFullscreen ? window.innerHeight : Math.floor(width / targetAspect); 289 | // } else if (useFullscreen) { 290 | // // width = 291 | // width = 6540; 292 | // height = window.innerHeight; 293 | // } else { 294 | 295 | // } 296 | 297 | pixelRatio = defined(pixelRatio, window.devicePixelRatio); 298 | 299 | if (renderer.getPixelRatio() !== pixelRatio) renderer.setPixelRatio(pixelRatio); 300 | renderer.setSize(width, height); 301 | const el = document.querySelector('.canvas-text-container'); 302 | el.style.width = `${width}px`; 303 | 304 | const aspect = width / height; 305 | camera.scale.x = aspect; 306 | camera.scale.y = 1; 307 | 308 | app.targetScale = aspect / designAspect; 309 | app.unitScale.x = aspect; 310 | 311 | camera.updateProjectionMatrix(); 312 | camera.updateMatrix(); 313 | camera.updateMatrixWorld(); 314 | app.width = width; 315 | app.height = height; 316 | app.pixelRatio = pixelRatio; 317 | app.aspect = aspect; 318 | 319 | app.sceneBounds.min.set(-1, -1); 320 | app.sceneBounds.max.set(1, 1); 321 | // project clip space into real world space 322 | tmpVec3.set(app.sceneBounds.min.x, app.sceneBounds.min.y, 0).unproject(camera); 323 | app.sceneBounds.min.copy(tmpVec3); 324 | tmpVec3.set(app.sceneBounds.max.x, app.sceneBounds.max.y, 0).unproject(camera); 325 | app.sceneBounds.max.copy(tmpVec3); 326 | 327 | hasResized = true; 328 | draw(); 329 | } 330 | 331 | function bounce() { 332 | const scale = { value: scene.scale.x }; 333 | anime({ 334 | targets: scale, 335 | easing: 'easeInQuad', 336 | value: 0.9, 337 | duration: 500, 338 | update: () => { 339 | scene.scale.setScalar(scale.value); 340 | }, 341 | complete: () => { 342 | anime({ 343 | duration: 500, 344 | targets: scale, 345 | easing: 'easeOutQuad', 346 | value: 1, 347 | update: () => { 348 | scene.scale.setScalar(scale.value); 349 | } 350 | }); 351 | } 352 | }); 353 | } 354 | 355 | function clear() { 356 | // stop all animations, clear shapes 357 | stoppedAnimations.length = 0; 358 | anime.running.forEach(a => a.pause()); 359 | anime.running.length = 0; 360 | traverse('onTrigger', 'clear'); 361 | } 362 | 363 | function reset() { 364 | // clear all animations and shapes and re-run loop 365 | clear(); 366 | resetRandomSeed(); 367 | } 368 | 369 | function resetRandomSeed() { 370 | RND.setSeed(RND.getRandomSeed()); 371 | } 372 | 373 | function resume() { 374 | if (running) return; 375 | stoppedAnimations.forEach(anim => anim.play()); 376 | stoppedAnimations.length = 0; 377 | running = true; 378 | previousTime = rightNow(); 379 | raf = window.requestAnimationFrame(animate); 380 | } 381 | 382 | function stop() { 383 | if (!running) return; 384 | stoppedAnimations = anime.running.slice(); 385 | stoppedAnimations.forEach(r => r.pause()); 386 | anime.running.length = 0; 387 | running = false; 388 | window.cancelAnimationFrame(raf); 389 | } 390 | 391 | function animate() { 392 | raf = window.requestAnimationFrame(animate); 393 | 394 | if (app.audio.playing) { 395 | app.audioSignal = app.audio.updateFrequencies(); 396 | } 397 | 398 | const now = rightNow(); 399 | const deltaTime = (now - previousTime) / 1000; 400 | elapsedTime += deltaTime; 401 | previousTime = now; 402 | 403 | render(elapsedTime, deltaTime); 404 | } 405 | 406 | function draw() { 407 | render(elapsedTime, 0); 408 | } 409 | 410 | function render(time, deltaTime) { 411 | const frameInterval = 1 / tickFPS; 412 | const deltaSinceTick = time - lastTickTime; 413 | if (deltaSinceTick > frameInterval) { 414 | lastTickTime = time - (deltaSinceTick % frameInterval); 415 | traverse('frame', tickFrame++, time); 416 | } 417 | 418 | traverse('update', time, deltaTime); 419 | renderer.render(scene, camera); 420 | } 421 | 422 | function traverse(fn, ...args) { 423 | scene.traverse(t => { 424 | if (typeof t[fn] === 'function') { 425 | t[fn](...args); 426 | } 427 | }); 428 | } 429 | 430 | function createScene(scene) { 431 | // temporarily disabled, it can be a bit distracting in generative/default mode 432 | scene.add(new ZigZagScene(app)); 433 | scene.add(new MainScene(app)); 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /src/geometry/Polygon2D.js: -------------------------------------------------------------------------------- 1 | const earcut = require('earcut'); 2 | const flatten = require('../util/flattenVertices'); 3 | const RND = require('../util/random'); 4 | const buffer = require('three-buffer-vertex-data'); 5 | const unlerp = require('unlerp'); 6 | 7 | // Given a set of 2D or 3D vectors, will triangulate them as a closed polygon 8 | // This is 'sorta' fast, but probably better to do per-frame motion in vertex shader, 9 | // and have occasional re-triangulation as new shapes are about to animate in. 10 | module.exports = class Polygon extends THREE.BufferGeometry { 11 | constructor(points) { 12 | super(); 13 | this.boundingBox2 = new THREE.Box2(); 14 | if (points) this.setPoints(points); 15 | } 16 | 17 | updateUVs(points) { 18 | const box = this.boundingBox2; 19 | box.makeEmpty(); 20 | box.setFromPoints(points); 21 | 22 | const width = box.max.x - box.min.x; 23 | const height = box.max.y - box.min.y; 24 | const uvs = points.map(p => { 25 | return [ 26 | width === 0 ? 0 : unlerp(box.min.x, box.max.x, p.x), 27 | height === 0 ? 0 : unlerp(box.min.y, box.max.y, p.y) 28 | ]; 29 | }); 30 | 31 | buffer.attr(this, 'uv', uvs, 2); 32 | } 33 | 34 | updateRandoms(points) { 35 | const randoms = points.map((p, i, list) => RND.randomFloat(0, 1)); 36 | buffer.attr(this, 'random', randoms, 1); 37 | } 38 | 39 | // Pass an already-triangulated polygon 40 | setComplex(points, cells) { 41 | if (points.length > 0 && !points[0].toArray) throw new Error('must specify vector to setComplex'); 42 | buffer.attr(this, 'position', points.map(p => p.toArray().slice(0, 2)), 2); 43 | buffer.index(this, cells); 44 | 45 | this.updateRandoms(points); 46 | this.updateUVs(points); 47 | } 48 | 49 | getBounds2D () { 50 | return this.boundingBox2; 51 | } 52 | 53 | // Triangulate a polygon 54 | setPoints (points) { 55 | if (points.length > 0 && !points[0].toArray) throw new Error('must specify vector to setPoints'); 56 | const array = flatten(points); 57 | const indices = earcut(array); 58 | buffer.attr(this, 'position', array, 2); 59 | buffer.index(this, indices); 60 | 61 | this.updateRandoms(points); 62 | this.updateUVs(points); 63 | } 64 | 65 | // For now, make these methods return empty bounding volumes, 66 | // since it doesn't work so well with 2D position attribute data. 67 | computeBoundingSphere() { 68 | if (this.boundingSphere === null) { 69 | this.boundingSphere = new THREE.Sphere(); 70 | } 71 | this.boundingSphere.center.set(0, 0, 0); 72 | this.boundingSphere.radius = 0; 73 | } 74 | 75 | computeBoundingBox() { 76 | if (this.boundingBox === null) { 77 | this.boundingBox = new THREE.Box3(); 78 | } 79 | this.boundingBox.makeEmpty(); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/geometry/getBlob.js: -------------------------------------------------------------------------------- 1 | const convexHull = require('convex-hull'); 2 | const RND = require('../util/random'); 3 | const newArray = require('new-array'); 4 | 5 | module.exports = function getCircularBlob (opt = {}) { 6 | const count = RND.randomInt(8, 10); 7 | const radius = 1; 8 | // Get a random set of points within a radius 9 | const input = newArray(count).map(() => { 10 | const r = RND.randomFloat(0.95, 1.05) * radius; 11 | return new THREE.Vector2().fromArray(RND.randomCircle([], r)).multiplyScalar(radius); 12 | }); 13 | // Get the convex hull that outlines all those points 14 | const edges = convexHull(input.map(p => p.toArray())); 15 | // Get a polyline of those edges 16 | return edges.map(edge => input[edge[0]]); 17 | }; 18 | -------------------------------------------------------------------------------- /src/geometry/getPolygon.js: -------------------------------------------------------------------------------- 1 | const defined = require('defined'); 2 | const RND = require('../util/random'); 3 | 4 | module.exports = (opt = {}) => { 5 | const steps = defined(opt.sides, 3); 6 | const points = []; 7 | const jitter = opt.jitter !== false; 8 | const jitterAmount = defined(opt.jitterAmount, 0.25); 9 | const radianJitter = defined(opt.radianJitter, 0.01); 10 | for (let i = 0; i < steps; i++) { 11 | let angle = i / steps * Math.PI * 2; 12 | if (jitter) { 13 | angle += Math.PI * 2 * RND.randomFloat(-1, 1) * radianJitter; 14 | } 15 | const x = Math.cos(angle); 16 | const y = Math.sin(angle); 17 | const vec = new THREE.Vector2(x, y); 18 | 19 | if (jitter) { 20 | const r = RND.randomFloat(0, 1) * jitterAmount; 21 | const off = new THREE.Vector2().fromArray(RND.randomCircle([], r)); 22 | vec.add(off); 23 | } 24 | 25 | points.push(vec); 26 | } 27 | return points; 28 | }; -------------------------------------------------------------------------------- /src/geometry/getRectangle.js: -------------------------------------------------------------------------------- 1 | const convexHull = require('convex-hull'); 2 | const RND = require('../util/random'); 3 | const newArray = require('new-array'); 4 | 5 | module.exports = function getCircularBlob (opt = {}) { 6 | const size = 1; 7 | const min = new THREE.Vector2(-1, -1).multiplyScalar(size); 8 | const max = new THREE.Vector2(1, 1).multiplyScalar(size); 9 | let path = [ 10 | min, 11 | new THREE.Vector2(max.x, min.y), 12 | max, 13 | new THREE.Vector2(min.x, max.y) 14 | ]; 15 | 16 | const rotation = RND.randomFloat(-1, 1) * Math.PI * 2; 17 | let width, height; 18 | 19 | const minDim = 0.5; 20 | const maxDim = 1.0; 21 | const dimScale = RND.randomFloat(0.5, 1.5); 22 | width = RND.randomFloat(minDim, maxDim); 23 | height = RND.randomFloat(minDim, maxDim); 24 | 25 | const typeList = [ 0, 1, 2 ]; 26 | const type = typeList[RND.randomInt(typeList.length)]; 27 | if (type === 0) { 28 | width *= dimScale; 29 | } else if (type === 1) { 30 | height *= dimScale; 31 | } else { 32 | width = height; 33 | } 34 | 35 | const center = new THREE.Vector2(0, 0); 36 | path.forEach(p => { 37 | p.x *= width; 38 | p.y *= height; 39 | const rotationOffset = RND.randomFloat(1) > 0.5 ? 0 : RND.randomFloat(0.0, 0.25); 40 | p.rotateAround(center, rotation + RND.randomFloat(-1, 1) * rotationOffset); 41 | }); 42 | return path; 43 | }; 44 | -------------------------------------------------------------------------------- /src/geometry/getSVGShape.js: -------------------------------------------------------------------------------- 1 | const svgMesh3d = require('svg-mesh-3d'); 2 | const SVG_SHAPES = require('../assets/svg-shapes.json'); 3 | const cache = {}; 4 | 5 | module.exports = function (key) { 6 | if (key in cache) return cache[key]; 7 | const svgPath = SVG_SHAPES[key]; 8 | 9 | // tweak these numbers to taste, for simplifcation and curve rounding 10 | const complex = svgMesh3d(svgPath, { simplify: 0.5, randomization: 0, scale: 2 }); 11 | const result = { 12 | positions: complex.positions.map(([x, y]) => { 13 | return new THREE.Vector2(x, y); 14 | }), 15 | cells: complex.cells 16 | }; 17 | 18 | cache[key] = result; 19 | return result; 20 | }; 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | global.THREE = require('three'); 2 | const query = require('./util/query'); 3 | const createArtwork = require('./createArtwork'); 4 | const keycode = require('keycode'); 5 | const canvas = document.querySelector('#canvas'); 6 | const presets = require('./scene/presets'); 7 | 8 | // Create the API. You should only create this once and re-use it. 9 | const artwork = createArtwork(canvas, { 10 | }); 11 | 12 | // artwork.onFinishIntro = () => { 13 | // artwork.transitionToPreset('default'); 14 | // }; 15 | 16 | // Some time before start(), we need to set the initial size 17 | artwork.resize(); 18 | 19 | // The next line is only necessary for the staging link prototype 20 | window.addEventListener('resize', () => artwork.resize()); 21 | 22 | // Load the assets at some point before start() 23 | // You should only call this once. 24 | artwork.load().then(() => { 25 | // Now that everything is loaded, we can start() and stop() the animation 26 | artwork.start({ intro: query.intro }); 27 | 28 | // You should not have these events in your redux/react app, but they 29 | // show how to use the API a bit more 30 | window.addEventListener('keydown', ev => { 31 | const key = keycode(ev); 32 | if (key === '1') { 33 | artwork.transitionToPreset('default'); 34 | } else if (key === '2') { 35 | artwork.transitionToPreset('ambient'); 36 | } else if (key === '3') { 37 | artwork.transitionToPreset('intro0'); 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/material/getShapeMaterial.js: -------------------------------------------------------------------------------- 1 | const defined = require('defined'); 2 | const path = require('path'); 3 | const glslify = require('glslify'); 4 | const RND = require('../util/random'); 5 | 6 | const vertexShader = glslify(path.resolve(__dirname, '../shader/shape.vert')); 7 | const fragmentShader = glslify(path.resolve(__dirname, '../shader/shape.frag')); 8 | 9 | module.exports = function (opt = {}) { 10 | const shader = new THREE.ShaderMaterial({ 11 | defines: Object.assign({}, opt.defines), 12 | vertexShader, 13 | fragmentShader, 14 | side: THREE.DoubleSide, 15 | extensions: { 16 | derivatives: true 17 | }, 18 | transparent: opt.transparent !== false, 19 | depthTest: Boolean(opt.depthTest), 20 | depthWrite: Boolean(opt.depthWrite), 21 | uniforms: { 22 | audioSignal: { value: new THREE.Vector3() }, 23 | frame: { value: 0 }, 24 | time: { value: 0 }, 25 | animate: { value: 0 }, 26 | maskMap: { value: new THREE.Texture() }, 27 | mapOffset: { value: new THREE.Vector2() }, 28 | mapScale: { value: 1 }, 29 | mapMask: { value: false, type: 'b' }, 30 | maskMapResolution: { value: new THREE.Vector2() }, 31 | map: { value: new THREE.Texture() }, 32 | shapeResolution: { value: new THREE.Vector2() }, 33 | randomOffset: { value: 0 }, 34 | centroid: { value: new THREE.Vector2() }, 35 | direction: { value: new THREE.Vector2(1, 0) }, 36 | velocity: { value: new THREE.Vector2() }, 37 | resolution: { value: new THREE.Vector2() }, 38 | mapResolution: { value: new THREE.Vector2() }, 39 | color: { value: new THREE.Color() }, 40 | altColor: { value: new THREE.Color() }, 41 | opacity: { value: 1 } 42 | } 43 | }); 44 | 45 | shader.randomize = function (opt = {}) { 46 | if (!opt.assets) throw new Error('expected { assets } option for patterns'); 47 | 48 | const newValues = opt.newValues !== false; 49 | const materialType = opt.materialType; 50 | this.defines.HAS_AUDIO = defined(opt.audio, this.defines.HAS_AUDIO, false); 51 | this.defines.HAS_FILL = materialType === 'outline' || /fill/i.test(materialType); 52 | this.defines.HAS_TEXTURE_PATTERN = /texture-pattern/i.test(materialType); 53 | this.defines.HAS_SHADER_PATTERN = /shader-pattern/i.test(materialType); 54 | this.defines.HIGH_FREQ_MOTION = defined(opt.highFrequencyMotion, this.defines.HIGH_FREQ_MOTION, true); 55 | 56 | const { tiles, masks } = opt.assets; 57 | const map = tiles[RND.randomInt(0, tiles.length)]; 58 | this.uniforms.map.value = map; 59 | this.uniforms.mapScale.value = RND.randomFloat(0.75, 1.0); 60 | this.uniforms.mapOffset.value.set(RND.randomFloat(-1, 1), RND.randomFloat(-1, 1)); 61 | this.uniforms.mapMask.value = RND.randomBoolean(); 62 | this.uniforms.mapResolution.value.set(map.image.width, map.image.height); 63 | 64 | const maskMap = masks[RND.randomInt(0, masks.length)]; 65 | this.uniforms.maskMap.value = maskMap; 66 | this.uniforms.maskMapResolution.value.set(maskMap.image.width, maskMap.image.height); 67 | 68 | const bounds = opt.bounds; 69 | this.uniforms.shapeResolution.value.set( 70 | bounds.max.x - bounds.min.x, 71 | bounds.max.y - bounds.min.y 72 | ); 73 | 74 | if (newValues) this.uniforms.randomOffset.value = RND.randomFloat(0, 1); 75 | if (opt.color) this.uniforms.color.value = opt.color; 76 | if (opt.altColor) this.uniforms.altColor.value = opt.altColor || opt.color; 77 | if (opt.centroid) this.uniforms.centroid.value.copy(opt.centroid); 78 | 79 | this.needsUpdate = true; 80 | return this; 81 | }; 82 | 83 | return shader; 84 | }; 85 | -------------------------------------------------------------------------------- /src/object/BaseObject.js: -------------------------------------------------------------------------------- 1 | module.exports = class BaseObject extends THREE.Object3D { 2 | constructor (app) { 3 | super(); 4 | 5 | // the app state that holds width/height etc 6 | this.app = app; 7 | 8 | // Whether the mesh is currently part of the scene or not 9 | this.active = false; 10 | } 11 | 12 | // For subclasses to implement... 13 | 14 | randomize () { 15 | } 16 | 17 | setAnimation (value) { 18 | } 19 | 20 | update (time, dt) { 21 | } 22 | 23 | frame (frame, time) { 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/object/Shape.js: -------------------------------------------------------------------------------- 1 | const defined = require('defined'); 2 | const BaseObject = require('./BaseObject'); 3 | const Polygon2D = require('../geometry/Polygon2D'); 4 | const { resampleLineByCount } = require('../util/polyline'); 5 | const RND = require('../util/random'); 6 | const colliderCircle = require('../util/colliderCircle'); 7 | const getPolygon = require('../geometry/getPolygon'); 8 | const getRectangle = require('../geometry/getRectangle'); 9 | const getBlob = require('../geometry/getBlob'); 10 | const getSVGShape = require('../geometry/getSVGShape'); 11 | const getShapeMaterial = require('../material/getShapeMaterial'); 12 | const circleIntersectsBox = require('../util/circleIntersectBox'); 13 | const strokePolygon = require('../util/strokePolygon'); 14 | 15 | const getCentroid = path => { 16 | return path 17 | .reduce((sum, point) => { 18 | return sum.add(point); 19 | }, new THREE.Vector2()) 20 | .divideScalar(path.length); 21 | }; 22 | 23 | const tmpVec2 = new THREE.Vector2(); 24 | 25 | module.exports = class Shape extends BaseObject { 26 | constructor (app) { 27 | super(app); 28 | 29 | // Debugging with wireframe material to see mesh structure 30 | const debugMaterial = false; 31 | const geometry = new Polygon2D(); 32 | const material = debugMaterial 33 | ? new THREE.MeshBasicMaterial({ color: 'black', wireframe: true, side: THREE.DoubleSide }) 34 | : getShapeMaterial(); 35 | 36 | this.mesh = new THREE.Mesh(geometry, material); 37 | this.mesh.frustumCulled = false; 38 | this.add(this.mesh); 39 | 40 | this.collisionArea = colliderCircle(); 41 | if (this.collisionArea.mesh) { 42 | this.add(this.collisionArea.mesh); 43 | } 44 | 45 | this.rotationSpeed = 0; 46 | this.running = false; 47 | this.time = 0; 48 | this.duration = 1; 49 | this.speed = 1; 50 | this.finished = false; 51 | this.maxVelocity = 0.002; 52 | this.isInView = false; 53 | 54 | this.velocity = new THREE.Vector2(); 55 | // this.acceleration = new THREE.Vector2(); 56 | this.friction = 0.99999; 57 | 58 | this.materialType = null; 59 | this.shapeType = null; 60 | } 61 | 62 | randomize (opt = {}) { 63 | const prevShapeType = this.shapeType; 64 | const prevMaterialType = this.materialType; 65 | this.shapeType = opt.shapeType || this.shapeType; 66 | this.materialType = opt.materialType || this.materialType; 67 | 68 | const shapeType = this.shapeType; 69 | let materialType = this.materialType; 70 | 71 | let centroid; 72 | let highFrequencyMotion = true; 73 | 74 | if (shapeType !== prevShapeType || materialType === 'stroke') { 75 | // get a new list of points 76 | let points; 77 | let svg; 78 | if (shapeType === 'polygon') points = getPolygon(); 79 | else if (shapeType === 'square') points = getPolygon({ sides: 4 }); 80 | else if (shapeType === 'rectangle-blob') points = getRectangle(); 81 | else if (shapeType === 'triangle') points = getPolygon({ sides: 3 }); 82 | else if (shapeType === 'circle') points = getPolygon({ sides: 32, jitter: false }); 83 | else if (shapeType === 'circle-blob') points = getBlob(); 84 | else if (shapeType === 'svg-heart') svg = getSVGShape('heart'); 85 | else if (shapeType === 'svg-feather') svg = getSVGShape('feather'); 86 | else if (shapeType === 'svg-lightning') svg = getSVGShape('lightning'); 87 | else points = getBlob(); 88 | 89 | // SVG is already triangulated 90 | if (!points && svg) { 91 | points = svg.positions; 92 | } 93 | 94 | // get centroid of polygon 95 | centroid = getCentroid(points); 96 | 97 | const isStroke = materialType === 'stroke'; 98 | // generate the new (triangulated) geometry data 99 | if (isStroke) { 100 | highFrequencyMotion = false; 101 | svg = false; 102 | // const finalCount = RND.randomInt(100, 200); 103 | // const resampled = resampleLineByCount(points, finalCount, true); 104 | points = this._roundPoints(points, shapeType, materialType); 105 | const thickness = RND.randomFloat(0.025, 0.075); 106 | const { positions, cells } = strokePolygon(points, { 107 | thickness 108 | }); 109 | points = positions.map(p => new THREE.Vector2().fromArray(p)); 110 | this.mesh.geometry.setComplex(points, cells); 111 | } else if (svg) { 112 | this.mesh.geometry.setComplex(svg.positions, svg.cells); 113 | } else { 114 | points = this._roundPoints(points, shapeType, materialType); 115 | 116 | // resample along the path so we can add high frequency noise to give it rough edges in vert shader 117 | const finalCount = RND.randomInt(200, 600); 118 | const resampled = resampleLineByCount(points, finalCount, true); 119 | this.mesh.geometry.setPoints(resampled); 120 | } 121 | } 122 | 123 | // get a new material with color etc 124 | if (this.mesh.material.randomize) { 125 | this.mesh.material.randomize({ 126 | ...opt, 127 | highFrequencyMotion, 128 | centroid, 129 | materialType: this.materialType === 'stroke' ? 'fill' : this.materialType, 130 | bounds: this.mesh.geometry.getBounds2D(), 131 | assets: this.app.assets 132 | }); 133 | } 134 | 135 | return true; 136 | } 137 | 138 | _roundPoints (points, shapeType, materialType) { 139 | // If we should 'round' the points with splines 140 | const round = shapeType !== 'circle'; 141 | if (round) { 142 | const minTension = shapeType === 'rectangle-blob' ? 0 : 0.1; 143 | const maxTension = shapeType === 'rectangle-blob' ? 1 : 0.25; 144 | let roundTension = RND.randomBoolean() ? minTension : RND.randomFloat(minTension, maxTension); 145 | if (materialType === 'stroke') roundTension = 0.15; 146 | const roundType = shapeType === 'circle-blob' ? 'chordal' : 'catmullrom'; 147 | let roundSegments = shapeType === 'circle-blob' ? 30 : 40; 148 | if (materialType === 'stroke') roundSegments = 40; 149 | const curve = new THREE.CatmullRomCurve3(points.map(p => new THREE.Vector3(p.x, p.y, 0))); 150 | curve.closed = true; 151 | curve.tension = roundTension; 152 | curve.curveType = roundType; 153 | points = curve 154 | .getSpacedPoints(roundSegments) 155 | .slice(0, roundSegments) 156 | .map(p => new THREE.Vector2(p.x, p.y)); 157 | } 158 | return points; 159 | } 160 | 161 | reset (opt = {}) { 162 | this.time = 0; 163 | this.finished = false; 164 | this.running = false; 165 | this.isInView = false; 166 | this.hasBeenSeen = false; 167 | this.resetSpeeds(opt); 168 | } 169 | 170 | resetSpeeds (opt = {}) { 171 | let speedFactor = 1; 172 | if (opt.mode === 'default') speedFactor = 2; 173 | else if (opt.mode === 'intro') speedFactor = 5; 174 | 175 | this.speed = RND.randomFloat(0.25, 0.5) * speedFactor; 176 | if (opt.mode === 'ambient') { 177 | this.duration = RND.randomFloat(30, 60); 178 | this.rotationSpeed = RND.randomSign() * RND.randomFloat(0.0005, 0.001) * 0.01; 179 | } else { 180 | this.duration = opt.mode === 'intro' ? RND.randomFloat(2, 8) : RND.randomFloat(10, 20); 181 | this.rotationSpeed = RND.randomSign() * RND.randomFloat(0.0005, 0.001); 182 | } 183 | } 184 | 185 | setAnimation(value) { 186 | // animate in / out state 187 | if (this.mesh.material.uniforms) { 188 | this.mesh.material.uniforms.animate.value = value; 189 | } 190 | } 191 | 192 | _finish () { 193 | if (!this.finished) { 194 | this.finished = true; 195 | } 196 | 197 | if (this.running && typeof this.onFinishMovement === 'function') { 198 | this.onFinishMovement(); 199 | } 200 | this.running = false; 201 | } 202 | 203 | update(time, dt) { 204 | if (this.active && this.visible && this.running && !this.finished) { 205 | const worldSphere = this.collisionArea.getWorldSphere(this); 206 | this.isInView = circleIntersectsBox(worldSphere, this.app.sceneBounds); 207 | if (this.isInView && !this.hasBeenSeen) { 208 | this.hasBeenSeen = true; 209 | } else if (!this.isInView && this.hasBeenSeen) { 210 | this._finish(); 211 | } 212 | } else { 213 | this.isInView = false; 214 | } 215 | 216 | if (this.running && !this.finished) { 217 | this.time += dt; 218 | } 219 | 220 | if (this.time > this.duration) { 221 | this._finish(); 222 | } 223 | 224 | // animation values 225 | this.rotation.z += this.rotationSpeed; 226 | if (this.mesh.material.uniforms) { 227 | this.mesh.material.uniforms.time.value = time; 228 | this.mesh.material.uniforms.resolution.value.set(this.app.width, this.app.height); 229 | } 230 | 231 | this.position.x += this.velocity.x * this.speed; 232 | this.position.y += this.velocity.y * this.speed; 233 | this.velocity.multiplyScalar(this.friction); 234 | this.velocity.clampScalar(-this.maxVelocity, this.maxVelocity); 235 | 236 | const bounds = this.mesh.geometry.getBounds2D(); 237 | const size = bounds.getSize(tmpVec2); 238 | this.collisionArea.center.set(bounds.min.x + size.x / 2, bounds.min.y + size.y / 2); 239 | 240 | const radiusScale = 0.25; 241 | this.collisionArea.radius = Math.sqrt(size.x * size.x + size.y * size.y) * radiusScale; 242 | 243 | this.collisionArea.update(); 244 | } 245 | 246 | frame(frame, time) { 247 | // called on every 'tick', i.e. a fixed fps lower than 60, to give a jittery feeling 248 | if (this.mesh.material.uniforms) this.mesh.material.uniforms.frame.value = time; 249 | } 250 | }; 251 | -------------------------------------------------------------------------------- /src/object/ZigZag.js: -------------------------------------------------------------------------------- 1 | const anime = require('animejs'); 2 | const normalizePath = require('normalize-path-scale'); 3 | const newArray = require('new-array'); 4 | const BaseObject = require('./BaseObject'); 5 | const defined = require('defined'); 6 | const { MeshLine, MeshLineMaterial } = require('three.meshline'); 7 | const RND = require('../util/random'); 8 | 9 | const SVG_PATH = 10 | 'M386.4,306.3 C386.4,353.3 367.9,385.6 345.6,385.6 C323.3,385.6 305.2,360.7 305.2,329.9 L305.2,306.3 C305.2,275.5 292.1,250.6 269.8,250.6 C247.5,250.6 234.5,275.5 234.5,306.3 L234.5,329.9 C234.5,360.7 209.5,385.6 187.2,385.6 C164.9,385.6 146.8,360.7 146.8,329.9 L146.8,306.2 C146.8,275.4 128.8,250.5 106.5,250.5 C84.2,250.5 73.1,275.4 73.2,306.2 L73.2,329.8 C73.2,360.5 55.1,377.5 32.9,377.5 C10.6,377.5 -7.5,360.6 -7.5,329.8 L-7.5,306.1 C-7.5,275.4 -25.6,250.5 -47.9,250.5 C-70.2,250.5 -95.2,275.4 -95.2,306.2 L-95.2,329.8 C-95.1,360.6 -108.2,377.5 -130.5,377.5 C-152.8,377.5 -168.8,360.5 -168.8,329.8 L-168.8,306.1 C-169,275.3 -187,250.4 -209.3,250.4 C-231.6,250.4 -249.6,275.3 -249.6,306.1'; 11 | 12 | function makePath(svgData) { 13 | const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 14 | path.setAttributeNS(null, 'd', svgData); 15 | 16 | return path; 17 | } 18 | 19 | module.exports = class ZigZag extends BaseObject { 20 | constructor(app, opt = {}) { 21 | super(app); 22 | 23 | this.app = app; 24 | const path = makePath(SVG_PATH); 25 | 26 | const pathSampleCount = defined(opt.segments, 200); 27 | const pathPoints = normalizePath( 28 | newArray(pathSampleCount).map((_, i) => { 29 | const point = path.getPointAtLength((i / pathSampleCount) * path.getTotalLength()); 30 | return [point.x, point.y]; 31 | }) 32 | ); 33 | this.pathPoints = pathPoints; 34 | 35 | this.speed = defined(opt.speed, 0.5); 36 | this.zigZagIdx = 0; 37 | this.headPos = new THREE.Vector2(); 38 | this.tailPos = new THREE.Vector2(); 39 | 40 | this.line = new MeshLine(); 41 | this.line.setGeometry(this.getLineGeometry(this.zigZagIdx)); 42 | 43 | const color = defined(opt.color, { r: 0, g: 0, b: 0 }); 44 | this.color = color; 45 | 46 | const material = new MeshLineMaterial({ 47 | resolution: new THREE.Vector2(app.width, app.height), 48 | color, 49 | lineWidth: defined(opt.lineWidth, 0.04) 50 | }); 51 | material.depthTest = false; 52 | material.depthWrite = false; 53 | material.transparent = true; // for layering 54 | 55 | this.mesh = new THREE.Mesh(this.line.geometry, material); 56 | this.mesh.frustumCulled = false; 57 | 58 | this.add(this.mesh); 59 | } 60 | 61 | destroy() { 62 | this.mesh.geometry.dispose(); 63 | } 64 | 65 | randomize(opt = {}) { 66 | if (this.lineWidthAnimation) this.lineWidthAnimation.pause(); 67 | if (this.colorAnimation) this.colorAnimation.pause(); 68 | if (opt.color) { 69 | this.color = opt.color; 70 | this.mesh.material.uniforms.color.value = opt.color; 71 | } 72 | if (typeof opt.lineWidth === 'number') this.mesh.material.uniforms.lineWidth.value = opt.lineWidth; 73 | this.delay = opt.delay || 0; 74 | this.initTime = undefined; 75 | if (opt.speed) this.speed = opt.speed; 76 | 77 | this.mesh.material.uniforms.resolution.value.x = this.app.width; 78 | this.mesh.material.uniforms.resolution.value.y = this.app.height; 79 | } 80 | 81 | transitionColor(color) { 82 | if (this.colorAnimation) this.colorAnimation.pause(); 83 | 84 | this.colorAnimation = anime({ 85 | targets: this.color, 86 | r: color.r, 87 | g: color.g, 88 | b: color.b, 89 | easing: 'linear', 90 | duration: 1000 91 | }); 92 | } 93 | 94 | animateOut() { 95 | if (this.lineWidthAnimation) this.lineWidthAnimation.pause(); 96 | this.lineWidthAnimation = anime({ 97 | targets: this.mesh.material.uniforms.lineWidth, 98 | value: 0, 99 | easing: 'easeOutExpo', 100 | duration: 2000 101 | }); 102 | } 103 | 104 | reset() { 105 | this.headPos = new THREE.Vector2(); 106 | this.tailPos = new THREE.Vector2(); 107 | this.zigZagIdx = 0; 108 | this.line.setGeometry(this.getLineGeometry(this.zigZagIdx)); 109 | } 110 | 111 | getLineGeometry(idx) { 112 | const geometry = new THREE.Geometry(); 113 | 114 | this.getZigZagPoints(idx).forEach(v => geometry.vertices.push(v)); 115 | 116 | this.headPos.x = geometry.vertices[0].x; 117 | this.headPos.y = geometry.vertices[0].y; 118 | 119 | this.tailPos.x = geometry.vertices[geometry.vertices.length - 1].x; 120 | this.tailPos.y = geometry.vertices[geometry.vertices.length - 1].y; 121 | 122 | return geometry; 123 | } 124 | 125 | getZigZagPoints(idx) { 126 | const zigZagPoints = this.pathPoints.map((_, i) => { 127 | const finalIdx = Math.floor(i + idx) % this.pathPoints.length; 128 | const finalPoint = this.pathPoints[finalIdx]; 129 | const xOffset = Math.floor((i + idx) / this.pathPoints.length) * 2; 130 | const yOffset = 0; 131 | 132 | return new THREE.Vector3(finalPoint[0] - xOffset, finalPoint[1] + yOffset, 0); 133 | }); 134 | 135 | return zigZagPoints; 136 | } 137 | 138 | update(time) { 139 | if (!this.initTime) this.initTime = time; 140 | if (time - this.initTime < this.delay) { 141 | this.mesh.material.visible = false; 142 | return; 143 | } 144 | this.mesh.material.visible = true; 145 | 146 | this.zigZagIdx += this.speed; 147 | this.line.setGeometry(this.getLineGeometry(this.zigZagIdx)); 148 | } 149 | }; 150 | -------------------------------------------------------------------------------- /src/scene/MainScene.js: -------------------------------------------------------------------------------- 1 | const RND = require('../util/random'); 2 | const clamp = require('clamp'); 3 | const newArray = require('new-array'); 4 | const anime = require('animejs'); 5 | const colliderCircle = require('../util/colliderCircle'); 6 | const touches = require('touches'); 7 | const defined = require('defined'); 8 | const Shape = require('../object/Shape'); 9 | const pickColors = require('../util/pickColors'); 10 | const noop = () => {}; 11 | 12 | const shapeTypes = [ 13 | { weight: 100, value: 'circle-blob' }, 14 | { weight: 100, value: 'rectangle-blob' }, 15 | { weight: 30, value: 'triangle' }, 16 | { weight: 5, value: 'circle' }, 17 | { weight: 10, value: 'square' }, 18 | { weight: 40, value: 'svg-heart' }, 19 | { weight: 40, value: 'svg-feather' }, 20 | { weight: 10, value: 'svg-lightning' } 21 | ]; 22 | 23 | const strokeShapesToAllow = ['square', 'rectangle-blob', 'svg-heart']; 24 | 25 | // Other types: 26 | // 'squiggle', 'ring', 27 | // 'eye', 'feather', 'lightning', 'heart' 28 | 29 | const makeMaterialTypesWeights = (mode, opt = {}) => { 30 | return [ 31 | { weight: mode === 'ambient' ? 5 : 100, value: 'fill' }, 32 | { weight: mode === 'intro' ? 200 : 50, value: 'texture-pattern' }, 33 | // opt.stroke !== false ? { weight: mode === 'intro' ? 12 : 12, value: 'stroke' } : false 34 | // { weight: 50, value: 'shader-pattern' } 35 | // { weight: 25, value: 'fill-texture-pattern' } 36 | ].filter(Boolean).filter(t => { 37 | if (opt.currentMaterialType) { 38 | return t.value !== opt.currentMaterialType; 39 | } 40 | return true; 41 | }); 42 | }; 43 | 44 | const makeScale = ({ mode, materialType }) => { 45 | if (mode === 'ambient' && materialType === 'fill') return RND.randomFloat(0.5, 1); 46 | return RND.weighted([ 47 | { weight: 100, value: RND.randomFloat(0.5, 1.0) }, 48 | { weight: 50, value: RND.randomFloat(0.5, 2) } 49 | ]); 50 | // if (RND.randomFloat(1) > 0.75) return RND.randomFloat(0.5, 3); 51 | // return RND.randomFloat(0.5, 3.0); 52 | // return mode === 'intro' ? RND.randomFloat(0.5, 2) : RND.randomFloat(0.5, 2.75); 53 | // return materialType === 'fill' ? RND.randomFloat(0.5, 2) : RND.randomFloat(0.5, 2.5); 54 | // if (mode === 'intro') return RND.randomFloat(0.5, 2.5); 55 | // if (mode !== 'ambient') return RND.randomFloat(0.5, 2); 56 | // white fill in ambient mode only looks good for small shapes 57 | // return materialType === 'fill' ? RND.randomFloat(0.5, 0.75) : RND.randomFloat(0.5, 4.0); 58 | }; 59 | 60 | // const scales = [ 61 | // { weight: 50, value: () => RND.randomFloat(2.5, 4) }, 62 | // { weight: 100, value: () => RND.randomFloat(1.5, 2.5) }, 63 | // { weight: 50, value: () => RND.randomFloat(0.75, 1.5) }, 64 | // { weight: 25, value: () => RND.randomFloat(0.5, 0.75) } 65 | // ] 66 | 67 | // const effects = { 68 | // dropShadow: true, // only works with certain geom types? 69 | // sharpEdges: false // rounded edges or not for things like triangle/etc 70 | // } 71 | 72 | const getRandomMaterialProps = (preset, opt = {}) => { 73 | // Randomize the object and its materials 74 | const materialType = RND.weighted(makeMaterialTypesWeights(preset.mode, opt)); 75 | 76 | const { color, altColor } = pickColors(preset.colors, materialType === 'texture-pattern' ? 'pattern' : 'shape'); 77 | 78 | let computedShapeTypes = shapeTypes; 79 | if (materialType === 'stroke') { 80 | computedShapeTypes = computedShapeTypes.filter(t => { 81 | return strokeShapesToAllow.includes(t.value); 82 | }); 83 | } 84 | computedShapeTypes = computedShapeTypes.filter(t => { 85 | if (opt.currentShapeType) { 86 | return t.value !== opt.currentShapeType; 87 | } 88 | return true; 89 | }); 90 | const shapeType = RND.weighted(computedShapeTypes); 91 | return { shapeType, materialType, altColor, color }; 92 | }; 93 | 94 | module.exports = class MainScene extends THREE.Object3D { 95 | constructor(app) { 96 | super(); 97 | this.app = app; 98 | window.test = this; 99 | this.introTimer = 0; 100 | this.presetTweens = []; 101 | this.tweens = []; 102 | this.running = true; 103 | 104 | const maxCapacity = 100; 105 | 106 | this.poolContainer = new THREE.Group(); 107 | this.add(this.poolContainer); 108 | this.pool = newArray(maxCapacity).map((_, i) => { 109 | const mesh = new Shape(app); 110 | mesh.visible = false; 111 | mesh.poolIndex = i; 112 | mesh.sortOrder = 0; 113 | this.poolContainer.add(mesh); 114 | return mesh; 115 | }); 116 | 117 | this.textCollider = colliderCircle({ radius: 0.85 }); 118 | this.textCollider.center.x = -0.1; 119 | if (this.textCollider.mesh) this.add(this.textCollider.mesh); 120 | this.updatePresetRadius(); 121 | } 122 | 123 | updatePresetRadius () { 124 | this.textCollider.radius = this.app.preset.mode === 'intro' ? 0.85 : 0.5; 125 | } 126 | 127 | clear() { 128 | // reset pool to initial state 129 | this.pool.forEach(p => { 130 | p.visible = false; 131 | p.active = false; 132 | p.onFinishMovement = noop; 133 | }); 134 | this.clearPresetTweens(); 135 | } 136 | 137 | clearPresetTweens () { 138 | this.presetTweens.forEach(p => p.pause()); 139 | this.presetTweens.length = 0; 140 | } 141 | 142 | getRandomPosition () { 143 | const app = this.app; 144 | const edges = [ 145 | [new THREE.Vector2(-1, -1), new THREE.Vector2(1, -1)], 146 | [new THREE.Vector2(1, -1), new THREE.Vector2(1, 1)], 147 | [new THREE.Vector2(1, 1), new THREE.Vector2(-1, 1)], 148 | [new THREE.Vector2(-1, 1), new THREE.Vector2(-1, -1)] 149 | ]; 150 | const edgeIndex = RND.randomInt(edges.length); 151 | const isTopOrBottom = edgeIndex === 0 || edgeIndex === 2; 152 | const edge = edges[edgeIndex]; 153 | // const t = RND.randomFloat(0, 1) 154 | const t = isTopOrBottom 155 | ? RND.randomBoolean() 156 | ? RND.randomFloat(0.0, 0.35) 157 | : RND.randomFloat(0.65, 1) 158 | : RND.randomFloat(0, 1); 159 | const vec = edge[0].clone().lerp(edge[1], t); 160 | // vec.x *= RND.randomFloat(1.0, 1.2); 161 | // vec.y *= RND.randomFloat(1.0, 1.25); 162 | vec.multiply(app.unitScale); 163 | return vec; 164 | } 165 | 166 | getComputedRandomPosition () { 167 | const count = 20; 168 | 169 | const spheres = this.pool.filter(p => p.active).map(shape => { 170 | return shape.collisionArea.getWorldSphere(shape); 171 | }); 172 | 173 | let hitText = false; 174 | const radius = 0.5; 175 | const textSphere = this.textCollider.getWorldSphere(this); 176 | const positions = newArray(count).map(() => { 177 | const position = this.getRandomPosition(); 178 | 179 | let collisions = 0; 180 | for (let i = 0; i < spheres.length; i++) { 181 | const sphere = spheres[i]; 182 | const sumRadii = sphere.radius + radius; 183 | const deltaSq = position.distanceToSquared(sphere.center) 184 | if (deltaSq <= (sumRadii * sumRadii)) { 185 | collisions++; 186 | } 187 | } 188 | return { 189 | position, 190 | hitText, 191 | collisions 192 | }; 193 | }); 194 | 195 | // Flip between spawning shapes clustered near other shapes, and sometimes 196 | // away from other shapes. But more frequently we want sparseness 197 | // const dense = false; 198 | const dense = RND.randomFloat(1) > 0.85; 199 | if (dense) positions.sort((a, b) => b.collisions - a.collisions); 200 | else positions.sort((a, b) => a.collisions - b.collisions); 201 | 202 | const p = positions[0].position.multiplyScalar(RND.randomFloat(0.5, 1.2)); 203 | 204 | // ensure it doesn't spawn right in center 205 | const deltaTxtSq = p.distanceToSquared(textSphere.center); 206 | const collisionRadius = 0.85; 207 | const sumTxtRadii = collisionRadius + textSphere.radius; 208 | if (deltaTxtSq <= (sumTxtRadii * sumTxtRadii)) { 209 | // push the shape away from center text radius a bit... 210 | const dir = p.clone().normalize(); 211 | p.addScaledVector(dir, textSphere.radius); 212 | } 213 | 214 | return p; 215 | } 216 | 217 | findAvailableObject () { 218 | const pool = this.pool; 219 | const activeCount = pool.filter(p => p.active).length; 220 | if (activeCount >= this.app.preset.capacity) return; 221 | return RND.shuffle(pool).find(p => !p.active); 222 | } 223 | 224 | next () { 225 | if (!this.running) return; 226 | const app = this.app; 227 | const pool = this.pool; 228 | 229 | // Get unused mesh 230 | const object = this.findAvailableObject(); 231 | 232 | // No free meshes 233 | if (!object) return; 234 | const preset = app.preset; 235 | 236 | const materialProps = getRandomMaterialProps(preset); 237 | object.reset({ mode: preset.mode }); // reset time properties 238 | 239 | const result = object.randomize({ 240 | audio: this.app.intro, 241 | ...materialProps 242 | }); // reset color/etc 243 | 244 | if (!result) return; 245 | 246 | // Now in scene, no longer in pool 247 | object.active = true; 248 | // But initially hidden until we animate in 249 | object.visible = false; 250 | 251 | // randomize position and scale 252 | let scale = makeScale({ mode: preset.mode, materialType: materialProps.materialType }); 253 | scale *= 1.5; 254 | if (materialProps.materialType === 'stroke') { 255 | scale = RND.randomFloat(0.75, 1.25); 256 | } 257 | // if (preset.mode === 'intro') { 258 | // scale *= RND.randomFloat(0.75, 1); 259 | // } 260 | // const scale = RND.weighted(scales)() 261 | object.scale.setScalar(scale * (1 / 3) * app.targetScale); 262 | 263 | let sortWeight = 0; 264 | if (/pattern/i.test(materialProps.materialType)) { 265 | sortWeight += RND.randomFloat(0.75, 1); 266 | } 267 | sortWeight += 1 - clamp(object.scale.x / 2, 0, 1); 268 | object.sortOrder = Math.round(this.app.preset.capacity * Math.min(1, sortWeight)); 269 | 270 | let p = this.getComputedRandomPosition(); 271 | 272 | if (materialProps.materialType === 'stroke') { 273 | p.multiplyScalar(RND.randomFloat(0.5, 1)); 274 | } 275 | p.multiplyScalar(RND.randomFloat(0.85, 1.15)); 276 | object.position.set(p.x, p.y, 0); 277 | 278 | let randomDirection; 279 | const moveTowardCenter = preset.mode !== 'intro' && RND.randomFloat(1) > 0.9; 280 | if (moveTowardCenter) randomDirection = p.clone().normalize().negate(); 281 | else randomDirection = new THREE.Vector2().fromArray(RND.randomCircle([], 1)); 282 | 283 | // const randomLength = RND.randomFloat(0.25, 5); 284 | // randomDirection.y /= app.unitScale.x; 285 | // other.addScaledVector(randomDirection, 1); 286 | // other.addScaledVector(randomDirection, randomLength); 287 | 288 | const heading = object.position 289 | .clone() 290 | .normalize() 291 | .negate(); 292 | const rotStrength = RND.randomFloat(0, 1); 293 | heading.addScaledVector(randomDirection, rotStrength).normalize(); 294 | 295 | // start at zero 296 | const animation = { value: 0 }; 297 | object.setAnimation(animation.value); 298 | const updateAnimation = () => { 299 | object.setAnimation(animation.value); 300 | }; 301 | 302 | let animationDuration; 303 | if (preset.mode === 'ambient') animationDuration = RND.randomFloat(16000, 16000 * 2); 304 | else if (preset.mode === 'intro') animationDuration = RND.randomFloat(4000, 8000); 305 | else animationDuration = RND.randomFloat(4000, 8000); 306 | 307 | const durationMod = app.targetScale; 308 | object.velocity.setScalar(0); 309 | object.velocity.addScaledVector(heading, 0.001 * durationMod); 310 | 311 | // const newAngle = object.rotation.z + RND.randomFloat(-1, 1) * Math.PI * 2 * 0.25 312 | let defaultDelay = RND.randomFloat(0, 11000); 313 | if (preset.mode === 'intro') { 314 | defaultDelay = RND.randomFloat(0, 10000); 315 | } 316 | let startDelay = defaultDelay; 317 | const animIn = anime({ 318 | targets: animation, 319 | value: 1, 320 | update: updateAnimation, 321 | easing: 'easeOutExpo', 322 | delay: startDelay, 323 | begin: () => { 324 | object.running = true; 325 | object.visible = true; 326 | }, 327 | duration: animationDuration 328 | }); 329 | 330 | this.tweens.push(animIn); 331 | object.onFinishMovement = () => { 332 | object.onFinishMovement = noop; 333 | animIn.pause(); 334 | const animOut = anime({ 335 | targets: animation, 336 | update: updateAnimation, 337 | value: 0, 338 | complete: () => { 339 | // Hide completely 340 | object.onFinishMovement = null; 341 | object.visible = false; 342 | object.running = false; 343 | // Place back in pool for re-use 344 | object.active = false; 345 | this.next(); 346 | }, 347 | easing: 'easeOutQuad', 348 | duration: animationDuration 349 | }); 350 | this.tweens.push(animOut); 351 | }; 352 | } 353 | 354 | sortObjects () { 355 | this.poolContainer.children.sort((a, b) => { 356 | if (a.sortOrder === b.sortOrder) return a.poolIndex - b.poolIndex; 357 | return a.sortOrder - b.sortOrder; 358 | }); 359 | } 360 | 361 | sortObject (object) { 362 | const curIdx = this.poolContainer.indexOf(object); 363 | } 364 | 365 | start(opt = {}) { 366 | this.running = true; 367 | this.clearPresetTweens(); 368 | 369 | this.beats = this.app.audio.BEAT_TIMES.slice(); 370 | 371 | const app = this.app; 372 | const pool = this.pool; 373 | this.introTimer = 0; 374 | this.emitInitial(); 375 | } 376 | 377 | stop () { 378 | this.tweens.forEach(t => t.pause()); 379 | this.tweens.length = 0; 380 | this.clear(); 381 | } 382 | 383 | emitInitial () { 384 | // if (this.app.preset.mode === 'intro') { 385 | // this.next(); 386 | // } else { 387 | for (let i = 0; i < this.app.preset.initialCapacity; i++) { 388 | this.next(); 389 | } 390 | 391 | // sort initially.... 392 | this.sortObjects(); 393 | // } 394 | } 395 | 396 | beat () { 397 | this.next(); 398 | } 399 | 400 | trimCapacity () { 401 | const active = this.pool.filter(p => p.active); 402 | if (active.length <= this.app.preset.capacity) { 403 | // Less than initial, let's fill things up 404 | const remainder = Math.max(0, Math.min(this.app.preset.capacity, this.app.preset.initialCapacity - active.length)); 405 | for (let i = 0; i < remainder; i++) { 406 | this.next(); 407 | } 408 | } else { 409 | const toKill = RND.shuffle(active).slice(this.app.preset.capacity); 410 | toKill.forEach(k => k.onFinishMovement()); 411 | } 412 | } 413 | 414 | onPresetChanged (preset, oldPreset) { 415 | this.running = true; 416 | console.log('changed') 417 | 418 | // Preset has 'hard' changed, i.e. flash to new content 419 | this.pool.forEach(shape => { 420 | if (!shape.active) return; 421 | const newProps = getRandomMaterialProps(preset); 422 | shape.randomize(newProps); 423 | }); 424 | this.stop(); // cancel all waiting tweens! 425 | this.emitInitial(); 426 | this.updatePresetRadius(); 427 | } 428 | 429 | onPresetTransition (preset, oldPreset) { 430 | this.running = true; 431 | // kill old tweens 432 | this.clearPresetTweens(); 433 | 434 | // Transition colors to new features 435 | this.pool.forEach(shape => { 436 | if (!shape.active || !shape.mesh.material.uniforms) return; 437 | shape.resetSpeeds({ mode: preset.mode }); 438 | const newProps = getRandomMaterialProps(preset); 439 | 440 | const oldColor = shape.mesh.material.uniforms.color.value.clone(); 441 | const newColor = newProps.color.clone(); 442 | const tween = { value: 0 }; 443 | const t = anime({ 444 | targets: tween, 445 | duration: 5000, 446 | value: 1, 447 | update: () => { 448 | const color = shape.mesh.material.uniforms.color.value; 449 | color.copy(oldColor).lerp(newColor, tween.value); 450 | } 451 | }); 452 | this.presetTweens.push(t); 453 | }); 454 | 455 | // Set new capacity without killing existing 456 | this.trimCapacity(); 457 | this.updatePresetRadius(); 458 | } 459 | 460 | onTrigger(event, args) { 461 | const app = this.app; 462 | if (event === 'fadeOut') { 463 | this.pool.forEach(s => { 464 | if (s.active) s.onFinishMovement(); 465 | }); 466 | this.running = false; 467 | } else if (event === 'introSwap') { 468 | 469 | } else if (event === 'randomize') { 470 | // this.pool.forEach(p => { 471 | // p.renderOrder = RND.randomInt(-10, 10); 472 | // }); 473 | // console.log('sort'); 474 | 475 | } else if (event === 'palette') { 476 | // force shapes to animate out, this will call next() again, and make them re-appear with proper colors 477 | this.pool.forEach(shape => { 478 | if (shape.active) { 479 | shape.onFinishMovement(); 480 | } 481 | }); 482 | } else if (event === 'clear') { 483 | this.clear(); 484 | } else if (event === 'start') { 485 | this.start(); 486 | } else if (event === 'switchMode') { 487 | 488 | } else if (event === 'colliderPosition') { 489 | this.textCollider.center.x = args.x; 490 | this.textCollider.center.y = args.y; 491 | if (args.radius) this.textCollider.radius = args.radius; 492 | } else if (event === 'beat' && this.app.preset.mode === 'intro') { 493 | this.beat(); 494 | } 495 | } 496 | 497 | update(time, dt) { 498 | this.introTimer += dt; 499 | if (this.running && this.beats && this.app.intro && this.app.audio.playing) { 500 | const time = this.app.audio.element.currentTime; 501 | let hit = false; 502 | let isMajor = false; 503 | if (isFinite(time)) { 504 | let indexToKill = -1; 505 | for (let i = 0; i < this.beats.length; i++) { 506 | const b = this.beats[i]; 507 | if (time > (b.time - this.app.audio.BEAT_LEAD_TIME)) { 508 | hit = true; 509 | isMajor = b.major; 510 | indexToKill = i; 511 | break; 512 | } 513 | } 514 | if (indexToKill !== -1) { 515 | this.beats = this.beats.slice(indexToKill + 1); 516 | } 517 | 518 | if (hit) { 519 | this.pool.forEach(shape => { 520 | if (!shape.active || shape.materialType === 'stroke') return; 521 | let { materialType, shapeType } = getRandomMaterialProps(this.app.preset, { 522 | stroke: false, 523 | currentShapeType: shape.shapeType, 524 | currentMaterialType: shape.materialType 525 | }); 526 | if (!isMajor) shapeType = undefined; 527 | shape.randomize({ materialType, shapeType, newValues: false }); 528 | }); 529 | } 530 | } 531 | } 532 | 533 | this.textCollider.update(); 534 | 535 | const tmpVec2 = new THREE.Vector2(); 536 | const tmpVec3 = new THREE.Vector3(); 537 | 538 | const b = this.textCollider.getWorldSphere(this); 539 | this.pool.forEach(shape => { 540 | if (!shape.active || !shape.running) return; 541 | 542 | if (shape.mesh.material.uniforms) { 543 | shape.mesh.material.uniforms.audioSignal.value.fromArray(this.app.audioSignal); 544 | } 545 | 546 | const a = shape.collisionArea.getWorldSphere(shape); 547 | 548 | const size = shape.scale.x / this.app.targetScale * 3; 549 | 550 | if (a.intersectsSphere(b)) { 551 | tmpVec3.copy(a.center).sub(b.center); 552 | const bounce = 0.000025 * (4 / size); 553 | shape.velocity.addScaledVector(tmpVec2.copy(tmpVec3), bounce); 554 | } 555 | }); 556 | } 557 | }; 558 | 559 | function circlesCollide(a, b) { 560 | const delta = a.center.distanceToSquared(b.center); 561 | const r = a.radius + b.radius; 562 | return delta <= r * r; 563 | } 564 | -------------------------------------------------------------------------------- /src/scene/ZigZagScene.js: -------------------------------------------------------------------------------- 1 | const RND = require('../util/random'); 2 | const ZigZag = require('../object/ZigZag'); 3 | const newArray = require('new-array'); 4 | const pickColors = require('../util/pickColors'); 5 | const presets = require('../scene/presets'); 6 | 7 | function pointOutsideRect([x, y], [rw, rh]) { 8 | return x > rw || x < -rw || y > rh || y < -rh; 9 | } 10 | 11 | const tmpView = new THREE.Box2(); 12 | 13 | module.exports = class ZigZagScene extends THREE.Object3D { 14 | constructor(app) { 15 | super(); 16 | this.app = app; 17 | 18 | this.createPool(); 19 | } 20 | 21 | createPool() { 22 | const maxCapacity = 6; 23 | this.pool = newArray(maxCapacity).map(() => { 24 | const mesh = new ZigZag(this.app, { 25 | segments: RND.randomInt(100, 200) 26 | }); 27 | 28 | mesh.active = false; 29 | mesh.visible = false; 30 | this.add(mesh); 31 | 32 | return mesh; 33 | }); 34 | } 35 | 36 | clear() { 37 | this.pool.forEach(p => { 38 | p.visible = false; 39 | p.active = false; 40 | p.wasVisible = false; 41 | p.reset(); 42 | }); 43 | } 44 | 45 | start() { 46 | this.running = true; 47 | this.pool.forEach(() => this.next()); 48 | } 49 | 50 | onTrigger(event) { 51 | if (event === 'fadeOut') { 52 | this.running = false; 53 | this.pool.forEach(s => { 54 | s.transitionColor(presets.intro0.background); 55 | }); 56 | } else if (event === 'randomize') { 57 | // recreate pool to get new random zigzags 58 | this.clear(); 59 | this.start(); 60 | } else if (event === 'clear') { 61 | this.clear(); 62 | } else if (event === 'start') { 63 | this.start(); 64 | } 65 | } 66 | 67 | onPresetChanged(preset, oldPreset) { 68 | this.running = true; 69 | this.clear(); 70 | this.start(); 71 | } 72 | 73 | onPresetTransition(preset, oldPreset) { 74 | this.running = true; 75 | // Transition colors to new features 76 | this.pool.forEach(shape => { 77 | if (!shape.active) return; 78 | const { color } = pickColors(preset.colors); 79 | shape.transitionColor(color); 80 | }); 81 | 82 | this.trimCapacity(); 83 | this.start(); 84 | // TODO: Could animate out shapes here if capacity is less than current? 85 | } 86 | 87 | trimCapacity() { 88 | const active = this.pool.filter(p => p.active); 89 | const capacity = this.app.preset.zigZagCapacity; 90 | if (active.length <= capacity) return; // less than max 91 | const toKill = RND.shuffle(active).slice(capacity); 92 | toKill.forEach(k => k.animateOut()); 93 | } 94 | 95 | next() { 96 | if (!this.running) return; 97 | const { app, pool } = this; 98 | 99 | const getRandomPosition = () => { 100 | const edges = [ 101 | [new THREE.Vector2(-1, -1), new THREE.Vector2(1, -1)], 102 | [new THREE.Vector2(1, -1), new THREE.Vector2(1, 1)], 103 | [new THREE.Vector2(1, 1), new THREE.Vector2(-1, 1)], 104 | [new THREE.Vector2(-1, 1), new THREE.Vector2(-1, -1)] 105 | ]; 106 | 107 | const edgeIndex = RND.randomInt(edges.length); 108 | const edge = edges[edgeIndex]; 109 | const t = RND.randomFloat(0, 1); 110 | 111 | const vec = edge[0].clone().lerp(edge[1], t); 112 | vec.multiply(app.unitScale).multiplyScalar(RND.randomFloat(1.3, 1.5)); 113 | 114 | return vec; 115 | }; 116 | 117 | const findAvailableObject = () => { 118 | const activeCount = pool.filter(p => p.active).length; 119 | if (activeCount >= this.app.preset.zigZagCapacity) return; 120 | 121 | return RND.shuffle(pool).find(p => !p.active); 122 | }; 123 | 124 | const object = findAvailableObject(); 125 | if (!object) return; 126 | 127 | object.active = true; 128 | object.visible = true; 129 | object.wasVisible = false; 130 | 131 | const lineWidth = RND.randomFloat(0.02, 0.05) * 1; 132 | 133 | const scale = RND.randomFloat(0.2, 0.5); 134 | object.scale.setScalar(scale * app.targetScale); 135 | 136 | const position = getRandomPosition(); 137 | object.position.set(position.x, position.y, 0); 138 | 139 | const target = [RND.randomFloat(-3, 3), RND.randomFloat(-1, 1)]; 140 | const angle = Math.atan2(position.y - target[1], position.x - target[0]) + Math.PI / 2; 141 | object.rotation.z = angle; 142 | 143 | const delay = RND.randomFloat(0, 20); 144 | const speed = RND.randomFloat(0.75, 1.75); 145 | 146 | const { color } = pickColors(this.app.preset.colors); 147 | object.reset(); 148 | object.randomize({ color, lineWidth, delay, speed }); 149 | } 150 | 151 | update() { 152 | tmpView.copy(this.app.sceneBounds); 153 | tmpView.expandByScalar(1.05); // slight padding to avoid popping 154 | 155 | const position2d = new THREE.Vector2(); 156 | 157 | this.pool.forEach(p => { 158 | if (!p.active) return; 159 | 160 | position2d.x = p.position.x; 161 | position2d.y = p.position.y; 162 | 163 | const head = p.headPos 164 | .clone() 165 | .multiplyScalar(p.scale.x) 166 | .add(position2d) 167 | .rotateAround(position2d, p.rotation.z); 168 | 169 | const tail = p.tailPos 170 | .clone() 171 | .multiplyScalar(p.scale.x) 172 | .add(position2d) 173 | .rotateAround(position2d, p.rotation.z); 174 | 175 | const isOutside = !tmpView.containsPoint(head) && !tmpView.containsPoint(tail); 176 | p.isOutside = isOutside; 177 | 178 | if (isOutside) { 179 | if (p.wasVisible) { 180 | p.visible = false; 181 | p.active = false; 182 | this.next(); 183 | } 184 | } else { 185 | p.wasVisible = true; 186 | } 187 | }); 188 | } 189 | }; 190 | -------------------------------------------------------------------------------- /src/scene/presets.js: -------------------------------------------------------------------------------- 1 | const defaultIntroSettings = { 2 | foreground: '#FFFFFF', 3 | mode: 'intro', 4 | capacity: 30, 5 | initialCapacity: 30, 6 | zigZagCapacity: 0 7 | }; 8 | 9 | const generative = { 10 | mode: 'default', 11 | background: '#f3ecda', 12 | foreground: '#304061', 13 | capacity: 30, 14 | initialCapacity: 30, 15 | zigZagCapacity: 1, 16 | colors: [ 17 | { weight: 100, value: '#e10079' }, 18 | { weight: 100, value: '#6058c5' }, 19 | { weight: 100, value: '#ffc4bc' }, 20 | { weight: 50, value: '#dde4f2' }, 21 | { weight: 50, value: '#051add' }, 22 | { weight: 50, value: '#303f62' } 23 | ] 24 | // All colors equal: 25 | // colors: ['#313F61', '#DF1378', '#0C2AD9', '#FEC3BE', '#DDE4F0', '#7A899C'] 26 | }; 27 | 28 | module.exports = { 29 | default: { 30 | ...generative 31 | }, 32 | ambient: { 33 | mode: 'ambient', 34 | capacity: 10, 35 | initialCapacity: 10, 36 | zigZagCapacity: 0, 37 | background: '#313F61', 38 | foreground: '#ffffff', 39 | colors: ['#FFFFFF'] 40 | }, 41 | introIdle: { 42 | ...defaultIntroSettings, 43 | background: '#000', 44 | colors: ['#000'] 45 | }, 46 | intro0: { 47 | ...defaultIntroSettings, 48 | background: '#303f62', 49 | colors: [ 50 | { weight: 100, value: '#0C2AD9' }, 51 | { weight: 100, value: '#6058c5' }, 52 | { weight: 100, value: '#DF1378' }, 53 | { weight: 50, value: '#FEC3BE' }, 54 | { weight: 25, value: '#7A899C' }, 55 | ] 56 | }, 57 | intro1: { 58 | ...defaultIntroSettings, 59 | background: '#df1379', 60 | colors: ['#FFFFFF'] 61 | }, 62 | intro2: { 63 | ...defaultIntroSettings, 64 | background: '#605BC2', 65 | colors: ['#FFFFFF'] 66 | }, 67 | intro3: { 68 | ...defaultIntroSettings, 69 | background: '#314061', 70 | colors: ['#FFFFFF'] 71 | }, 72 | intro4: { 73 | ...defaultIntroSettings, 74 | background: '#0D2AD9', 75 | colors: ['#FFFFFF'] 76 | }, 77 | intro5: { 78 | ...defaultIntroSettings, 79 | background: '#605BC2', 80 | colors: ['#FFFFFF'] 81 | }, 82 | intro6: { 83 | ...generative, 84 | mode: 'intro' 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /src/shader/example-gradient.frag: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | uniform float frame; 3 | uniform float opacity; 4 | uniform vec3 color; 5 | uniform float randomOffset; 6 | 7 | varying vec2 vUv; 8 | varying vec2 vPosition; 9 | varying float vSize; 10 | 11 | #pragma glslify: gradient = require('./util/surface-gradient.glsl'); 12 | 13 | void main () { 14 | vec3 fragColor = color; 15 | fragColor = gradient(fragColor, vUv, frame, randomOffset); 16 | gl_FragColor = vec4(fragColor, opacity); 17 | } -------------------------------------------------------------------------------- /src/shader/shape.frag: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | uniform float frame; 3 | uniform float opacity; 4 | uniform vec3 color; 5 | uniform vec3 altColor; 6 | uniform float randomOffset; 7 | uniform sampler2D map; 8 | uniform sampler2D maskMap; 9 | 10 | uniform vec2 mapResolution; 11 | uniform vec2 maskMapResolution; 12 | uniform vec2 shapeResolution; 13 | uniform vec2 resolution; 14 | uniform float animate; 15 | uniform vec2 centroid; 16 | uniform vec2 mapOffset; 17 | uniform bool mapMask; 18 | uniform float mapScale; 19 | 20 | varying float vRandom; 21 | varying vec2 vUv; 22 | varying vec2 vPosition; 23 | varying float vSize; 24 | 25 | #pragma glslify: noise = require('glsl-noise/classic/3d'); 26 | #pragma glslify: backgroundUV = require('./util/background.glsl'); 27 | #pragma glslify: aastep = require('glsl-aastep'); 28 | 29 | void main () { 30 | gl_FragColor = vec4(color, opacity); 31 | 32 | float pattern; 33 | vec2 uvPos = vPosition; 34 | if (!mapMask) uvPos += centroid; 35 | vec2 patternUV = uvPos * mapScale; 36 | patternUV.y += time * 0.1 * randomOffset; 37 | patternUV.x += time * 0.05; 38 | patternUV += mapOffset; 39 | 40 | #if defined(HAS_TEXTURE_PATTERN) 41 | pattern = texture2D(map, patternUV).r; 42 | #elif defined(HAS_SHADER_PATTERN) 43 | // Here we could do custom shader patterns 44 | // The different types of shader patterns (stripe, whatever) 45 | // should be chosen with a uniform I guess? 46 | pattern = step(0.5, fract(patternUV.x * 6.0)); 47 | #endif 48 | 49 | 50 | #if defined(HAS_TEXTURE_PATTERN) || defined(HAS_SHADER_PATTERN) 51 | #if defined(HAS_FILL) 52 | gl_FragColor.rgb = mix(color, altColor, pattern); 53 | #else 54 | gl_FragColor.a *= pattern; 55 | #endif 56 | #endif 57 | 58 | vec2 bgUV = vUv * 2.0 - 1.0; 59 | float shapeAspect = shapeResolution.x / shapeResolution.y; 60 | if (shapeResolution.x > shapeResolution.y) { 61 | bgUV *= shapeAspect; 62 | } else { 63 | bgUV /= shapeAspect; 64 | } 65 | // float d = texture2D(maskMap, bgUV * 0.5 + 0.5).r; 66 | 67 | 68 | if (animate < 1.0) { 69 | vec2 maskUVPos = vPosition; 70 | maskUVPos += centroid; 71 | vec2 maskUV = maskUVPos * 1.25; 72 | maskUV.y += time * -0.1 * randomOffset; 73 | maskUV.x += time * -0.05; 74 | maskUV += mapOffset; 75 | 76 | // float centDist = (distance(vPosition, centroid)); 77 | float n = 0.0; 78 | n += noise(vec3(vUv * mix(1.0, 2.0, animate), randomOffset)); 79 | n = n * 0.5 + 0.5; 80 | float sdf = texture2D(maskMap, maskUV).a * n; 81 | float d = aastep(0.5 * (1.0 - animate), sdf); 82 | gl_FragColor.a *= d; 83 | 84 | 85 | // #ifdef NOISE_ANIMATION 86 | // float n = 0.0; 87 | // n += noise(vec3(vPosition * mapScale * mix(4.0, 2.0, animate), randomOffset)); 88 | // float anim = aastep(1.0 - animate, n * 0.5 + 0.5); 89 | // gl_FragColor.a *= anim; 90 | // #endif 91 | } 92 | } -------------------------------------------------------------------------------- /src/shader/shape.vert: -------------------------------------------------------------------------------- 1 | attribute float random; 2 | 3 | uniform float frame; 4 | uniform float time; 5 | uniform float animate; 6 | uniform vec2 centroid; 7 | uniform vec2 direction; 8 | uniform vec3 audioSignal; 9 | uniform float randomOffset; 10 | 11 | varying vec2 vPosition; 12 | varying float vSize; 13 | varying vec2 vUv; 14 | varying float vRandom; 15 | 16 | #pragma glslify: motion = require('./util/motion.glsl'); 17 | #pragma glslify: noise = require('glsl-noise/simplex/3d'); 18 | 19 | void main () { 20 | vec2 normal = normalize(position.xy); 21 | vec2 pos = position.xy + motion(position.xy, normal, time, randomOffset); 22 | 23 | // Scaling effect: this needs to be re-considered into something more interesting 24 | // pos = mix(centroid, pos, audioSignal); 25 | vec2 dir = normalize(pos - centroid); 26 | 27 | #ifdef HAS_AUDIO 28 | pos += dir * 0.25 * audioSignal.x; 29 | pos += dir * 0.15 * (noise(vec3(position.xy * 1.0, time * 0.1)) * 0.5 + 0.5) * audioSignal.y; 30 | #endif 31 | 32 | vec4 modelViewPos = modelViewMatrix * vec4(pos.xy, 0.0, 1.0); 33 | gl_Position = projectionMatrix * modelViewPos; 34 | vUv = uv; 35 | vRandom = random; 36 | vPosition = pos; 37 | } -------------------------------------------------------------------------------- /src/shader/util/background.glsl: -------------------------------------------------------------------------------- 1 | vec2 backgroundUV (vec2 uv, vec2 resolution, vec2 texResolution) { 2 | float tAspect = texResolution.x / texResolution.y; 3 | float pAspect = resolution.x / resolution.y; 4 | float pwidth = resolution.x; 5 | float pheight = resolution.y; 6 | 7 | float width = 0.0; 8 | float height = 0.0; 9 | if (tAspect > pAspect) { 10 | height = pheight; 11 | width = height * tAspect; 12 | } else { 13 | width = pwidth; 14 | height = width / tAspect; 15 | } 16 | float x = (pwidth - width) / 2.0; 17 | float y = (pheight - height) / 2.0; 18 | vec2 nUv = uv; 19 | nUv -= vec2(x, y) / resolution; 20 | nUv /= vec2(width, height) / resolution; 21 | return nUv; 22 | } 23 | 24 | #pragma glslify: export(backgroundUV) -------------------------------------------------------------------------------- /src/shader/util/motion.glsl: -------------------------------------------------------------------------------- 1 | 2 | #pragma glslify: PI = require('glsl-pi'); 3 | #pragma glslify: noise = require('glsl-noise/simplex/4d'); 4 | 5 | vec2 motion (vec2 position, vec2 normal, float time, float randomOffset) { 6 | vec2 ret = vec2(0.0); 7 | float amplitudeScale = 2.0; 8 | 9 | // high freq first 10 | float frequency = mix(250.0, 4500.0, randomOffset); 11 | float n = 0.0; 12 | float amplitude = 0.0; 13 | 14 | #ifdef HIGH_FREQ_MOTION 15 | n = noise(vec4(position.xy * frequency, randomOffset, randomOffset + time)); 16 | amplitude = mix(0.0075, 0.0075 * 2.0, randomOffset); 17 | ret += normal * n * amplitude * amplitudeScale; 18 | #endif 19 | 20 | // now low freq 21 | float timeScaled = mix(0.5, 1.0, randomOffset) * time; 22 | frequency = mix(0.1, 2.0, randomOffset); 23 | n = noise(vec4(position.xy * frequency, randomOffset, randomOffset + timeScaled)); 24 | amplitude = 0.025; 25 | ret += normal * n * amplitude * amplitudeScale; 26 | 27 | return ret; 28 | } 29 | 30 | #pragma glslify: export(motion); -------------------------------------------------------------------------------- /src/shader/util/surface-gradient.glsl: -------------------------------------------------------------------------------- 1 | #pragma glslify: random = require('glsl-random'); 2 | #pragma glslify: PI = require('glsl-pi'); 3 | 4 | vec2 rotateAround (vec2 vec, vec2 center, float angle) { 5 | float c = cos( angle ); 6 | float s = sin( angle ); 7 | 8 | float x = vec.x - center.x; 9 | float y = vec.y - center.y; 10 | 11 | vec2 outVec; 12 | outVec.x = x * c - y * s + center.x; 13 | outVec.y = x * s + y * c + center.y; 14 | return outVec; 15 | } 16 | 17 | float linearGradient (vec2 uv, vec2 start, vec2 end) { 18 | vec2 gradientDirection = end - start; 19 | float gradientLenSq = dot(gradientDirection, gradientDirection); 20 | vec2 relCoords = uv - start; 21 | float t = dot(relCoords, gradientDirection); 22 | if (gradientLenSq != 0.0) t /= gradientLenSq; 23 | return t; 24 | } 25 | 26 | vec3 gradientNoise (vec3 fragColor, vec2 uv, float time, float randomOffset) { 27 | vec2 vRot = rotateAround(uv - 0.5, vec2(0.0), time); 28 | 29 | float angle = randomOffset * PI * 2.0 + time * 0.05; 30 | float radius = 0.5; 31 | vec2 direction = vec2(cos(angle), sin(angle)); 32 | vec2 start = 0.5 + direction * -radius; 33 | vec2 end = 0.5 + direction * radius; 34 | float gradient = linearGradient(uv, start, end); 35 | 36 | // vec2 vNorm = vRot - 0.5; 37 | float center = length(uv - 0.5); 38 | float rnd = random(vec2(gl_FragCoord.x + randomOffset + time * 0.0009, gl_FragCoord.y)); 39 | return mix(fragColor, fragColor * 1.2, gradient * step(0.2, rnd)); 40 | } 41 | 42 | #pragma glslify: export(gradientNoise); -------------------------------------------------------------------------------- /src/util/circleIntersectBox.js: -------------------------------------------------------------------------------- 1 | module.exports = function (circle, bbox) { 2 | const rx = bbox.min.x; 3 | const ry = bbox.min.y; 4 | const rw = bbox.max.x - bbox.min.x; 5 | const rh = bbox.max.y - bbox.min.y; 6 | 7 | const cx = circle.center.x; 8 | const cy = circle.center.y; 9 | const r = circle.radius; 10 | 11 | const dx = Math.abs(cx - rx - rw / 2); 12 | const xDist = rw / 2 + r; 13 | if (dx > xDist) return false; 14 | const dy = Math.abs(cy - ry - rh / 2); 15 | const yDist = rh / 2 + r; 16 | if (dy > yDist) return false; 17 | if (dx <= (rw / 2) || dy <= (rh / 2)) return true; 18 | const xCornerDist = dx - rw / 2; 19 | const yCornerDist = dy - rh / 2; 20 | const xCornerDistSq = xCornerDist * xCornerDist; 21 | const yCornerDistSq = yCornerDist * yCornerDist; 22 | const maxCornerDistSq = r * r; 23 | return xCornerDistSq + yCornerDistSq <= maxCornerDistSq; 24 | }; 25 | -------------------------------------------------------------------------------- /src/util/colliderCircle.js: -------------------------------------------------------------------------------- 1 | const defined = require('defined'); 2 | const query = require('../util/query'); 3 | const debug = query.debug; 4 | let geom; 5 | 6 | const circleGeom = () => { 7 | if (geom) return geom; 8 | geom = new THREE.CircleGeometry(1, 64); 9 | return geom; 10 | }; 11 | 12 | module.exports = function (opt = {}) { 13 | const obj = { 14 | center: opt.center || new THREE.Vector2(), 15 | radius: defined(opt.radius, 1), 16 | update, 17 | sphere: new THREE.Sphere(), 18 | mesh: null 19 | }; 20 | 21 | obj.getWorldSphere = (target) => { 22 | obj.sphere.center.set( 23 | obj.center.x, 24 | obj.center.y, 25 | 0 26 | ); 27 | obj.sphere.radius = obj.radius; 28 | target.updateMatrixWorld(); 29 | return obj.sphere.applyMatrix4(target.matrixWorld); 30 | }; 31 | 32 | if (debug) { 33 | obj.mesh = new THREE.Mesh( 34 | circleGeom(), 35 | new THREE.MeshBasicMaterial({ 36 | depthTest: false, 37 | depthWrite: false, 38 | transparent: true, 39 | wireframe: true, 40 | side: THREE.DoubleSide, 41 | color: 'cyan' 42 | }) 43 | ); 44 | } 45 | 46 | return obj; 47 | 48 | function update () { 49 | if (obj.mesh) { 50 | obj.mesh.position.x = obj.center.x; 51 | obj.mesh.position.y = obj.center.y; 52 | obj.mesh.scale.setScalar(obj.radius); 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/util/createAudio.js: -------------------------------------------------------------------------------- 1 | const webAudioPlayer = require('web-audio-player'); 2 | const analyserAverage = require('analyser-frequency-average'); 3 | const noop = () => {}; 4 | const smoothstep = require('smoothstep'); 5 | const CircularBuffer = require('circular-buffer'); 6 | const ease = require('eases/quad-in-out'); 7 | const clamp = require('clamp'); 8 | 9 | const audioAverageCount = 15; 10 | const frequencyBins = [ 11 | { start: 70, end: 150 }, 12 | { start: 4000, end: 6000 }, 13 | { start: 6000, end: 10000 } 14 | ]; 15 | 16 | const BEAT_LEAD_TIME = 0.15; 17 | const BEAT_TIMES = [ 18 | { time: 3.916 }, 19 | { time: 7.721 }, 20 | { time: 11.582 }, 21 | { time: 15.442 }, 22 | { time: 19.248 }, 23 | { time: 23.108 }, 24 | { time: 26.913 }, 25 | { time: 30.774 }, 26 | { time: 34.634 }, 27 | { time: 38.440, major: true }, 28 | ]; 29 | 30 | 31 | module.exports = function () { 32 | const context = new (window.AudioContext || window.webkitAudioContext)(); 33 | const src = 'assets/audio/intro-short.mp3'; 34 | const player = webAudioPlayer(src, { 35 | context, 36 | buffer: false 37 | }); 38 | const analyser = context.createAnalyser(); 39 | const biquadFilter = context.createBiquadFilter(); 40 | biquadFilter.type = 'lowpass'; 41 | // biquadFilter.Q.setValueAtTime(10, context.currentTime); 42 | biquadFilter.frequency.setValueAtTime(500, context.currentTime); 43 | // biquadFilter.gain.setValueAtTime(1, context.currentTime) 44 | 45 | biquadFilter.connect(analyser); 46 | 47 | const lowpass = context.createBiquadFilter(); 48 | lowpass.type = 'lowpass'; 49 | lowpass.frequency.setValueAtTime(200, context.currentTime); 50 | lowpass.connect(context.destination); 51 | 52 | player.node.connect(lowpass); 53 | player.node.connect(biquadFilter); 54 | 55 | const freqs = new Uint8Array(analyser.fftSize); 56 | const bins = frequencyBins; 57 | const velFactor = 0.05; 58 | const velFriction = 0.5; 59 | const signalsVel = bins.map(() => 0); 60 | const signalsRaw = bins.map(() => 0); 61 | const signalsAveraged = bins.map(() => 0); 62 | const averages = bins.map(() => new CircularBuffer(audioAverageCount)); 63 | 64 | player.BEAT_TIMES = BEAT_TIMES; 65 | player.BEAT_LEAD_TIME = BEAT_LEAD_TIME; 66 | 67 | player.fadeIn = () => { 68 | lowpass.frequency.setTargetAtTime(100, context.currentTime, 0.5); 69 | lowpass.frequency.setTargetAtTime(100, context.currentTime + 0.5, 0.5); 70 | lowpass.frequency.exponentialRampToValueAtTime(20000, context.currentTime + 3); 71 | }; 72 | 73 | player.fadeOut = (cb = noop) => { 74 | // lowpass.frequency.setTargetAtTime(100, context.currentTime, 0.5); 75 | // lowpass.frequency.setTargetAtTime(100, context.currentTime + 0.5, 0.5); 76 | lowpass.frequency.exponentialRampToValueAtTime(100, context.currentTime + 10); 77 | player.node.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 10); 78 | setTimeout(() => { 79 | cb(); 80 | }, 5000); 81 | }; 82 | 83 | player.updateFrequencies = () => { 84 | analyser.getByteTimeDomainData(freqs); 85 | 86 | bins.forEach((bin, i) => { 87 | let signal = analyserAverage(analyser, freqs, bin.start, bin.end); 88 | signal = smoothstep(0.2, 0.7, signal); 89 | 90 | signalsVel[i] += signal * velFactor; 91 | signalsVel[i] = clamp(signalsVel[i], 0, 1); 92 | signalsVel[i] *= velFriction; 93 | averages[i].enq(signal); 94 | signalsRaw[i] = signal; 95 | }); 96 | 97 | averages.forEach((avg, i) => { 98 | const len = avg.size(); 99 | let sum = 0; 100 | for (let i = 0; i < len; i++) { 101 | sum += avg.get(i); 102 | } 103 | sum /= len; 104 | // signalsAveraged[i] = (sum); 105 | signalsAveraged[i] = ease(sum); 106 | }); 107 | 108 | // console.log(signalsVel[0]) 109 | return signalsAveraged; 110 | }; 111 | return player; 112 | }; 113 | -------------------------------------------------------------------------------- /src/util/flattenVertices.js: -------------------------------------------------------------------------------- 1 | const getDimensions = require('./getDimensions'); 2 | 3 | module.exports = function flattenVertices (points, outputArray) { 4 | if (!points || points.length === 0) return []; 5 | const dimensions = getDimensions(points[0]); 6 | const output = outputArray || new Array(points.length * dimensions); 7 | for (let i = 0; i < points.length; i++) { 8 | const point = points[i]; 9 | point.toArray(output, i * dimensions); 10 | } 11 | return output; 12 | }; 13 | -------------------------------------------------------------------------------- /src/util/getDimensions.js: -------------------------------------------------------------------------------- 1 | module.exports = function getDimensions (point) { 2 | if (point.isVector4) return 4; 3 | if (point.isVector3) return 3; 4 | if (point.isVector2) return 2; 5 | return point.length; 6 | }; 7 | -------------------------------------------------------------------------------- /src/util/introText.js: -------------------------------------------------------------------------------- 1 | const anime = require('animejs'); 2 | const lerp = require('lerp'); 3 | 4 | module.exports = function(api, params = {}) { 5 | const container = document.querySelector('.canvas-text-container'); 6 | const textEl = document.querySelector('.canvas-text'); 7 | const bigTextEl = document.querySelector('.canvas-big-text'); 8 | 9 | const texts = [ 10 | { preset: 'intro0', text: 'SinnerSchrader, Greenkeeper, Cobot & The AMP Project present' }, 11 | { preset: 'intro1', text: 'A JSConf International production' }, 12 | { preset: 'intro2', text: 'In cooperation with wwwtf.berlin' }, 13 | { preset: 'intro3', text: 'And supported by the Chrome team', shorter: true }, 14 | { preset: 'intro4', text: '{ live : js } Network and 2xAA' }, 15 | { preset: 'intro5', text: 'Nested Loops' }, 16 | { preset: 'intro6', text: 'Curated by Feli, Holger, Jan, Malte, Megan & Simone' }, 17 | { text: 'Welcome to', bigText: 'JSConf EU 2018' } 18 | ]; 19 | 20 | let index = 0; 21 | 22 | function removeChildren(node) { 23 | while (node.firstChild) { 24 | node.removeChild(node.firstChild); 25 | } 26 | } 27 | 28 | function buildText(textEl, text, bigTextEl, bigText) { 29 | const addTextToEl = (text, el) => { 30 | const chunks = text.split(' ').map(str => { 31 | const span = document.createElement('div'); 32 | span.className = 'text-chunk'; 33 | span.textContent = `${str} `; 34 | return { element: span, text: str }; 35 | }); 36 | 37 | chunks.forEach(c => el.appendChild(c.element)); 38 | 39 | return chunks; 40 | }; 41 | 42 | let chunks = addTextToEl(text, textEl); 43 | 44 | if (bigText && bigTextEl) { 45 | chunks = chunks.concat(addTextToEl(bigText, bigTextEl)); 46 | } 47 | 48 | return chunks; 49 | } 50 | 51 | function next(opt = {}) { 52 | const { delay = 0 } = opt; 53 | const item = texts[index]; 54 | const curIndex = index; 55 | const nextItem = index < texts.length - 1 ? texts[index + 1] : null; 56 | // textEl.textContent = item.text; 57 | // textEl.style.opacity = '0'; 58 | 59 | if (item.preset) { 60 | textEl.style.color = api.getPresets()[item.preset].foreground; 61 | bigTextEl.style.color = api.getPresets()[item.preset].foreground; 62 | } 63 | 64 | removeChildren(textEl); 65 | removeChildren(bigTextEl); 66 | 67 | const chunks = buildText(textEl, item.text, bigTextEl, item.bigText); 68 | 69 | const spans = chunks.map(p => p.element); 70 | const updateClip = (el, val) => { 71 | // val = 1 - val; 72 | // el.style.clipPath = `inset(0 ${Math.min(100, Math.round(val * 100))}% 0 0)`; 73 | }; 74 | spans.forEach(s => { 75 | s.style.opacity = '0'; 76 | updateClip(s, 0); 77 | // s.style.transform = `translateY(-40px)`; 78 | }); 79 | 80 | textEl.style.width = item.shorter ? '13vw' : '15vw'; 81 | 82 | const stagger = 20; 83 | const delayFn = (el, i) => { 84 | return delay + i * stagger; 85 | }; 86 | const delayFnOut = (el, i) => { 87 | return 0 + i * stagger; 88 | }; 89 | const easeAnimIn = [0.08, 1.41, 0.55, 1.01]; 90 | anime 91 | .timeline() 92 | .add({ 93 | targets: spans, 94 | opacity: { 95 | value: [0, 1], 96 | delay: delayFn, 97 | duration: 1000, 98 | easing: 'easeOutQuad' 99 | }, 100 | translateX: { 101 | value: [-15, 0], 102 | delay: delayFn, 103 | duration: 3000, 104 | easing: easeAnimIn 105 | }, 106 | // skewY: { 107 | // value: [-5, 0], 108 | // delay: delayFn, 109 | // duration: 1000, 110 | // easing: 'easeOutExpo' 111 | // }, 112 | update: ev => { 113 | if (spans.length <= 0) return; 114 | // spans.forEach(span => { 115 | // const val = parseFloat(span.style.opacity); 116 | // updateClip(span, val); 117 | // }); 118 | } 119 | }) 120 | .add({ 121 | targets: spans, 122 | opacity: { 123 | value: 0, 124 | delay: delayFnOut, 125 | duration: 2000, 126 | easing: 'easeInExpo' 127 | }, 128 | translateX: { 129 | value: 15, 130 | delay: delayFnOut, 131 | duration: 2000, 132 | easing: 'easeInExpo' 133 | }, 134 | begin: () => { 135 | if (nextItem) { 136 | setTimeout(() => { 137 | if (nextItem.preset) api.transitionToPreset(nextItem.preset); 138 | api.triggerIntroSwap({ index: curIndex, items: texts }); 139 | }, 1750); 140 | } 141 | } 142 | }) 143 | .finished.then(() => { 144 | index++; 145 | if (index > texts.length - 1) { 146 | api.onFadeOutIntro(); 147 | } else { 148 | next({ delay: 1000 }); 149 | } 150 | }); 151 | } 152 | 153 | index = 0; 154 | 155 | next(params); 156 | 157 | // var basicTimeline = anime.timeline(); 158 | // basicTimeline 159 | // .add({ 160 | // targets: '#basicTimeline .square.el', 161 | // translateX: 250, 162 | // easing: 'easeOutExpo' 163 | // }) 164 | // .add({ 165 | // targets: '#basicTimeline .circle.el', 166 | // translateX: 250, 167 | // easing: 'easeOutExpo' 168 | // }) 169 | // .add({ 170 | // targets: '#basicTimeline .triangle.el', 171 | // translateX: 250, 172 | // easing: 'easeOutExpo' 173 | // }); 174 | }; 175 | -------------------------------------------------------------------------------- /src/util/loadAssets.js: -------------------------------------------------------------------------------- 1 | const load = require('load-asset'); 2 | const createAudio = require('./createAudio'); 3 | 4 | module.exports = function (opt = {}) { 5 | const renderer = opt.renderer; 6 | 7 | const baseSettings = {}; 8 | 9 | const textureResolution = 512; // 512 or 1024 10 | const tileFiles = ['bigdot', /*'contours',*/ 'funkygerms', 'leppard', 'littlesticks', 'smalldot', 'worms'].map(f => { 11 | return { 12 | url: `assets/image/tile/${f}_${textureResolution}_.png`, 13 | type: loadTextureType, 14 | settings: { 15 | minFilter: THREE.LinearFilter, 16 | wrapS: THREE.RepeatWrapping, 17 | wrapT: THREE.RepeatWrapping, 18 | generateMipmaps: false 19 | } 20 | }; 21 | }); 22 | 23 | const maskFiles = ['e'].map(f => { 24 | return { 25 | url: `assets/image/mask/${f}.png`, 26 | type: loadTextureType, 27 | settings: { 28 | minFilter: THREE.LinearFilter, 29 | wrapS: THREE.RepeatWrapping, 30 | wrapT: THREE.RepeatWrapping, 31 | generateMipmaps: false 32 | } 33 | }; 34 | }); 35 | 36 | return load.any( 37 | { 38 | // audioFile: { url: 'assets/audio/intro-short.mp3' }, 39 | masks: load.any(maskFiles, err), 40 | tiles: load.any(tileFiles, err) 41 | // Can add other named assets here 42 | // e.g. 43 | // image: 'foo.png', 44 | // texture: { url: 'blah.png', type: loadTextureType } 45 | }, 46 | ev => { 47 | console.log(`[canvas] Loading Progress: ${ev.progress}`); 48 | } 49 | ); 50 | 51 | function err (ev) { 52 | if (ev.error) { 53 | } 54 | } 55 | 56 | function loadTextureType (ev) { 57 | return load({ ...ev, type: 'image' }).then(image => { 58 | const texture = new THREE.Texture(image); 59 | Object.assign(texture, ev.settings || {}); 60 | texture.needsUpdate = true; 61 | if (renderer) renderer.setTexture2D(texture, 0); 62 | return texture; 63 | }); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/util/pickColors.js: -------------------------------------------------------------------------------- 1 | const RND = require('../util/random'); 2 | const clamp = require('clamp'); 3 | 4 | const getColor = colorStyle => { 5 | const color = new THREE.Color().set(colorStyle); 6 | const hOff = RND.randomFloat(-1, 1) * (2 / 360); 7 | const sOff = RND.randomFloat(-1, 1) * 0.01; 8 | const lOff = RND.randomFloat(-1, 1) * 0.025; 9 | color.offsetHSL(hOff, sOff, lOff); 10 | color.r = clamp(color.r, 0, 1); 11 | color.g = clamp(color.g, 0, 1); 12 | color.b = clamp(color.b, 0, 1); 13 | return color; 14 | }; 15 | 16 | const isColor = color => { 17 | return typeof color === 'string' || (color && color.isColor); 18 | }; 19 | 20 | module.exports = function (colors, entityType) { 21 | let palette; 22 | 23 | const hasWeights = colors.some(c => !isColor(c)); 24 | let filteredColors = colors; 25 | if (hasWeights && entityType && colors.some(c => typeof c[entityType] !== 'undefined')) { 26 | filteredColors = colors.filter(c => c[entityType]); 27 | } 28 | 29 | palette = hasWeights 30 | ? RND.weighted(filteredColors) 31 | : filteredColors[RND.randomInt(filteredColors.length)]; 32 | 33 | const color = getColor(palette); 34 | const altPalette = RND.shuffle(filteredColors).find(c => c !== palette); 35 | const altColor = getColor(altPalette); 36 | return { color, altColor }; 37 | } -------------------------------------------------------------------------------- /src/util/polyline.js: -------------------------------------------------------------------------------- 1 | const tmp = new THREE.Vector2(0, 0); 2 | 3 | module.exports.resampleLineBySpacing = function (points, spacing = 1 , closed = false) { 4 | if (spacing <= 0) { 5 | throw new Error('Spacing must be positive and larger than 0'); 6 | } 7 | let totalLength = 0; 8 | let curStep = 0; 9 | let lastPosition = points.length - 1; 10 | if (closed) { 11 | lastPosition++; 12 | } 13 | const result = []; 14 | for (let i = 0; i < lastPosition; i++) { 15 | const repeatNext = i === points.length - 1; 16 | const cur = points[i]; 17 | const next = repeatNext ? points[0] : points[i + 1]; 18 | const diff = tmp.copy(next).sub(cur); 19 | 20 | let curSegmentLength = diff.length(); 21 | totalLength += curSegmentLength; 22 | 23 | while (curStep * spacing <= totalLength) { 24 | let curSample = curStep * spacing; 25 | let curLength = curSample - (totalLength - curSegmentLength); 26 | let relativeSample = curLength / curSegmentLength; 27 | result.push(cur.clone().lerp(next, relativeSample)); 28 | curStep++; 29 | } 30 | } 31 | return result; 32 | }; 33 | 34 | module.exports.getPolylinePerimeter = function (points, closed = false) { 35 | let perimeter = 0; 36 | let lastPosition = points.length - 1; 37 | for (let i = 0; i < lastPosition; i++) { 38 | perimeter += tmp.copy(points[i]).distanceTo(points[i + 1]); 39 | } 40 | if (closed && points.length > 1) { 41 | perimeter += tmp.copy(points[points.length - 1]).distanceTo(points[0]); 42 | } 43 | return perimeter; 44 | }; 45 | 46 | module.exports.resampleLineByCount = function (points, count = 1 , closed = false) { 47 | if (count <= 0) return []; 48 | const perimeter = module.exports.getPolylinePerimeter(points, closed); 49 | return module.exports.resampleLineBySpacing(points, perimeter / count, closed); 50 | }; 51 | -------------------------------------------------------------------------------- /src/util/query.js: -------------------------------------------------------------------------------- 1 | // an object holding all the parsed query parameters 2 | // (tries to parse them as numbers/boolean) 3 | const qs = require('query-string'); 4 | 5 | function parseOptions () { 6 | if (typeof window === 'undefined') return {}; 7 | const parsed = qs.parse(window.location.search); 8 | Object.keys(parsed).forEach(key => { 9 | if (parsed[key] === null) parsed[key] = true; 10 | if (parsed[key] === 'false') parsed[key] = false; 11 | if (parsed[key] === 'true') parsed[key] = true; 12 | if (isNumber(parsed[key])) { 13 | parsed[key] = Number(parsed[key]); 14 | } 15 | }); 16 | return parsed; 17 | } 18 | 19 | function isNumber (x) { 20 | if (typeof x === 'number') return true; 21 | if (/^0x[0-9a-f]+$/i.test(x)) return true; 22 | return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x); 23 | } 24 | 25 | module.exports = parseOptions(); 26 | -------------------------------------------------------------------------------- /src/util/random.js: -------------------------------------------------------------------------------- 1 | // a utility for random number generation 2 | const seedRandom = require('seed-random'); 3 | const SimplexNoise = require('simplex-noise'); 4 | 5 | class Rand { 6 | constructor (defaultSeed = null, opt = {}) { 7 | this.defaultRandom = Math.random; 8 | this.quiet = opt.quiet; 9 | this.setSeed(defaultSeed); 10 | } 11 | 12 | setSeed (seed) { 13 | if (typeof seed === 'number' || typeof seed === 'string') { 14 | if (!this.quiet) console.log('Random Seed:', seed); 15 | this.seed = String(seed); 16 | this.random = seedRandom(this.seed); 17 | } else { 18 | this.seed = null; 19 | this.random = this.defaultRandom; 20 | } 21 | this.simplex = new SimplexNoise(this.random); 22 | } 23 | 24 | gauss () { 25 | return Math.sqrt(-2.0 * Math.log(this.random())) * Math.cos(2.0 * Math.PI * this.random()); 26 | } 27 | 28 | getSeed () { 29 | return this.seed; 30 | } 31 | 32 | noise2D (x, y) { 33 | return this.simplex.noise2D(x, y); 34 | } 35 | 36 | noise3D (x, y, z) { 37 | return this.simplex.noise3D(x, y, z); 38 | } 39 | 40 | noise4D (x, y, z, w) { 41 | return this.simplex.noise4D(x, y, z, w); 42 | } 43 | 44 | randomSign () { 45 | return this.randomBoolean() ? 1 : -1; 46 | } 47 | 48 | randomBoolean () { 49 | return this.random() > 0.5; 50 | } 51 | 52 | randomFloat (min, max) { 53 | if (max === undefined) { 54 | max = min; 55 | min = 0; 56 | } 57 | 58 | if (typeof min !== 'number' || typeof max !== 'number') { 59 | throw new TypeError('Expected all arguments to be numbers'); 60 | } 61 | 62 | return this.random() * (max - min) + min; 63 | } 64 | 65 | randomInt (min, max) { 66 | if (max === undefined) { 67 | max = min; 68 | min = 0; 69 | } 70 | 71 | if (typeof min !== 'number' || typeof max !== 'number') { 72 | throw new TypeError('Expected all arguments to be numbers'); 73 | } 74 | 75 | return Math.floor(this.randomFloat(min, max)); 76 | } 77 | 78 | shuffle (arr) { 79 | if (!Array.isArray(arr)) { 80 | throw new TypeError('Expected Array, got ' + typeof arr); 81 | } 82 | 83 | var rand; 84 | var tmp; 85 | var len = arr.length; 86 | var ret = arr.slice(); 87 | while (len) { 88 | rand = Math.floor(this.random() * len--); 89 | tmp = ret[len]; 90 | ret[len] = ret[rand]; 91 | ret[rand] = tmp; 92 | } 93 | return ret; 94 | } 95 | 96 | randomCircle (out, scale = 1) { 97 | var r = this.random() * 2.0 * Math.PI; 98 | out[0] = Math.cos(r) * scale; 99 | out[1] = Math.sin(r) * scale; 100 | return out; 101 | } 102 | 103 | randomSphere (out, scale = 1) { 104 | var r = this.random() * 2.0 * Math.PI; 105 | var z = (this.random() * 2.0) - 1.0; 106 | var zScale = Math.sqrt(1.0 - z * z) * scale; 107 | out[0] = Math.cos(r) * zScale; 108 | out[1] = Math.sin(r) * zScale; 109 | out[2] = z * scale; 110 | return out; 111 | } 112 | 113 | randomHemisphere (out, scale = 1) { 114 | var r = this.random() * 1.0 * Math.PI; 115 | var z = (this.random() * 2.0) - 1.0; 116 | var zScale = Math.sqrt(1.0 - z * z) * scale; 117 | out[0] = Math.cos(r) * zScale; 118 | out[1] = Math.sin(r) * zScale; 119 | out[2] = z * scale; 120 | return out; 121 | } 122 | 123 | randomQuaternion (out) { 124 | const u1 = this.random(); 125 | const u2 = this.random(); 126 | const u3 = this.random(); 127 | 128 | const sq1 = Math.sqrt(1 - u1); 129 | const sq2 = Math.sqrt(u1); 130 | 131 | const theta1 = Math.PI * 2 * u2; 132 | const theta2 = Math.PI * 2 * u3; 133 | 134 | const x = Math.sin(theta1) * sq1; 135 | const y = Math.cos(theta1) * sq1; 136 | const z = Math.sin(theta2) * sq2; 137 | const w = Math.cos(theta2) * sq2; 138 | out[0] = x; 139 | out[1] = y; 140 | out[2] = z; 141 | out[3] = w; 142 | return out; 143 | } 144 | 145 | weighted (set) { 146 | if (set.length === 0) return null; 147 | return set[this.weightedIndex(set)].value; 148 | } 149 | 150 | weightedIndex (set) { 151 | if (set.length === 0) return -1; 152 | return this.weightedRandom(set.map(s => s.weight)); 153 | } 154 | 155 | weightedRandom (weights) { 156 | if (weights.length === 0) return -1; 157 | let totalWeight = 0; 158 | 159 | for (let i = 0; i < weights.length; i++) { 160 | totalWeight += weights[i]; 161 | } 162 | 163 | if (totalWeight <= 0) throw new Error('Weights must sum to > 0'); 164 | 165 | let random = this.random() * totalWeight; 166 | for (let i = 0; i < weights.length; i++) { 167 | if (random < weights[i]) { 168 | return i; 169 | } 170 | random -= weights[i]; 171 | } 172 | return 0; 173 | } 174 | } 175 | 176 | const initialSeed = getRandomSeed() 177 | // const initialSeed = '13'; // '42785'; // '86475' // 31144 178 | module.exports = new Rand(initialSeed); 179 | //8 180 | module.exports.createInstance = (seed, opt) => new Rand(seed, opt); 181 | module.exports.getRandomSeed = getRandomSeed; 182 | 183 | function getRandomSeed () { 184 | const seed = String(Math.floor(Math.random() * 100000)); 185 | return seed; 186 | } 187 | -------------------------------------------------------------------------------- /src/util/strokePolygon.js: -------------------------------------------------------------------------------- 1 | const PolygonOffset = require('polygon-offset'); 2 | const stroke = require('extrude-polyline'); 3 | 4 | module.exports = function (vectors, opts = {}) { 5 | const { thickness = 0.05 } = opts; 6 | const points = vectors.map(p => p.toArray()); 7 | if (!vectors[0].equals(vectors[vectors.length - 1])) { 8 | points.push(points[0]); 9 | } 10 | return stroke({ 11 | thickness, 12 | cap: 'square', 13 | join: 'bevel', 14 | miterLimit: 10 15 | }).build(points); 16 | // const offset = new PolygonOffset(); 17 | // const stroke = offset.data(p).offsetLine(0.5); 18 | // if (stroke.length !== 1) { 19 | // return false; 20 | // } 21 | // const outline = stroke[0]; 22 | 23 | } 24 | 25 | require('extrude-polyline')({ 26 | thickness: 20, 27 | cap: 'square', 28 | join: 'bevel', 29 | miterLimit: 10 30 | }) 31 | --------------------------------------------------------------------------------