├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── app ├── app.js ├── base.css ├── decorators │ ├── FullScreenInBackground.js │ ├── HandleCameraOrbit.js │ └── PostProcessing.js ├── demos │ ├── demo2 │ │ ├── index.html │ │ ├── index.js │ │ └── style.styl │ ├── demo3 │ │ ├── index.html │ │ ├── index.js │ │ └── style.styl │ ├── demo4 │ │ ├── index.html │ │ ├── index.js │ │ └── style.styl │ ├── demo5 │ │ ├── index.html │ │ ├── index.js │ │ └── style.styl │ └── index │ │ ├── index.html │ │ ├── index.js │ │ └── style.styl ├── objects │ ├── AnimatedMeshLine.js │ ├── AnimatedText3D.js │ ├── LineGenerator.js │ └── Stars.js └── utils │ ├── engine.js │ ├── fontFile.js │ ├── getRandomFloat.js │ ├── getRandomInt.js │ ├── getRandomItem.js │ └── hasTouch.js ├── article.html ├── config ├── webpack.commun.js ├── webpack.dev.js └── webpack.prod.js ├── favicon.ico ├── package-lock.json ├── package.json └── previews ├── preview.gif ├── preview.png ├── preview2.gif ├── preview3.gif ├── preview4.gif ├── preview5.gif ├── preview_all.gif └── preview_sphere.gif /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "useBuiltIns": "entry", 5 | "targets": { 6 | "browsers": [ 7 | "last 2 versions", 8 | "IE >= 9" 9 | ] 10 | } 11 | }] 12 | ], 13 | "plugins": [ 14 | ["@babel/plugin-proposal-decorators", { "legacy": true }] 15 | ], 16 | "env": {} 17 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "experimentalObjectRestSpread": true, 8 | "jsx": true 9 | } 10 | }, 11 | "globals": { 12 | "ASSET_PATH": true, 13 | "_":true, 14 | "__":true, 15 | "THREE": false, 16 | "Int8Array": false, 17 | "Int16Array": false, 18 | "Int32Array": false, 19 | "Uint8Array": false, 20 | "Uint16Array": false, 21 | "Uint32Array": false, 22 | "Float32Array": false, 23 | "Float64Array": false, 24 | "Array": false, 25 | "Uint8ClampedArray": false, 26 | "google": false, 27 | "Promise": false, 28 | "document": false, 29 | "navigator": false, 30 | "window": false, 31 | "TimelineLite": false, 32 | "TimelineMax": false, 33 | "TweenLite": false, 34 | "TweenMax": false, 35 | "Back": false, 36 | "Bounce": false, 37 | "Circ": false, 38 | "Cubic": false, 39 | "Ease": false, 40 | "EaseLookup": false, 41 | "Elastic": false, 42 | "Expo": false, 43 | "Linear": false, 44 | "Power0": false, 45 | "Power1": false, 46 | "Power2": false, 47 | "Power3": false, 48 | "Power4": false, 49 | "Quad": false, 50 | "Quart": false, 51 | "Quint": false, 52 | "RoughEase": false, 53 | "Sine": false, 54 | "SlowMo": false, 55 | "SteppedEase": false, 56 | "Strong": false, 57 | "Draggable": false, 58 | "SplitText": false, 59 | "VelocityTracker": false, 60 | "CSSPlugin": false, 61 | "ThrowPropsPlugin": false, 62 | "BezierPlugin": false 63 | }, 64 | "plugins": [ 65 | "react", 66 | "standard", 67 | "promise" 68 | ], 69 | "extends": [ 70 | ], 71 | "env": { 72 | "browser": true, 73 | "node": true 74 | }, 75 | "rules": { 76 | "no-console": 0, 77 | "no-extra-semi": 2, 78 | "semi-spacing": 2, 79 | "semi": 2, 80 | "guard-for-in": 1, 81 | "no-trailing-spaces": 1, 82 | "react/jsx-uses-vars": 1, 83 | "react/jsx-uses-react": 1, 84 | "react/jsx-wrap-multilines": 1, 85 | "react/react-in-jsx-scope": 1, 86 | "react/prop-types": 0, 87 | "react/sort-comp": [1, { 88 | "order": [ 89 | "static-methods", 90 | "lifecycle", 91 | "everything-else", 92 | "render" 93 | ], 94 | "groups": { 95 | "lifecycle": [ 96 | "displayName", 97 | "propTypes", 98 | "contextTypes", 99 | "childContextTypes", 100 | "mixins", 101 | "statics", 102 | "defaultProps", 103 | "constructor", 104 | "getDefaultProps", 105 | "getInitialState", 106 | "state", 107 | "getChildContext", 108 | "componentWillMount", 109 | "componentDidMount", 110 | "componentWillReceiveProps", 111 | "shouldComponentUpdate", 112 | "componentWillUpdate", 113 | "componentDidUpdate", 114 | "componentWillUnmount", 115 | "componentWillAppear", 116 | "componentWillEnter", 117 | "componentWillLeave" 118 | ] 119 | } 120 | } 121 | ], 122 | "no-multi-spaces": 1, 123 | "no-use-before-define": 1, 124 | "arrow-spacing": [2, { "before": true, "after": true }], 125 | "block-spacing": [2, "always"], 126 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 127 | "camelcase": [2, { "properties": "never" }], 128 | "comma-spacing": [2, { "before": false, "after": true }], 129 | "comma-dangle": 0, 130 | "id-length": [1, {"min":1, "max": 50}], 131 | "space-before-function-paren": 0, 132 | "comma-style": [2, "last"], 133 | "curly": [2, "multi-line"], 134 | "dot-location": [2, "property"], 135 | "eqeqeq": [2, "always", {"null": "ignore"}], 136 | "func-call-spacing": [2, "never"], 137 | "handle-callback-err": [2, "^(err|error)$" ], 138 | "indent": [2, 2, { "SwitchCase": 1 }], 139 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 140 | "keyword-spacing": [2, { "before": true, "after": true }], 141 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 142 | "new-parens": 2, 143 | "no-array-constructor": 2, 144 | "no-caller": 2, 145 | "no-class-assign": 2, 146 | "no-cond-assign": 2, 147 | "no-const-assign": 2, 148 | "no-constant-condition": [2, { "checkLoops": false }], 149 | "no-control-regex": 2, 150 | "no-dupe-args": 2, 151 | "no-dupe-class-members": 2, 152 | "no-dupe-keys": 2, 153 | "no-duplicate-case": 2, 154 | "no-duplicate-imports": 2, 155 | "no-empty-character-class": 2, 156 | "no-empty-pattern": 2, 157 | "no-eval": 2, 158 | "no-ex-assign": 2, 159 | "no-extend-native": 2, 160 | "no-extra-bind": 2, 161 | "no-extra-boolean-cast": 2, 162 | "no-extra-parens": [2, "functions"], 163 | "no-fallthrough": 2, 164 | "no-floating-decimal": 2, 165 | "no-func-assign": 2, 166 | "no-global-assign": 2, 167 | "no-implied-eval": 2, 168 | "no-inner-declarations": [2, "functions"], 169 | "no-invalid-regexp": 2, 170 | "no-irregular-whitespace": 2, 171 | "no-iterator": 2, 172 | "no-label-var": 2, 173 | "no-labels": [2, { "allowLoop": false, "allowSwitch": false }], 174 | "no-lone-blocks": 2, 175 | "no-mixed-spaces-and-tabs": 2, 176 | "no-multi-str": 2, 177 | "no-negated-in-lhs": 2, 178 | "no-new": 2, 179 | "no-new-func": 2, 180 | "no-new-object": 2, 181 | "no-new-require": 2, 182 | "no-new-symbol": 2, 183 | "no-new-wrappers": 2, 184 | "no-obj-calls": 2, 185 | "no-octal": 2, 186 | "no-octal-escape": 2, 187 | "no-path-concat": 2, 188 | "no-proto": 2, 189 | "no-redeclare": 2, 190 | "no-regex-spaces": 2, 191 | "no-return-assign": [2, "except-parens"], 192 | "no-self-assign": 2, 193 | "no-self-compare": 2, 194 | "no-sequences": 2, 195 | "no-shadow-restricted-names": 2, 196 | "no-sparse-arrays": 2, 197 | "no-tabs": 2, 198 | "no-template-curly-in-string": 2, 199 | "no-this-before-super": 2, 200 | "no-throw-literal": 2, 201 | "no-undef": 2, 202 | "no-undef-init": 2, 203 | "no-unexpected-multiline": 2, 204 | "no-unmodified-loop-condition": 2, 205 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 206 | "no-unreachable": 2, 207 | "no-unsafe-finally": 2, 208 | "no-unsafe-negation": 2, 209 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 210 | "no-unused-expressions": 2, 211 | "no-useless-call": 2, 212 | "no-useless-computed-key": 2, 213 | "no-useless-escape": 1, 214 | "no-useless-rename": 2, 215 | "no-whitespace-before-property": 1, 216 | "no-with": 2, 217 | "object-property-newline": [2, { "allowMultiplePropertiesPerLine": true }], 218 | "one-var": [2, { "initialized": "never" }], 219 | "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], 220 | "padded-blocks": [2, "never"], 221 | "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], 222 | "rest-spread-spacing": [2, "never"], 223 | "space-in-parens": [1, "never"], 224 | "space-infix-ops": 1, 225 | "space-unary-ops": [1, { "words": true, "nonwords": false }], 226 | "template-curly-spacing": [2, "never"], 227 | "unicode-bom": [2, "never"], 228 | "use-isnan": 2, 229 | "valid-typeof": 2, 230 | "wrap-iife": [2, "any", { "functionPrototypeMethods": true }], 231 | "yield-star-spacing": [2, "both"], 232 | "yoda": [2, "never"], 233 | "standard/object-curly-even-spacing": [2, "either"], 234 | "standard/array-bracket-even-spacing": [2, "either"], 235 | "standard/computed-property-even-spacing": [2, "even"], 236 | "promise/param-names": 2 237 | } 238 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | 4 | node_modules 5 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Animated Mesh Lines 2 | 3 | 5 usages of the library THREE.MeshLines for Three.js to create and animate 3D custom lines, by [Jérémie Boulay](https://jeremieboulay.fr/portfolio/) 4 | 5 |

6 | 7 | Animated Mesh Lines 8 | 9 |

10 | 11 |

12 | 13 | Animated Mesh Lines - Shooting Star 14 | 15 | 16 | Animated Mesh Lines - Energy 17 | 18 | 19 | Animated Mesh Lines - Colors 20 | 21 | 22 | Animated Mesh Lines - Boreal Sky 23 | 24 |

25 | 26 | [Article on Codrops](https://tympanus.net/codrops/?p=37034) 27 | 28 | [Demo](https://tympanus.net/Development/AnimatedMeshLines) 29 | 30 | ## Credits 31 | 32 | Thanks to: 33 | - [Ricardo Cabello](https://mrdoob.com/) - For [three.js](https://threejs.org) 34 | - [Jaume Sanchez Elias](https://twitter.com/thespite) - For [THREE.MeshLine](https://github.com/spite/THREE.MeshLine) 35 | - [GreenSock](https://greensock.com/) - For [Gsap](https://greensock.com/) 36 | - [Robin Delaporte](https://robindelaporte.fr/) - To be my source of inspiration 😉. 37 | 38 | 39 | ## License 40 | This resource can be used freely if integrated or build upon in personal or commercial projects such as websites, web apps and web templates intended for sale. It is not allowed to take the resource "as-is" and sell it, redistribute, re-publish it, or sell "pluginized" versions of it. Free plugins built using this resource should have a visible mention and link to the original work. Always consider the licenses of all included libraries, scripts and images used. 41 | 42 | ## Misc 43 | 44 | Follow Jeremboo: [Portfolio](https://jeremieboulay.fr/portfolio/), [Twitter](https://twitter.com/JeremBoo), [Codepen](https://codepen.io/Jeremboo/), [GitHub](https://github.com/Jeremboo) 45 | 46 | Follow Codrops: [Twitter](http://www.twitter.com/codrops), [Facebook](http://www.facebook.com/codrops), [Google+](https://plus.google.com/101095823814290637419), [GitHub](https://github.com/codrops), [Pinterest](http://www.pinterest.com/codrops/), [Instagram](https://www.instagram.com/codropsss/) 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * demo.js 3 | * http://www.codrops.com 4 | * 5 | * Licensed under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 8 | * Copyright 2017, Codrops 9 | * http://www.codrops.com 10 | */ 11 | 12 | import hasTouch from 'utils/hasTouch'; 13 | import './base.css'; 14 | 15 | class App { 16 | constructor() { 17 | this.demos = document.querySelectorAll('.frame__demo'); 18 | 19 | this.isMobile = hasTouch(); 20 | } 21 | 22 | onHide(hideMethod) { 23 | this.demos.forEach((demo) => { 24 | demo.addEventListener('click', (e) => { 25 | e.preventDefault(); 26 | if (e.target.classList.contains('.frame__demo--current')) return; 27 | hideMethod(() => { 28 | window.location = e.target.href; 29 | }); 30 | }); 31 | }); 32 | } 33 | } 34 | 35 | export default new App(); -------------------------------------------------------------------------------- /app/base.css: -------------------------------------------------------------------------------- 1 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden]{display:none;}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}body{margin:0;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;margin:0.67em 0;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:bold;}dfn{font-style:italic;}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em;}pre{white-space:pre-wrap;}q{quotes:"\201C" "\201D" "\2018" "\2019";}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-0.5em;}sub{bottom:-0.25em;}img{border:0;}svg:not(:root){overflow:hidden;}figure{margin:0;}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}legend{border:0;padding:0;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,select{text-transform:none;}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}button[disabled],html input[disabled]{cursor:default;}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;} 2 | *, 3 | *::after, 4 | *::before { 5 | box-sizing: border-box; 6 | } 7 | 8 | :root { 9 | font-size: 16px; 10 | } 11 | 12 | body, html, main, .frame{ 13 | width: 100%; 14 | height: 100%; 15 | } 16 | 17 | body { 18 | position: fixed; 19 | top: 0; 20 | left: 0; 21 | --color-text: #fff; 22 | --color-bg: #0e0e0f; 23 | --color-link: #0f51e4; 24 | --color-link-hover: #ebd944; 25 | color: var(--color-text); 26 | background-color: var(--color-bg); 27 | font-family: Futura, "futura-pt", Arial, sans-serif; 28 | -webkit-font-smoothing: antialiased; 29 | -moz-osx-font-smoothing: grayscale; 30 | } 31 | 32 | a { 33 | display: inline-block; 34 | text-decoration: none; 35 | color: var(--color-link); 36 | outline: none; 37 | transition: color 0.2s, transform 0.2s; 38 | transform: translate(0px, 0px); 39 | } 40 | 41 | a:hover, 42 | a:focus { 43 | color: var(--color-link-hover); 44 | outline: none; 45 | transform: translate(0px, -2px); 46 | } 47 | 48 | .message { 49 | background: var(--color-text); 50 | color: var(--color-bg); 51 | padding: 1rem; 52 | text-align: center; 53 | } 54 | 55 | .frame { 56 | padding: 3rem 5vw; 57 | text-align: center; 58 | position: relative; 59 | z-index: 1000; 60 | } 61 | 62 | .frame__title { 63 | font-size: 1rem; 64 | margin: 0 0 1rem; 65 | font-weight: normal; 66 | } 67 | 68 | .frame__links { 69 | display: inline; 70 | } 71 | 72 | .frame a { 73 | text-transform: lowercase; 74 | } 75 | 76 | .frame__github, 77 | .frame__links a:not(:last-child), 78 | .frame__demos a:not(:last-child) { 79 | margin-right: 1rem; 80 | } 81 | 82 | .frame__demos { 83 | width: 100%; 84 | padding: 0 2.5rem; 85 | margin: 1rem 0; 86 | position: absolute; 87 | bottom: 1rem; 88 | left: 50%; 89 | transform: translate(-50%, 0); 90 | } 91 | /* 92 | .frame__demo:after { 93 | content: ''; 94 | position:absolute; 95 | bottom: 0; 96 | left: 0; 97 | width: 0%; 98 | height: 2px; 99 | background-color: var(--color-text); 100 | transition: width 0.2s; 101 | } */ 102 | 103 | .frame__demo { 104 | margin-top: 1rem; 105 | } 106 | 107 | .frame__demo--current, 108 | .frame__demo:hover { 109 | color: var(--color-text); 110 | } 111 | .frame__demo--current { 112 | border-bottom: 2px solid var(--color-text); 113 | transform: none; 114 | } 115 | 116 | .content { 117 | display: flex; 118 | flex-direction: column; 119 | width: 100vw; 120 | height: calc(100vh - 13rem); 121 | position: relative; 122 | justify-content: flex-start; 123 | align-items: center; 124 | } 125 | 126 | .overlay { 127 | position: absolute; 128 | pointer-events: none; 129 | top: 0; 130 | left: 0; 131 | width: 100%; 132 | height: 100%; 133 | z-index: 99999; 134 | background-color: #0e0e0f; 135 | opacity: 1; 136 | } 137 | 138 | @media screen and (min-width: 53em) { 139 | .message { 140 | display: none; 141 | } 142 | .frame { 143 | position: fixed; 144 | text-align: left; 145 | z-index: 10000; 146 | top: 0; 147 | left: 0; 148 | display: grid; 149 | align-content: space-between; 150 | width: 100%; 151 | max-width: none; 152 | height: 100vh; 153 | padding: 3rem; 154 | pointer-events: none; 155 | grid-template-columns: auto 1fr; 156 | grid-template-rows: auto auto auto; 157 | grid-template-areas: 158 | 'title links'\ 159 | '... ...'\ 160 | 'github demos'\ 161 | ; 162 | } 163 | .frame__title-wrap { 164 | grid-area: title; 165 | display: flex; 166 | } 167 | .frame__title { 168 | margin: 0; 169 | } 170 | .frame__tagline { 171 | position: relative; 172 | margin: 0 0 0 4rem; 173 | padding: 0 0 0 1rem; 174 | } 175 | .frame__tagline::before { 176 | content: ''; 177 | position: absolute; 178 | right: 100%; 179 | top: 50%; 180 | height: 1px; 181 | width: 3rem; 182 | background: var(--color-text); 183 | } 184 | .frame__github { 185 | grid-area: github; 186 | justify-self: start; 187 | margin: 0; 188 | } 189 | .frame__demos { 190 | position: initial; 191 | left: initial; 192 | width: auto; 193 | transform: none; 194 | margin: 0; 195 | padding: 0; 196 | grid-area: demos; 197 | justify-self: end; 198 | } 199 | .frame__demo { 200 | margin-top: 0; 201 | } 202 | .frame__links { 203 | grid-area: links; 204 | padding: 0; 205 | justify-self: end; 206 | } 207 | .frame a { 208 | pointer-events: auto; 209 | } 210 | .content { 211 | height: 100vh; 212 | justify-content: center; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /app/decorators/FullScreenInBackground.js: -------------------------------------------------------------------------------- 1 | 2 | export default (Target) => class FullScreenInBackground extends Target { 3 | constructor(props) { 4 | super(window.innerWidth, window.innerHeight, props); 5 | 6 | // Put automaticaly the canvas in background 7 | this.dom.style.position = 'absolute'; 8 | this.dom.style.top = '0'; 9 | this.dom.style.left = '0'; 10 | this.dom.style.zIndex = '-1'; 11 | document.body.appendChild(this.dom); 12 | 13 | this.resize = this.resize.bind(this); 14 | 15 | window.addEventListener('resize', this.resize); 16 | window.addEventListener('orientationchange', this.resize); 17 | this.resize(); 18 | } 19 | 20 | resize() { 21 | super.resize(window.innerWidth, window.innerHeight); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /app/decorators/HandleCameraOrbit.js: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | import app from 'App'; 4 | 5 | export default (cameraAmpl = { x: 5, y: 5 }, velocity = 0.1, lookAt = new Vector3()) => 6 | (Target) => class HandleCameraOrbit extends Target { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.cameraAmpl = cameraAmpl; 11 | this.cameraVelocity = velocity; 12 | this.lookAt = lookAt; 13 | 14 | this.mousePosition = { x: 0, y: 0 }; 15 | this.normalizedOrientation = new Vector3(); 16 | 17 | this.update = this.update.bind(this); 18 | this.handleMouseMove = this.handleMouseMove.bind(this); 19 | this.handleOrientationMove = this.handleOrientationMove.bind(this); 20 | 21 | if (app.isMobile) { 22 | window.addEventListener('deviceorientation', this.handleOrientationMove); 23 | } else { 24 | window.addEventListener('mousemove', this.handleMouseMove); 25 | } 26 | } 27 | 28 | handleMouseMove(event) { 29 | this.mousePosition.x = event.clientX || (event.touches && event.touches[0].clientX) || this.mousePosition.x; 30 | this.mousePosition.y = event.clientY || (event.touches && event.touches[0].clientY) || this.mousePosition.y; 31 | 32 | this.normalizedOrientation.set( 33 | -((this.mousePosition.x / this.width) - 0.5) * this.cameraAmpl.x, 34 | ((this.mousePosition.y / this.height) - 0.5) * this.cameraAmpl.y, 35 | 0.5, 36 | ); 37 | } 38 | 39 | handleOrientationMove(event) { 40 | // https://stackoverflow.com/questions/40716461/how-to-get-the-angle-between-the-horizon-line-and-the-device-in-javascript 41 | const rad = Math.atan2(event.gamma, event.beta); 42 | if (Math.abs(rad) > 1.5) return; 43 | this.normalizedOrientation.x = -(rad) * this.cameraAmpl.y; 44 | // TODO handle orientation.y 45 | } 46 | 47 | update() { 48 | super.update(); 49 | 50 | this.camera.position.x += (this.normalizedOrientation.x - this.camera.position.x) * this.cameraVelocity; 51 | this.camera.position.y += (this.normalizedOrientation.y - this.camera.position.y) * this.cameraVelocity; 52 | this.camera.lookAt(this.lookAt); 53 | } 54 | } 55 | ; -------------------------------------------------------------------------------- /app/decorators/PostProcessing.js: -------------------------------------------------------------------------------- 1 | import { Clock } from 'three'; 2 | import { 3 | BloomEffect, EffectComposer, EffectPass, RenderPass, 4 | } from 'postprocessing'; 5 | 6 | 7 | export default (Target) => class PostProcessing extends Target { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.clock = new Clock(); 12 | 13 | this.currentPass = false; 14 | this.effects = {}; 15 | this.passes = []; 16 | this.composer = new EffectComposer(this.renderer, { 17 | // stencilBuffer: true, 18 | // depthTexture: true, 19 | }); 20 | 21 | this.effects.render = new RenderPass(this.scene, this.camera); 22 | this.addPass(this.effects.render); 23 | } 24 | 25 | /** 26 | * * ******************* 27 | * * ADD EFFECTS 28 | * * ******************* 29 | */ 30 | addBloomEffect(props, opacity) { 31 | this.effects.bloom = new BloomEffect(props); 32 | this.effects.bloom.blendMode.opacity.value = opacity; 33 | this.addPass(new EffectPass(this.camera, this.effects.bloom)); 34 | } 35 | 36 | /** 37 | * * ******************* 38 | * * GLOBAL 39 | * * ******************* 40 | */ 41 | 42 | addPass(passe) { 43 | if (this.passes.length) this.passes[this.passes.length - 1].renderToScreen = false; 44 | this.passes.push(passe); 45 | this.composer.addPass(passe); 46 | this.passes[this.passes.length - 1].renderToScreen = true; 47 | } 48 | 49 | /** 50 | * * ******************* 51 | * * OVERWRITED FUNCTIONS 52 | * * ******************* 53 | */ 54 | render() { 55 | this.composer.render(this.clock.getDelta()); 56 | } 57 | resizeRender() { 58 | if (this.composer) this.composer.setSize(this.width, this.height); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /app/demos/demo2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Animated Mesh Lines | Confetti | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |

Animated Mesh Lines

19 |
20 | GitHub 21 | 25 |
26 | demo 1 27 | demo 2 28 | demo 3 29 | demo 4 30 | demo 5 31 |
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /app/demos/demo2/index.js: -------------------------------------------------------------------------------- 1 | import '../../base.css'; 2 | import './style.styl'; 3 | 4 | import { Color, Vector3 } from 'three'; 5 | import Engine from 'utils/engine'; 6 | import AnimatedText3D from 'objects/AnimatedText3D'; 7 | import LineGenerator from 'objects/LineGenerator'; 8 | 9 | import getRandomFloat from 'utils/getRandomFloat'; 10 | import getRandomItem from 'utils/getRandomItem'; 11 | 12 | import HandleCameraOrbit from 'decorators/HandleCameraOrbit'; 13 | import FullScreenInBackground from 'decorators/FullScreenInBackground'; 14 | 15 | import app from 'App'; 16 | 17 | 18 | /** 19 | * * ******************* 20 | * * ENGINE 21 | * * ******************* 22 | */ 23 | 24 | @FullScreenInBackground 25 | @HandleCameraOrbit({ x: 4, y: 4 }) 26 | class CustomEngine extends Engine {} 27 | 28 | const engine = new CustomEngine(); 29 | 30 | 31 | /** 32 | * * ******************* 33 | * * TITLE 34 | * * ******************* 35 | */ 36 | 37 | const text = new AnimatedText3D('Confetti', { color: '#0f070a', size: app.isMobile ? 0.5 : 0.8 }); 38 | text.position.x -= text.basePosition * 0.5; 39 | // text.position.y -= 0.5; 40 | engine.add(text); 41 | 42 | 43 | /** 44 | * * ******************* 45 | * * LIGNES 46 | * * ******************* 47 | */ 48 | 49 | const COLORS = ['#4062BB', '#52489C', '#59C3C3', '#F45B69', '#F45B69'].map((col) => new Color(col)); 50 | const STATIC_PROPS = { 51 | width: 0.1, 52 | nbrOfPoints: 5, 53 | }; 54 | 55 | class CustomLineGenerator extends LineGenerator { 56 | // start() { 57 | // const currentFreq = this.frequency; 58 | // this.frequency = 1; 59 | // setTimeout(() => { 60 | // this.frequency = currentFreq; 61 | // }, 1000); 62 | // super.start(); 63 | // } 64 | 65 | addLine() { 66 | super.addLine({ 67 | length: getRandomFloat(8, 15), 68 | visibleLength: getRandomFloat(0.05, 0.2), 69 | position: new Vector3( 70 | (Math.random() - 0.5) * 1.5, 71 | Math.random() - 1, 72 | (Math.random() - 0.5) * 2, 73 | ).multiplyScalar(getRandomFloat(5, 20)), 74 | turbulence: new Vector3( 75 | getRandomFloat(-2, 2), 76 | getRandomFloat(0, 2), 77 | getRandomFloat(-2, 2), 78 | ), 79 | orientation: new Vector3( 80 | getRandomFloat(-0.8, 0.8), 81 | 1, 82 | 1, 83 | ), 84 | speed: getRandomFloat(0.004, 0.008), 85 | color: getRandomItem(COLORS), 86 | }); 87 | } 88 | } 89 | const lineGenerator = new CustomLineGenerator({ 90 | frequency: 0.5 91 | }, STATIC_PROPS); 92 | engine.add(lineGenerator); 93 | 94 | /** 95 | * * ******************* 96 | * * START 97 | * * ******************* 98 | */ 99 | // Show 100 | engine.start(); 101 | const tlShow = new TimelineLite({ delay: 0.2, onStart: () => { 102 | lineGenerator.start(); 103 | }}); 104 | tlShow.to('.overlay', 0.6, { autoAlpha: 0 }); 105 | tlShow.fromTo(engine.lookAt, 3, { y: -4 }, { y: 0, ease: Power3.easeOut }, '-=0.4'); 106 | tlShow.add(text.show, '-=2'); 107 | 108 | // Hide 109 | app.onHide((onComplete) => { 110 | const tlHide = new TimelineLite(); 111 | tlHide.to(engine.lookAt, 2, { y: -6, ease: Power3.easeInOut }); 112 | tlHide.add(text.hide, 0); 113 | tlHide.add(lineGenerator.stop); 114 | tlHide.to('.overlay', 0.5, { autoAlpha: 1, onComplete }, '-=1.5'); 115 | }); 116 | -------------------------------------------------------------------------------- /app/demos/demo2/style.styl: -------------------------------------------------------------------------------- 1 | // Update the colors for this demo 2 | body { 3 | --color-text: #0f070a; 4 | --color-bg: #ffffff; 5 | --color-bg-2: #ecc7d5; 6 | --color-link: #4062BB; 7 | --color-link-hover: #52489C; 8 | background: radial-gradient(ellipse at 75% 0%, var(--color-bg) 50%, var(--color-bg-2) 250%); 9 | } -------------------------------------------------------------------------------- /app/demos/demo3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Animated Mesh Lines | Energy | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |

Animated Mesh Lines

19 |
20 | GitHub 21 | 25 |
26 | demo 1 27 | demo 2 28 | demo 3 29 | demo 4 30 | demo 5 31 |
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /app/demos/demo3/index.js: -------------------------------------------------------------------------------- 1 | import '../../base.css'; 2 | import './style.styl'; 3 | 4 | import { Color, Vector3 } from 'three'; 5 | import Engine from 'utils/engine'; 6 | import AnimatedText3D from 'objects/AnimatedText3D'; 7 | import LineGenerator from 'objects/LineGenerator'; 8 | 9 | import getRandomFloat from 'utils/getRandomFloat'; 10 | import getRandomItem from 'utils/getRandomItem'; 11 | 12 | import HandleCameraOrbit from 'decorators/HandleCameraOrbit'; 13 | import FullScreenInBackground from 'decorators/FullScreenInBackground'; 14 | 15 | import app from 'App'; 16 | 17 | 18 | /** 19 | * * ******************* 20 | * * ENGINE 21 | * * ******************* 22 | */ 23 | 24 | @FullScreenInBackground 25 | @HandleCameraOrbit({ x: 8, y: 8 }, 0.15) 26 | class CustomEngine extends Engine {} 27 | 28 | const engine = new CustomEngine(); 29 | 30 | 31 | /** 32 | * * ******************* 33 | * * TITLE 34 | * * ******************* 35 | */ 36 | class CustomAnimatedText3D extends AnimatedText3D { 37 | constructor(...props) { 38 | super(...props); 39 | this.t = 0; 40 | this.update = this.update.bind(this); 41 | } 42 | 43 | update() { 44 | this.t += 0.05; 45 | this.position.y += (Math.sin(this.t)) * 0.0025; 46 | } 47 | } 48 | const text = new CustomAnimatedText3D('Energy', { color: '#0f070a', size: app.isMobile ? 0.6 : 0.8 }); 49 | text.position.x -= text.basePosition * 0.5; 50 | text.position.y += 0.15; 51 | 52 | 53 | /** 54 | * * ******************* 55 | * * LIGNES 56 | * * ******************* 57 | */ 58 | const COLORS = ['#FDFFFC', '#FDFFFC', '#FDFFFC', '#FDFFFC', '#EA526F', '#71b9f2'].map((col) => new Color(col)); 59 | const STATIC_PROPS = { 60 | nbrOfPoints: 4, 61 | speed: 0.03, 62 | turbulence: new Vector3(1, 0.8, 1), 63 | orientation: new Vector3(1, 0, 0), 64 | transformLineMethod: p => { 65 | const a = ((0.5 - Math.abs(0.5 - p)) * 3); 66 | return a; 67 | } 68 | }; 69 | 70 | const POSITION_X = app.isMobile ? -1.8 : -3.2; 71 | 72 | const LENGTH_MIN = app.isMobile ? 3.25 : 5; 73 | const LENGTH_MAX = app.isMobile ? 3.7 : 7; 74 | class CustomLineGenerator extends LineGenerator { 75 | start() { 76 | const currentFreq = this.frequency; 77 | this.frequency = 1; 78 | setTimeout(() => { 79 | this.frequency = currentFreq; 80 | }, 500); 81 | super.start(); 82 | } 83 | 84 | addLine() { 85 | const line = super.addLine({ 86 | width: getRandomFloat(0.1, 0.3), 87 | length: getRandomFloat(LENGTH_MIN, LENGTH_MAX), 88 | visibleLength: getRandomFloat(0.05, 0.8), 89 | position: new Vector3( 90 | POSITION_X, 91 | 0.3, 92 | getRandomFloat(-1, 1), 93 | ), 94 | color: getRandomItem(COLORS), 95 | }); 96 | line.rotation.x = getRandomFloat(0, Math.PI * 2); 97 | 98 | if (Math.random() > 0.1) { 99 | const line = super.addLine({ 100 | width: getRandomFloat(0.05, 0.1), 101 | length: getRandomFloat(5, 10), 102 | visibleLength: getRandomFloat(0.05, 0.5), 103 | speed: 0.05, 104 | position: new Vector3( 105 | getRandomFloat(-9, 5), 106 | getRandomFloat(-5, 5), 107 | getRandomFloat(-10, 6), 108 | ), 109 | color: getRandomItem(COLORS), 110 | }); 111 | line.rotation.x = getRandomFloat(0, Math.PI * 2); 112 | } 113 | } 114 | } 115 | const lineGenerator = new CustomLineGenerator({ 116 | frequency: 0.1, 117 | }, STATIC_PROPS); 118 | engine.add(lineGenerator); 119 | 120 | /** 121 | * * ******************* 122 | * * START 123 | * * ******************* 124 | */ 125 | // Show 126 | engine.start(); 127 | const tlShow = new TimelineLite({ delay: 0.2 }); 128 | tlShow.to('.overlay', 0.6, { autoAlpha: 0 }); 129 | tlShow.fromTo(engine.lookAt, 3, { y: -4 }, { y: 0, ease: Power3.easeOut }, 0); 130 | tlShow.add(lineGenerator.start, '-=2.5'); 131 | tlShow.add(() => { 132 | engine.add(text); 133 | text.show(); 134 | }, '-=1.6'); 135 | 136 | // Hide 137 | app.onHide((onComplete) => { 138 | const tlHide = new TimelineLite(); 139 | tlHide.to(engine.lookAt, 2, { y: -6, ease: Power3.easeInOut }); 140 | tlHide.add(text.hide, 0); 141 | tlHide.add(lineGenerator.stop); 142 | tlHide.to('.overlay', 0.5, { autoAlpha: 1, onComplete }, '-=1.5'); 143 | }); 144 | -------------------------------------------------------------------------------- /app/demos/demo3/style.styl: -------------------------------------------------------------------------------- 1 | // Update the colors for this demo 2 | body { 3 | --color-text: #0f070a; 4 | --color-bg: #fabd69; 5 | --color-bg-2: #f98e4a; 6 | --color-link: #317bd0; 7 | --color-link-hover: #317bd0; 8 | background: radial-gradient(ellipse at 100% 0%, #fed96f 0%, var(--color-bg) 50%, var(--color-bg-2) 115%); 9 | } -------------------------------------------------------------------------------- /app/demos/demo4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Animated Mesh Lines | Colors | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |

Animated Mesh Lines

19 |
20 | GitHub 21 | 25 |
26 | demo 1 27 | demo 2 28 | demo 3 29 | demo 4 30 | demo 5 31 |
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /app/demos/demo4/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Color, 3 | } from 'three'; 4 | 5 | import Engine from 'utils/engine'; 6 | import AnimatedText3D from 'objects/AnimatedText3D'; 7 | import LineGenerator from 'objects/LineGenerator'; 8 | 9 | import getRandomFloat from 'utils/getRandomFloat'; 10 | import getRandomItem from 'utils/getRandomItem'; 11 | 12 | import HandleCameraOrbit from 'decorators/HandleCameraOrbit'; 13 | import FullScreenInBackground from 'decorators/FullScreenInBackground'; 14 | 15 | import app from 'App'; 16 | 17 | import '../../base.css'; 18 | import './style.styl'; 19 | 20 | /** 21 | * * ******************* 22 | * * ENGINE 23 | * * ******************* 24 | */ 25 | 26 | @FullScreenInBackground 27 | @HandleCameraOrbit({ x: 1, y: 1 }, 0.1) 28 | class CustomEngine extends Engine {} 29 | const engine = new CustomEngine(); 30 | engine.camera.position.z = 6; 31 | 32 | /** 33 | * * ******************* 34 | * * TITLE 35 | * * ******************* 36 | */ 37 | const text = new AnimatedText3D('Colors', { color: '#ffffff', size: app.isMobile ? 0.4 : 0.4, wireframe: false, opacity: 1, }); 38 | text.position.x = -text.basePosition * (app.isMobile ? 0.5 : 0.55); 39 | text.position.y = (app.isMobile ? -1.2 : -0.9); 40 | text.position.z = 2; 41 | text.rotation.x = -0.1; 42 | 43 | /** 44 | * * ******************* 45 | * * LIGNES 46 | * * ******************* 47 | */ 48 | 49 | const RADIUS_START = 0.3; 50 | const RADIUS_START_MIN = 0.1; 51 | const Z_MIN = -1; 52 | 53 | const Z_INCREMENT = 0.08; 54 | const ANGLE_INCREMENT = 0.025; 55 | const RADIUS_INCREMENT = 0.02; 56 | 57 | const COLORS = ['#dc202e', '#f7ed99', '#2d338b', '#76306b', '#ea8c2d'].map((col) => new Color(col)); 58 | const STATIC_PROPS = { 59 | transformLineMethod: p => p * 1.5, 60 | }; 61 | 62 | const position = { x: 0, y: 0, z: 0 }; 63 | class CustomLineGenerator extends LineGenerator { 64 | addLine() { 65 | if (this.lines.length > 400) return; 66 | 67 | let z = Z_MIN; 68 | let radius = (Math.random() > 0.8) ? RADIUS_START_MIN : RADIUS_START; 69 | let angle = getRandomFloat(0, Math.PI * 2); 70 | 71 | const points = []; 72 | while (z < engine.camera.position.z) { 73 | position.x = Math.cos(angle) * radius; 74 | position.y = Math.sin(angle) * radius; 75 | position.z = z; 76 | 77 | // incrementation 78 | z += Z_INCREMENT; 79 | angle += ANGLE_INCREMENT; 80 | radius += RADIUS_INCREMENT; 81 | 82 | // push 83 | points.push(position.x, position.y, position.z); 84 | } 85 | 86 | // Low lines 87 | super.addLine({ 88 | visibleLength: getRandomFloat(0.1, 0.4), 89 | // visibleLength: 1, 90 | points, 91 | // speed: getRandomFloat(0.001, 0.002), 92 | speed: getRandomFloat(0.001, 0.005), 93 | color: getRandomItem(COLORS), 94 | width: getRandomFloat(0.01, 0.06), 95 | }); 96 | } 97 | } 98 | const lineGenerator = new CustomLineGenerator({ 99 | frequency: 0.9, 100 | }, STATIC_PROPS); 101 | engine.add(lineGenerator); 102 | 103 | /** 104 | * * ******************* 105 | * * START 106 | * * ******************* 107 | */ 108 | // Show 109 | engine.start(); 110 | const tlShow = new TimelineLite({ delay: 0.2, onStart: () => { 111 | lineGenerator.start(); 112 | }}); 113 | tlShow.to('.overlay', 5, { autoAlpha: 0 }); 114 | tlShow.add(() => { 115 | engine.add(text); 116 | text.show(); 117 | }, '-=2'); 118 | 119 | // Hide 120 | app.onHide((onComplete) => { 121 | const tlHide = new TimelineLite(); 122 | tlHide.to('.overlay', 0.5, { autoAlpha: 1, onComplete }, 0.1); 123 | tlHide.add(text.hide, 0); 124 | tlHide.add(lineGenerator.stop); 125 | }); 126 | -------------------------------------------------------------------------------- /app/demos/demo4/style.styl: -------------------------------------------------------------------------------- 1 | // Update the colors for this demo 2 | body { 3 | --color-text: #ffffff; 4 | --color-bg: #143261; 5 | --color-bg-2: #010915; 6 | --color-link: #F6E27F; 7 | --color-link-hover: #E2C391; 8 | background: radial-gradient(ellipse at 50% 50%, var(--color-bg) 40%, var(--color-bg-2) 150%); 9 | } -------------------------------------------------------------------------------- /app/demos/demo5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Animated Mesh Lines | Boreal Sky | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |

Animated Mesh Lines

19 |
20 | GitHub 21 | 25 |
26 | demo 1 27 | demo 2 28 | demo 3 29 | demo 4 30 | demo 5 31 |
32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /app/demos/demo5/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Color, Vector3, SphereBufferGeometry, 3 | Mesh, Raycaster, MeshBasicMaterial, 4 | } from 'three'; 5 | 6 | import Engine from 'utils/engine'; 7 | import Stars from 'objects/Stars'; 8 | import AnimatedText3D from 'objects/AnimatedText3D'; 9 | import LineGenerator from 'objects/LineGenerator'; 10 | 11 | import getRandomFloat from 'utils/getRandomFloat'; 12 | import getRandomItem from 'utils/getRandomItem'; 13 | 14 | import HandleCameraOrbit from 'decorators/HandleCameraOrbit'; 15 | import FullScreenInBackground from 'decorators/FullScreenInBackground'; 16 | import PostProcessing from 'decorators/PostProcessing'; 17 | 18 | import app from 'App'; 19 | 20 | import '../../base.css'; 21 | import './style.styl'; 22 | 23 | /** 24 | * * ******************* 25 | * * ENGINE 26 | * * ******************* 27 | */ 28 | 29 | @FullScreenInBackground 30 | @PostProcessing 31 | @HandleCameraOrbit({ x: 1, y: 1 }, 0.1) 32 | class CustomEngine extends Engine {} 33 | 34 | const engine = new CustomEngine(); 35 | engine.camera.position.z = 2; 36 | engine.addBloomEffect({ 37 | resolutionScale: 0.5, 38 | kernelSize: 4, 39 | distinction: 0.01, 40 | }, 1); 41 | 42 | 43 | 44 | /** 45 | * * ******************* 46 | * * TITLE 47 | * * ******************* 48 | */ 49 | const text = new AnimatedText3D('Boreal Sky', { color: '#FFFFFF', size: app.isMobile ? 0.08 : 0.1, wireframe: true, opacity: 1, }); 50 | text.position.x -= text.basePosition * 0.5; 51 | engine.add(text); 52 | 53 | /** 54 | * * ******************* 55 | * * STARS 56 | * * ******************* 57 | */ 58 | const stars = new Stars(); 59 | stars.update = () => { 60 | stars.rotation.y -= 0.0004; 61 | stars.rotation.x -= 0.0002; 62 | }; 63 | engine.add(stars); 64 | 65 | /** 66 | * * ******************* 67 | * * LIGNES 68 | * * ******************* 69 | */ 70 | 71 | const radius = 4; 72 | const origin = new Vector3(); 73 | const direction = new Vector3(); 74 | const raycaster = new Raycaster(); 75 | const geometry = new SphereBufferGeometry(radius, 32, 32, 0, 3.2, 4, 2.1); 76 | const material = new MeshBasicMaterial({ wireframe: true, visible: false }); 77 | const sphere = new Mesh(geometry, material); 78 | engine.add(sphere); 79 | sphere.position.z = 2; 80 | 81 | const COLORS = ['#FFFAFF', '#0A2463', '#3E92CC', '#723bb7', '#efd28e', '#3f9d8c'].map((col) => new Color(col)); 82 | const STATIC_PROPS = { 83 | transformLineMethod: p => p, 84 | }; 85 | 86 | class CustomLineGenerator extends LineGenerator { 87 | addLine() { 88 | // V1 Regular and symetric lines --------------------------------------------- 89 | // i += 0.1; 90 | // let a = i; 91 | // let y = 12; 92 | // let incrementation = 0.1; 93 | // V2 --------------------------------------------- 94 | let incrementation = 0.1; 95 | let y = getRandomFloat(-radius * 0.6, radius * 1.8); 96 | let a = Math.PI * (-25) / 180; 97 | let aMax = Math.PI * (200) / 180; 98 | 99 | const points = []; 100 | while (a < aMax) { 101 | a += 0.2; 102 | y -= incrementation; 103 | origin.set(radius * Math.cos(a), y, radius * Math.sin(a)); 104 | direction.set(-origin.x, 0, -origin.z); 105 | direction.normalize(); 106 | raycaster.set(origin, direction); 107 | 108 | // save the points 109 | const intersect = raycaster.intersectObject(sphere, true); 110 | if (intersect.length) { 111 | points.push(intersect[0].point.x, intersect[0].point.y, intersect[0].point.z); 112 | } 113 | } 114 | 115 | if (points.length === 0) return; 116 | 117 | if (Math.random() > 0.5) { 118 | // Low lines 119 | super.addLine({ 120 | visibleLength: getRandomFloat(0.01, 0.2), 121 | points, 122 | speed: getRandomFloat(0.003, 0.008), 123 | color: getRandomItem(COLORS), 124 | width: getRandomFloat(0.01, 0.1), 125 | }); 126 | } else { 127 | // Fast lines 128 | super.addLine({ 129 | visibleLength: getRandomFloat(0.05, 0.2), 130 | points, 131 | speed: getRandomFloat(0.01, 0.1), 132 | color: COLORS[0], 133 | width: getRandomFloat(0.01, 0.01), 134 | }); 135 | } 136 | } 137 | } 138 | const lineGenerator = new CustomLineGenerator({ 139 | frequency: 0.99, 140 | }, STATIC_PROPS); 141 | engine.add(lineGenerator); 142 | 143 | 144 | /** 145 | * * ******************* 146 | * * START 147 | * * ******************* 148 | */ 149 | // Show 150 | engine.start(); 151 | const tlShow = new TimelineLite({ delay: 0.2, onStart: () => { 152 | lineGenerator.start(); 153 | }}); 154 | tlShow.to('.overlay', 2, { autoAlpha: 0 }); 155 | tlShow.fromTo(engine.lookAt, 3, { y: -4 }, { y: 0, ease: Power3.easeOut }, '-=2'); 156 | tlShow.add(text.show, '-=2'); 157 | 158 | // Hide 159 | app.onHide((onComplete) => { 160 | const tlHide = new TimelineLite(); 161 | tlHide.to(engine.lookAt, 2, { y: -6, ease: Power3.easeInOut }); 162 | tlHide.add(text.hide, 0); 163 | tlHide.add(lineGenerator.stop); 164 | tlHide.to('.overlay', 0.5, { autoAlpha: 1, onComplete }, '-=1.5'); 165 | }); 166 | -------------------------------------------------------------------------------- /app/demos/demo5/style.styl: -------------------------------------------------------------------------------- 1 | // Update the colors for this demo 2 | body { 3 | --color-text: #ffffff; 4 | --color-bg: #1f174e; 5 | --color-bg-2: #151436; 6 | --color-bg-3: #000000; 7 | --color-link: #8596df; 8 | --color-link-hover: #723bb7; 9 | background: radial-gradient(ellipse at 30% 48%, var(--color-bg) 0%, var(--color-bg-2) 45%, var(--color-bg-3) 150%); 10 | } -------------------------------------------------------------------------------- /app/demos/index/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Animated Mesh Lines | Shooting Stars | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 |

Animated Mesh Lines

20 |
21 | GitHub 22 | 26 |
27 | demo 1 28 | demo 2 29 | demo 3 30 | demo 4 31 | demo 5 32 |
33 |
34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /app/demos/index/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { Color, Vector3 } from 'three'; 3 | import { TimelineLite } from 'gsap'; 4 | 5 | import HandleCameraOrbit from 'decorators/HandleCameraOrbit'; 6 | import FullScreenInBackground from 'decorators/FullScreenInBackground'; 7 | 8 | import Engine from 'utils/engine'; 9 | import AnimatedText3D from 'objects/AnimatedText3D'; 10 | import LineGenerator from 'objects/LineGenerator'; 11 | import Stars from 'objects/Stars'; 12 | 13 | import getRandomFloat from 'utils/getRandomFloat'; 14 | 15 | import app from 'App'; 16 | import './style.styl'; 17 | 18 | 19 | /** 20 | * * ******************* 21 | * * ENGINE 22 | * * ******************* 23 | */ 24 | @FullScreenInBackground 25 | @HandleCameraOrbit({ x: 2, y: 3 }, 0.05) 26 | class CustomEngine extends Engine {} 27 | 28 | const engine = new CustomEngine(); 29 | 30 | 31 | /** 32 | * * ******************* 33 | * * TITLE 34 | * * ******************* 35 | */ 36 | const text = new AnimatedText3D('Shooting Stars', { color: '#dc2c5a', size: app.isMobile ? 0.4 : 0.8 }); 37 | text.position.x -= text.basePosition * 0.5; 38 | // text.position.y -= 0.5; 39 | engine.add(text); 40 | 41 | 42 | /** 43 | * * ******************* 44 | * * LIGNES 45 | * * ******************* 46 | */ 47 | const STATIC_PROPS = { 48 | width: 0.05, 49 | nbrOfPoints: 1, 50 | turbulence: new Vector3(), 51 | orientation: new Vector3(-1, -1, 0), 52 | color: new Color('#e6e0e3'), 53 | }; 54 | 55 | class CustomLineGenerator extends LineGenerator { 56 | addLine() { 57 | super.addLine({ 58 | length: getRandomFloat(5, 10), 59 | visibleLength: getRandomFloat(0.05, 0.2), 60 | speed: getRandomFloat(0.01, 0.02), 61 | position: new Vector3( 62 | getRandomFloat(-4, 8), 63 | getRandomFloat(-3, 5), 64 | getRandomFloat(-2, 5), 65 | ), 66 | }); 67 | } 68 | } 69 | const lineGenerator = new CustomLineGenerator({ frequency: 0.1 }, STATIC_PROPS); 70 | engine.add(lineGenerator); 71 | 72 | 73 | /** 74 | * * ******************* 75 | * * STARS 76 | * * ******************* 77 | */ 78 | const stars = new Stars(); 79 | engine.add(stars); 80 | 81 | /** 82 | * * ******************* 83 | * * START 84 | * * ******************* 85 | */ 86 | // Show 87 | engine.start(); 88 | const tlShow = new TimelineLite({ delay: 0.2, onStart: () => { 89 | lineGenerator.start(); 90 | }}); 91 | tlShow.to('.overlay', 2, { opacity: 0 }); 92 | tlShow.to('.background', 2, { y: -300 }, 0); 93 | tlShow.fromTo(engine.lookAt, 2, { y: -8 }, { y: 0, ease: Power2.easeOut }, 0); 94 | tlShow.add(text.show, '-=1'); 95 | 96 | // Hide 97 | app.onHide((onComplete) => { 98 | const tlHide = new TimelineLite(); 99 | tlHide.to(engine.lookAt, 2, { y: -6, ease: Power3.easeInOut }); 100 | tlHide.add(text.hide, 0); 101 | tlHide.add(lineGenerator.stop); 102 | tlHide.to('.overlay', 0.5, { autoAlpha: 1, onComplete }, '-=1.5'); 103 | }); 104 | -------------------------------------------------------------------------------- /app/demos/index/style.styl: -------------------------------------------------------------------------------- 1 | // Update the colors for this demo 2 | body { 3 | --color-text: #fff; 4 | --color-bg: #0e0e0f; 5 | --color-bg-2: #242635; 6 | --color-bg-3: #dc2c5a; 7 | --color-link: #dc2c5a; 8 | --color-link-hover: #ff0060; 9 | } 10 | 11 | gradientMargin = 800px; 12 | 13 | .background { 14 | position absolute; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: "calc(100% + %s)" % gradientMargin; 19 | // background: linear-gradient(200deg, var(--color-bg) 0%, var(--color-bg-2) 80%, var(--color-bg-3) 150%); 20 | background: radial-gradient(ellipse at 500% 0%, var(--color-bg) 50%, var(--color-bg-2) 80%, var(--color-bg-3) 100%); 21 | z-index: -2; 22 | transform: translateY(-(gradientMargin)); 23 | } -------------------------------------------------------------------------------- /app/objects/AnimatedMeshLine.js: -------------------------------------------------------------------------------- 1 | import { 2 | Mesh, Vector3, SplineCurve, Geometry, Color, 3 | } from 'three'; 4 | import { MeshLine, MeshLineMaterial } from 'three.meshline'; 5 | 6 | import getRandomFloat from 'utils/getRandomFloat'; 7 | 8 | 9 | export default class AnimatedMeshLine extends Mesh { 10 | constructor({ 11 | width = 0.1, 12 | speed = 0.01, 13 | visibleLength = 0.5, 14 | color = new Color('#000000'), 15 | opacity = 1, 16 | position = new Vector3(0, 0, 0), 17 | 18 | // Array of points already done 19 | points = false, 20 | // Params to create the array of points 21 | length = 2, 22 | nbrOfPoints = 3, 23 | orientation = new Vector3(1, 0, 0), 24 | turbulence = new Vector3(0, 0, 0), 25 | transformLineMethod = false, 26 | } = {}) { 27 | // * ****************************** 28 | // * Create the main line 29 | let linePoints = []; 30 | if (!points) { 31 | const currentPoint = new Vector3(); 32 | // The size of each segment oriented in the good directon 33 | const segment = orientation.normalize().multiplyScalar(length / nbrOfPoints); 34 | linePoints.push(currentPoint.clone()); 35 | for (let i = 0; i < nbrOfPoints - 1; i++) { 36 | // Increment the point depending to the orientation 37 | currentPoint.add(segment); 38 | // Add turbulence to the current point 39 | linePoints.push(currentPoint.clone().set( 40 | currentPoint.x + getRandomFloat(-turbulence.x, turbulence.x), 41 | currentPoint.y + getRandomFloat(-turbulence.y, turbulence.y), 42 | currentPoint.z + getRandomFloat(-turbulence.z, turbulence.z), 43 | )); 44 | } 45 | // Finish the curve to the correct point without turbulence 46 | linePoints.push(currentPoint.add(segment).clone()); 47 | // * ****************************** 48 | // * Smooth the line 49 | // TODO 3D spline curve https://math.stackexchange.com/questions/577641/how-to-calculate-interpolating-splines-in-3d-space 50 | // TODO https://github.com/mrdoob/three.js/blob/master/examples/webgl_geometry_nurbs.html 51 | const curve = new SplineCurve(linePoints); 52 | linePoints = new Geometry().setFromPoints(curve.getPoints(50)); 53 | } else { 54 | linePoints = points; 55 | } 56 | 57 | 58 | 59 | // * ****************************** 60 | // * Create the MeshLineGeometry 61 | const line = new MeshLine(); 62 | line.setGeometry(linePoints, transformLineMethod); 63 | const geometry = line.geometry; 64 | 65 | // * ****************************** 66 | // * Create the Line Material 67 | // dashArray - the length and space between dashes. (0 - no dash) 68 | // dashRatio - defines the ratio between that is visible or not (0 - more visible, 1 - more invisible). 69 | // dashOffset - defines the location where the dash will begin. Ideal to animate the line. 70 | // DashArray: The length of a dash = dashArray * length. 71 | // Here 2 mean a cash is 2 time longer that the original length 72 | const dashArray = 2; 73 | // Start to 0 and will be decremented to show the dashed line 74 | const dashOffset = 0; 75 | // The ratio between that is visible and other 76 | const dashRatio = 1 - (visibleLength * 0.5); // Have to be between 0.5 and 1. 77 | 78 | const material = new MeshLineMaterial({ 79 | lineWidth: width, 80 | dashArray, 81 | dashOffset, 82 | dashRatio, // The ratio between that is visible or not for each dash 83 | opacity, 84 | transparent: true, 85 | depthWrite: false, 86 | color, 87 | }); 88 | 89 | // * ****************************** 90 | // * Init 91 | super(geometry, material); 92 | this.position.copy(position); 93 | 94 | this.speed = speed; 95 | this.voidLength = dashArray * dashRatio; // When the visible part is out 96 | this.dashLength = dashArray - this.voidLength; 97 | 98 | this.dyingAt = 1; 99 | this.diedAt = this.dyingAt + this.dashLength; 100 | 101 | // Bind 102 | this.update = this.update.bind(this); 103 | } 104 | 105 | 106 | /** 107 | * * ******************* 108 | * * UPDATE 109 | * * ******************* 110 | */ 111 | update() { 112 | // Increment the dash 113 | this.material.uniforms.dashOffset.value -= this.speed; 114 | 115 | // TODO make that into a decorator 116 | // Reduce the opacity then the dash start to desapear 117 | if (this.isDying()) { 118 | this.material.uniforms.opacity.value = 0.9 + ((this.material.uniforms.dashOffset.value + 1) / this.dashLength); 119 | } 120 | } 121 | 122 | 123 | /** 124 | * * ******************* 125 | * * CONDITIONS 126 | * * ******************* 127 | */ 128 | isDied() { 129 | return this.material.uniforms.dashOffset.value < -this.diedAt; 130 | } 131 | 132 | isDying() { 133 | return this.material.uniforms.dashOffset.value < -this.dyingAt; 134 | } 135 | } -------------------------------------------------------------------------------- /app/objects/AnimatedText3D.js: -------------------------------------------------------------------------------- 1 | import { Object3D, ShapeGeometry, MeshBasicMaterial, Mesh, FontLoader } from 'three'; 2 | import { TimelineLite, Back } from 'gsap'; 3 | 4 | import fontFile from 'utils/fontFile'; 5 | 6 | const fontLoader = new FontLoader(); 7 | const font = fontLoader.parse(fontFile); 8 | 9 | export default class AnimatedText3D extends Object3D { 10 | constructor(text, { size = 0.8, letterSpacing = 0.03, color = '#000000', duration = 0.6, opacity = 1, wireframe = false } = {}) { 11 | super(); 12 | 13 | this.basePosition = 0; 14 | this.size = size; 15 | 16 | const letters = [...text]; 17 | letters.forEach((letter) => { 18 | if (letter === ' ') { 19 | this.basePosition += size * 0.5; 20 | } else { 21 | const geom = new ShapeGeometry( 22 | font.generateShapes(letter, size, 1), 23 | ); 24 | geom.computeBoundingBox(); 25 | const mat = new MeshBasicMaterial({ 26 | color, 27 | opacity: 0, 28 | transparent: true, 29 | wireframe, 30 | }); 31 | const mesh = new Mesh(geom, mat); 32 | 33 | mesh.position.x = this.basePosition; 34 | this.basePosition += geom.boundingBox.max.x + letterSpacing; 35 | this.add(mesh); 36 | } 37 | }); 38 | 39 | // Timeline 40 | this.tm = new TimelineLite({ paused: true }); 41 | this.tm.set({}, {}, `+=${duration * 1.1}`) 42 | this.children.forEach((letter) => { 43 | const data = { 44 | opacity: 0, 45 | position: -0.5, 46 | }; 47 | this.tm.to(data, duration, { 48 | opacity, 49 | position: 0, 50 | ease: Back.easeOut.config(2), 51 | onUpdate: () => { 52 | letter.material.opacity = data.opacity; 53 | letter.position.y = data.position; 54 | letter.position.z = data.position * 2; 55 | letter.rotation.x = data.position * 2; 56 | } 57 | }, `-=${duration - 0.03}`); 58 | }); 59 | 60 | // Bind 61 | this.show = this.show.bind(this); 62 | this.hide = this.hide.bind(this); 63 | } 64 | 65 | show() { 66 | this.tm.play(); 67 | } 68 | 69 | hide() { 70 | this.tm.reverse(); 71 | } 72 | } -------------------------------------------------------------------------------- /app/objects/LineGenerator.js: -------------------------------------------------------------------------------- 1 | import { Object3D } from 'three'; 2 | import AnimatedMeshLine from './AnimatedMeshLine'; 3 | 4 | export default class LineGenerator extends Object3D { 5 | constructor({ frequency = 0.1 } = {}, lineProps) { 6 | super(); 7 | 8 | this.frequency = frequency; 9 | this.lineStaticProps = lineProps; 10 | 11 | this.isStarted = false; 12 | 13 | this.i = 0; 14 | this.lines = []; 15 | this.nbrOfLines = -1; 16 | 17 | 18 | this.update = this.update.bind(this); 19 | this.start = this.start.bind(this); 20 | this.stop = this.stop.bind(this); 21 | } 22 | 23 | 24 | /** 25 | * * ******************* 26 | * * ANIMATION 27 | * * ******************* 28 | */ 29 | start() { 30 | this.isStarted = true; 31 | } 32 | 33 | stop(callback) { 34 | this.isStarted = false; 35 | // TODO callback when all lines are hidden 36 | } 37 | 38 | /** 39 | * * ******************* 40 | * * LINES 41 | * * ******************* 42 | */ 43 | addLine(props) { 44 | const line = new AnimatedMeshLine(Object.assign({}, this.lineStaticProps, props)); 45 | this.lines.push(line); 46 | this.add(line); 47 | this.nbrOfLines++; 48 | return line; 49 | } 50 | 51 | removeLine(line) { 52 | this.remove(line); 53 | this.nbrOfLines--; 54 | } 55 | 56 | 57 | /** 58 | * * ******************* 59 | * * UPDATE 60 | * * ******************* 61 | */ 62 | update() { 63 | // Add lines randomly 64 | if (this.isStarted && Math.random() < this.frequency) this.addLine(); 65 | 66 | // Update current Lines 67 | for (this.i = this.nbrOfLines; this.i >= 0; this.i--) { 68 | this.lines[this.i].update(); 69 | } 70 | 71 | // Filter and remove died lines 72 | const filteredLines = []; 73 | for (this.i = this.nbrOfLines; this.i >= 0; this.i--) { 74 | if (this.lines[this.i].isDied()) { 75 | this.removeLine(this.lines[this.i]); 76 | } else { 77 | filteredLines.push(this.lines[this.i]); 78 | } 79 | } 80 | this.lines = filteredLines; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/objects/Stars.js: -------------------------------------------------------------------------------- 1 | import { 2 | SphereBufferGeometry, MeshBasicMaterial, Mesh, Object3D, 3 | } from 'three'; 4 | 5 | import getRandomFloat from 'utils/getRandomFloat'; 6 | 7 | const starGeometry = new SphereBufferGeometry(0.5, 2, 2); 8 | const starMaterial = new MeshBasicMaterial({ color: 0xECF0F1, transparent: true, opacity: 0.3 }); 9 | 10 | class Star extends Mesh { 11 | constructor() { 12 | super(starGeometry, starMaterial); 13 | 14 | this.t = Math.random() * 10; 15 | this.position.set( 16 | Math.random() - 0.5, 17 | Math.random() - 0.5, 18 | -Math.random() * 0.5 19 | ).normalize().multiplyScalar(getRandomFloat(100, 300)); 20 | 21 | this.update = this.update.bind(this); 22 | } 23 | 24 | update() { 25 | this.t += 0.01; 26 | this.scale.x = this.scale.y = this.scale.z = Math.sin( this.t ) + 1; 27 | } 28 | } 29 | 30 | /** 31 | * * ******************* 32 | * * MAIN 33 | * * ******************* 34 | */ 35 | export default class Starts extends Object3D { 36 | constructor(nbrOfStars = 300) { 37 | super(); 38 | 39 | // TODO make instancied Stars 40 | for (let i = 0; i < nbrOfStars; i++) { 41 | const star = new Star(); 42 | this.add(star); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/utils/engine.js: -------------------------------------------------------------------------------- 1 | import { 2 | WebGLRenderer, PerspectiveCamera, Color, Scene, 3 | } from 'three'; 4 | 5 | export default class Engine { 6 | constructor(w, h, { backgroundColor, z = 10 } = {}) { 7 | this.width = w; 8 | this.height = h; 9 | this.meshCount = 0; 10 | this.meshListeners = []; 11 | this.devicePixelRatio = window.devicePixelRatio ? Math.min(1.6, window.devicePixelRatio) : 1; 12 | this.renderer = new WebGLRenderer({ antialias: true, alpha: true }); 13 | this.renderer.setPixelRatio(this.devicePixelRatio); 14 | if (backgroundColor !== undefined) this.renderer.setClearColor(new Color(backgroundColor)); 15 | this.scene = new Scene(); 16 | this.camera = new PerspectiveCamera(50, this.width / this.height, 1, 1000); 17 | this.camera.position.set(0, 0, z); 18 | 19 | this.dom = this.renderer.domElement; 20 | 21 | this.update = this.update.bind(this); 22 | this.resize = this.resize.bind(this); 23 | } 24 | 25 | /** 26 | * * ******************* 27 | * * SCENE MANAGMENT 28 | * * ******************* 29 | */ 30 | add(mesh) { 31 | this.scene.add(mesh); 32 | if (!mesh.update) return; 33 | this.meshListeners.push(mesh.update); 34 | this.meshCount++; 35 | } 36 | remove(mesh) { 37 | this.scene.remove(mesh); 38 | if (!mesh.update) return; 39 | const index = this.meshListeners.indexOf(mesh.update); 40 | if (index > -1) this.meshListeners.splice(index, 1); 41 | this.meshCount--; 42 | } 43 | 44 | start() { 45 | this.update(); 46 | } 47 | 48 | // Update render 49 | update() { 50 | let i = this.meshCount; 51 | while (--i >= 0) { 52 | this.meshListeners[i].apply(this, null); 53 | } 54 | this.render(); 55 | // Loop 56 | requestAnimationFrame(this.update); 57 | } 58 | 59 | render() { 60 | this.renderer.render(this.scene, this.camera); 61 | } 62 | 63 | // Resize 64 | resize(w, h) { 65 | this.width = w; 66 | this.height = h; 67 | this.camera.aspect = this.width / this.height; 68 | this.camera.updateProjectionMatrix(); 69 | this.resizeRender(); 70 | } 71 | 72 | resizeRender() { 73 | this.renderer.setSize(this.width, this.height); 74 | } 75 | } -------------------------------------------------------------------------------- /app/utils/getRandomFloat.js: -------------------------------------------------------------------------------- 1 | export default (min, max) => (Math.random() * (max - min)) + min; -------------------------------------------------------------------------------- /app/utils/getRandomInt.js: -------------------------------------------------------------------------------- 1 | export default (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; 2 | -------------------------------------------------------------------------------- /app/utils/getRandomItem.js: -------------------------------------------------------------------------------- 1 | import getRandomInt from './getRandomInt'; 2 | 3 | export default arr => arr[getRandomInt(0, arr.length - 1)]; -------------------------------------------------------------------------------- /app/utils/hasTouch.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/23885255/how-to-remove-ignore-hover-css-style-on-touch-devices 2 | export default () => 'ontouchstart' in document.documentElement || 3 | navigator.maxTouchPoints > 0 || 4 | navigator.msMaxTouchPoints > 0 5 | ; -------------------------------------------------------------------------------- /article.html: -------------------------------------------------------------------------------- 1 | Five animated demonstrations of Webgl lines created with the THREE.MeshLine library. You can read here how animate and build these lines to create your own animations. 2 | 3 | Animated Mesh Lines Demo 4 | 5 | 6 | Two years ago, I started playing with lines in Webgl with THREE.MeshLine, a library made by Jaume Sanchez Elias for Three.js. 7 | This library builds a strip of triangles billboarded to create a custom geometry and solves the fact that you can not handle the width of your lines with the classic lines. Indeed, the native Webgl GL_LINE method used does no support the width parameter. 8 | 9 | Cheaper in vertices than a TubeGeometry (usually used to create thick lines), the ribbon shape of its lines are also interesting! 10 | 11 | 12 |

Animate a MeshLine

13 | 14 | The only thing missed was the possibility to animate lines without having to rebuild the geometry each frame. 15 | Based on what had already been started and how SVG Line animation works, I implemented to the library 3 new parameters into the MeshLineMaterial to visualize animated dashed line directly through the sander. 16 | 17 | 22 | 23 | 24 | Like with a SVG path, there parameters correctly handled allow you to animate the entire traced line. 25 | Here is a complete example of how to create and animate a MeshLine: 26 | 27 |

 28 |   // Build an array of points
 29 |   const segmentLength = 1;
 30 |   const nbrOfPoints = 10;
 31 |   const points = [];
 32 |   for (let i = 0; i < nbrOfPoints; i++) {
 33 |     points.push(i * segmentLength, 0, 0);
 34 |   }
 35 | 
 36 |   // Build the geometry
 37 |   const line = new MeshLine();
 38 |   line.setGeometry(points);
 39 |   const geometry = line.geometry;
 40 | 
 41 |   // Build the material with good parameters to animate it.
 42 |   const material = new MeshLineMaterial({
 43 |     lineWidth: 0.1,
 44 |     color: new Color('#ff0000'),
 45 |     transparent: true,
 46 |     dashArray: 2,     // always has to be the double of the line
 47 |     dashOffset: 0,    // start the dash at zero
 48 |     dashRatio: 0.75,  // visible length range min: 0.99, max: 0.5
 49 |   });
 50 | 
 51 |   // Build the Mesh
 52 |   const lineMesh = new Mesh(geometry, material);
 53 |   lineMesh.position.x = -4.5;
 54 | 
 55 |   // ! Assuming you have your own webgl engine to add meshes on scene and update them.
 56 |   webgl.add(lineMesh);
 57 | 
 58 |   // ! Called each frame
 59 |   function update() {
 60 |     // Check if the dash is out to stop animate it.
 61 |     if (lineMesh.material.uniforms.dashOffset.value < -2) return;
 62 | 
 63 |     // Decrement the dashOffset value to animate the path with the dash.
 64 |     lineMesh.material.uniforms.dashOffset.value -= 0.01;
 65 |   }
 66 | 
67 | 68 | First animated MeshLine 69 | 70 | 71 |

Create your own line style

72 | 73 | Now that you know how to animate your lines, I will propose you some tips to customize the shape of your lines! 74 | Many of these methods are taken from the library examples. Feel free to explore! 75 | 76 |

Use SplineCurve or CatmullRomCurve3

77 | 78 | These classes smooth an array of points roughly positioned. 79 | They are perfect to build curved and fluid lines and keep the control of them (length, orientation, turbulences...). 80 | 81 | For instance, let's add some turbulences at our previous array of points: 82 |

 83 |   const segmentLength = 1;
 84 |   const nbrOfPoints = 10;
 85 |   const points = [];
 86 |   const turbulence = 0.5;
 87 |   for (let i = 0; i < nbrOfPoints; i++) {
 88 |     // ! We have to wrapped points into a THREE.Vector3 this time
 89 |     points.push(new Vector3(
 90 |       i * segmentLength,
 91 |       (Math.random() * (turbulence * 2)) - turbulence,
 92 |       (Math.random() * (turbulence * 2)) - turbulence,
 93 |     ));
 94 |   }
 95 | 
96 | 97 | Then, use one of these classes to smooth your array of lines before you create the geometry: 98 | 99 |

100 |   // 2D spline
101 |   // const linePoints = new Geometry().setFromPoints(new SplineCurve(points).getPoints(50));
102 | 
103 |   // 3D spline
104 |   const linePoints = new Geometry().setFromPoints(new CatmullRomCurve3(points).getPoints(50));
105 | 
106 |   const line = new MeshLine();
107 |   line.setGeometry(linePoints);
108 |   const geometry = line.geometry;
109 | 
110 | 111 | And you have your smooth curved line ! 112 | 113 | Animated MeshLine Curved 114 | 115 | Note that SplineCurve only smoothes in 2D (x and y axis) compared to CatmullRomCurve3 who takes in account the 3 axes. 116 | 117 | I recommend anyway to use the SplineCurve. It requires less performances to calculate lines and is often enough to create the curved effect requested 118 | For instance, my demos Confettis and Energy are only made with the SplineCurve method: 119 | 120 |

121 | AnimatedMeshLine - Confettis demo 122 |

123 |

124 | AnimatedMeshLine - Energy demo 125 |

126 | 127 |
128 | 129 | 130 | 131 |

Use Raycasting

132 | 133 | Another technique extracted from a THREE.MeshLine example is to use a Raycaster to scan a Mesh already present on the scene. 134 | Thus, you can create your lines that follow the shape of the object: 135 | 136 |

137 |   const radius = 4;
138 |   const yMax = -4;
139 |   const points = [];
140 |   const origin = new Vector3();
141 |   const direction = new Vector3();
142 |   const raycaster = new Raycaster();
143 | 
144 |   let y = 0;
145 |   let angle = 0;
146 |   // Start the scan
147 |   while (y < yMax) {
148 |     // Update the orientation and the position of the raycaster
149 |     y -= 0.1;
150 |     angle += 0.2;
151 |     origin.set(radius * Math.cos(angle), y, radius * Math.sin(angle));
152 |     direction.set(-origin.x, 0, -origin.z);
153 |     direction.normalize();
154 |     raycaster.set(origin, direction);
155 | 
156 |     // Save the coordinates raycsted.
157 |     // !Assuming the raycaster cross the object in the scene each time
158 |     const intersect = raycaster.intersectObject(objectToRaycast, true);
159 |     if (intersect.length) {
160 |       points.push(
161 |         intersect[0].point.x,
162 |         intersect[0].point.y,
163 |         intersect[0].point.z,
164 |       );
165 |     }
166 |   }
167 | 
168 | 169 | 170 | I used this method for the Boreal Sky demo. I used a sphere part as objectToRaycast: 171 | 172 | Boreal Sky - raycasting example 173 | 174 | 175 | Now, you have enough tools to explore and play with animated MeshLines. Feel free to share your own experiments and methods to create your lines! 176 | 177 | 178 |

References and Credits

179 | 185 | -------------------------------------------------------------------------------- /config/webpack.commun.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const webpack = require('webpack'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const poststylus = require('poststylus'); 7 | 8 | 9 | // Paths 10 | const distPath = path.resolve(__dirname, '../dist'); 11 | const nodeModulesPath = path.resolve(__dirname, './node_modules'); 12 | const demosPath = path.resolve(__dirname, '../app/demos'); 13 | 14 | // Init props 15 | const entries = {}; 16 | const plugins = [ 17 | new webpack.LoaderOptionsPlugin({ 18 | debug: true, 19 | options: { 20 | stylus: { 21 | use: [poststylus(['autoprefixer'])], 22 | }, 23 | }, 24 | }), 25 | ]; 26 | 27 | // * Dynamic entry points for each demos 28 | const dirs = fs.readdirSync(demosPath); 29 | dirs.forEach(dir => { 30 | // Set each entry for each demo 31 | entries[dir] = `${demosPath}/${dir}/index.js`; 32 | 33 | // Add an html webpack plugin for each entry 34 | plugins.push(new HtmlWebpackPlugin({ 35 | // inject: false, 36 | chunks: [dir, 'vendors'], 37 | filename: `${distPath}/${dir}.html`, 38 | template: `${demosPath}/${dir}/index.html`, 39 | })); 40 | }); 41 | 42 | // * WEBPACK CONFIG 43 | module.exports = { 44 | // node: { 45 | // fs: 'empty' 46 | // }, 47 | mode: 'development', 48 | entry: entries, 49 | output: { 50 | filename: '[name].js', 51 | path: distPath, 52 | // publicPath: myLocalIp, 53 | // publicPath: '/', 54 | }, 55 | resolve: { 56 | alias: { 57 | App: path.resolve(__dirname, '../app/app.js'), 58 | utils: path.resolve(__dirname, '../app/utils'), 59 | objects: path.resolve(__dirname, '../app/objects'), 60 | decorators: path.resolve(__dirname, '../app/decorators'), 61 | }, 62 | }, 63 | module: { 64 | rules: [{ 65 | test: /\.jsx?$/, 66 | loader: 'babel-loader', 67 | exclude: nodeModulesPath, 68 | }, 69 | { 70 | test: /\.(styl|css)$/, 71 | use: [ 72 | { 73 | loader: 'style-loader', 74 | options: { 75 | // sourceMap: true 76 | }, 77 | }, 78 | { 79 | loader: 'css-loader', 80 | options: { 81 | // sourceMap: true 82 | }, 83 | }, 84 | { 85 | loader: 'stylus-loader', 86 | options: { 87 | // sourceMap: true 88 | }, 89 | }, 90 | ], 91 | }, 92 | // { 93 | // test: /\.(png|jpe?g|gif)$/, 94 | // loader: 'file-loader?name=imgs/[hash].[ext]', 95 | // }, 96 | // { 97 | // test: /\.(eot|svg|ttf|woff(2)?)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 98 | // loader: 'file-loader?name=fonts/[name].[ext]', 99 | // }, 100 | { 101 | test: /\.(glsl|frag|vert)$/, 102 | exclude: nodeModulesPath, 103 | loader: 'raw-loader' 104 | }, 105 | { 106 | test: /\.(glsl|frag|vert)$/, 107 | exclude: nodeModulesPath, 108 | loader: 'glslify-loader' 109 | }], 110 | }, 111 | plugins: plugins, 112 | }; -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ip = require('ip'); 3 | const merge = require('webpack-merge'); 4 | 5 | const webpackCommunConfig = require('./webpack.commun'); 6 | 7 | 8 | const port = 3333; 9 | const ipAdress = ip.address() + ':' + port; 10 | // const myLocalIp = 'http://' + ipAdress + '/'; 11 | 12 | // MERGE 13 | module.exports = merge(webpackCommunConfig, { 14 | mode: 'development', 15 | output: { 16 | devtoolModuleFilenameTemplate: 'webpack:///[absolute-resource-path]', 17 | }, 18 | plugins: [ 19 | new webpack.HotModuleReplacementPlugin(), 20 | ], 21 | devtool: 'eval-source-map', 22 | // devtool: 'inline-source-map', 23 | devServer: { 24 | // compress: true, 25 | // headers: { 26 | // 'Access-Control-Allow-Origin': '*', 27 | // 'Access-Control-Allow-Credentials': 'true', 28 | // }, 29 | contentBase: webpackCommunConfig.output.path, 30 | historyApiFallback: true, 31 | disableHostCheck: true, 32 | host: '0.0.0.0', 33 | hot: true, 34 | inline: true, 35 | port: port, 36 | // Release of webpack-dev-server 2.4.3 => https://github.com/webpack/webpack-dev-server/issues/882 37 | public: ipAdress, 38 | }, 39 | }); -------------------------------------------------------------------------------- /config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | const merge = require('webpack-merge'); 5 | const TerserPlugin = require('terser-webpack-plugin'); 6 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 7 | 8 | const webpackCommunConfig = require('./webpack.commun'); 9 | 10 | 11 | // HACK 12 | // Inject into the css the extracter loader instead of the style-loader 13 | webpackCommunConfig.module.rules[1].use[0] = MiniCssExtractPlugin.loader; 14 | 15 | // MERGE 16 | module.exports = merge(webpackCommunConfig, { 17 | mode: 'production', 18 | plugins: [ 19 | new CleanWebpackPlugin([webpackCommunConfig.output.path], { root: path.resolve(__dirname, '..'), verbose: true }), 20 | new MiniCssExtractPlugin({ 21 | // Options similar to the same options in webpackOptions.output 22 | // both options are optional 23 | filename: '[name].css', 24 | chunkFilename: 'vendors.css', 25 | }), 26 | ], 27 | optimization: { 28 | nodeEnv: 'production', 29 | minimizer: [ 30 | new TerserPlugin(), 31 | new OptimizeCSSAssetsPlugin({}), 32 | ], 33 | splitChunks: { 34 | // include all types of chunks (JS, CCS, ...) 35 | chunks: 'all', 36 | name: 'vendors', 37 | }, 38 | }, 39 | }); -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeremboo/animated-mesh-lines/37a2ea9edc11e6b57b279518cf6e33e01669ae95/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animated-mesh-lines", 3 | "version": "0.0.0", 4 | "description": "", 5 | "author": { 6 | "name": "Jérémie Boulay", 7 | "email": "jeremi.boulay@gmail.com", 8 | "url": "https://jeremieboulay.fr" 9 | }, 10 | "scripts": { 11 | "start": "webpack-dev-server --config config/webpack.dev.js", 12 | "build": "webpack --progress --config config/webpack.prod.js" 13 | }, 14 | "license": "ISC", 15 | "repository": "https://github.com/Jeremboo/animated-mesh-lines.git", 16 | "dependencies": { 17 | "dat.gui": "^0.7.3", 18 | "gsap": "^2.0.2", 19 | "postprocessing": "^5.3.2", 20 | "three": "^0.98.0", 21 | "three.meshline": "^1.1.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/cli": "^7.1.2", 25 | "@babel/core": "^7.1.2", 26 | "@babel/plugin-proposal-decorators": "^7.2.2", 27 | "@babel/preset-env": "^7.1.0", 28 | "autoprefixer": "^6.7.7", 29 | "babel-loader": "^8.0.4", 30 | "babelify": "^10.0.0", 31 | "clean-webpack-plugin": "^1.0.0", 32 | "css-loader": "^2.0.0", 33 | "html-webpack-plugin": "^3.2.0", 34 | "ip": "^1.1.5", 35 | "mini-css-extract-plugin": "^0.5.0", 36 | "optimize-css-assets-webpack-plugin": "^5.0.1", 37 | "poststylus": "^1.0.0", 38 | "raw-loader": "^1.0.0", 39 | "style-loader": "^0.23.1", 40 | "stylus": "^0.54.5", 41 | "stylus-loader": "^3.0.2", 42 | "terser-webpack-plugin": "^1.1.0", 43 | "webpack": "^4.27.1", 44 | "webpack-cli": "^3.1.2", 45 | "webpack-dev-server": "^3.1.14", 46 | "webpack-merge": "^4.1.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /previews/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeremboo/animated-mesh-lines/37a2ea9edc11e6b57b279518cf6e33e01669ae95/previews/preview.gif -------------------------------------------------------------------------------- /previews/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeremboo/animated-mesh-lines/37a2ea9edc11e6b57b279518cf6e33e01669ae95/previews/preview.png -------------------------------------------------------------------------------- /previews/preview2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeremboo/animated-mesh-lines/37a2ea9edc11e6b57b279518cf6e33e01669ae95/previews/preview2.gif -------------------------------------------------------------------------------- /previews/preview3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeremboo/animated-mesh-lines/37a2ea9edc11e6b57b279518cf6e33e01669ae95/previews/preview3.gif -------------------------------------------------------------------------------- /previews/preview4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeremboo/animated-mesh-lines/37a2ea9edc11e6b57b279518cf6e33e01669ae95/previews/preview4.gif -------------------------------------------------------------------------------- /previews/preview5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeremboo/animated-mesh-lines/37a2ea9edc11e6b57b279518cf6e33e01669ae95/previews/preview5.gif -------------------------------------------------------------------------------- /previews/preview_all.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeremboo/animated-mesh-lines/37a2ea9edc11e6b57b279518cf6e33e01669ae95/previews/preview_all.gif -------------------------------------------------------------------------------- /previews/preview_sphere.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeremboo/animated-mesh-lines/37a2ea9edc11e6b57b279518cf6e33e01669ae95/previews/preview_sphere.gif --------------------------------------------------------------------------------