├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
18 | DashRatio: The ratio between that is visible or not (~0
: more visible, ~1
: less visible)
19 | DashArray: The length of a dash and her space (0 == no dash)
20 | DashOffset: The location where the first dash begin
21 |
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 |
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 |
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 |
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 |
122 |
123 |
124 |
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 |
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
--------------------------------------------------------------------------------