├── .babelrc
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .stylelintrc.json
├── README.md
├── favicon.ico
├── index.html
├── package.json
├── src
├── glsl
│ └── cover
│ │ ├── bgcover.glsl
│ │ ├── fragment.glsl
│ │ ├── index.js
│ │ ├── rotateUV.glsl
│ │ ├── scaleUV.glsl
│ │ └── vertex.glsl
├── img
│ └── portraits
│ │ ├── 01.jpg
│ │ ├── 02.jpg
│ │ ├── 03.jpg
│ │ ├── 04.jpg
│ │ ├── 05.jpg
│ │ └── 06.jpg
├── js
│ ├── Cloth.js
│ ├── Layout.js
│ ├── O.js
│ ├── Scene.js
│ ├── Slideshow.js
│ ├── Tile.js
│ ├── Wind.js
│ ├── index.js
│ └── utils
│ │ ├── CannonDebugRenderer.js
│ │ └── index.js
└── sass
│ ├── _base.scss
│ ├── _deco.scss
│ ├── _main.scss
│ ├── _reset.scss
│ ├── _slider.scss
│ ├── _typography.scss
│ ├── _utils.scss
│ └── styles.scss
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "useBuiltIns": "entry",
7 | "targets": {
8 | "browsers": ["ie >= 10", "> 1%"]
9 | }
10 | }
11 | ]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | end_of_line = lf
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
14 | [*.yml]
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "plugins": [
4 | "import"
5 | ],
6 | "parser": "babel-eslint",
7 | "env": { "browser": true },
8 | "globals": {
9 | "APP": true,
10 | "Linear": true,
11 | "Cubic": true,
12 | "Quad": true,
13 | "Expo": true,
14 | "Power1": true,
15 | "Power2": true,
16 | "Power3": true,
17 | "Power4": true,
18 | "Elastic": true,
19 | "Back": true,
20 | "Strong": true,
21 | "ExpoScaleEase": true,
22 | "TimelineMax": true,
23 | "ga": true
24 | },
25 | "rules": {
26 | "no-use-before-define": "off",
27 | "react/forbid-prop-types": "off",
28 | "react/jsx-filename-extension": ["error", { "extensions": [".js"] }],
29 | "semi": ["error", "never"],
30 | "key-spacing": [0],
31 | "no-multi-spaces": [1, {
32 | "exceptions": {
33 | "Property": true,
34 | "VariableDeclarator": true,
35 | "AssignmentExpression": true
36 | }
37 | }],
38 | "no-multiple-empty-lines": [1, {
39 | "max": 4
40 | }],
41 | "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
42 | "max-len": [1, 130],
43 | "indent": ["error", 4, { "SwitchCase": 1 }],
44 | "no-param-reassign": [2, { "props": false }],
45 | "padded-blocks": ["error", { "blocks": "never", "classes": "always" }],
46 | "object-curly-newline": ["error", {
47 | "ObjectExpression": { "minProperties": 6, "multiline": true, "consistent": true },
48 | "ObjectPattern": { "minProperties": 6, "multiline": true, "consistent": true }
49 | }]
50 | }
51 | }
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | dist
3 | .DS_Store
4 | package-lock.json
5 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard",
3 | "plugins": [
4 | "stylelint-scss",
5 | "stylelint-order"
6 | ],
7 | "rules": {
8 | "indentation": [4, {
9 | "ignore": ["value"]
10 | }],
11 |
12 |
13 | "max-empty-lines": 4,
14 | "block-opening-brace-space-before": "always-multi-line",
15 | "string-quotes": "single",
16 | "color-hex-case": "lower",
17 | "color-hex-length": "short",
18 | "color-named": "never",
19 |
20 |
21 | "selector-combinator-space-after": "always",
22 | "selector-attribute-quotes": "always",
23 | "selector-attribute-operator-space-before": "never",
24 | "selector-attribute-operator-space-after": "never",
25 | "selector-attribute-brackets-space-inside": "never",
26 |
27 |
28 | "declaration-block-single-line-max-declarations": 5,
29 | "declaration-block-trailing-semicolon": "always",
30 | "declaration-colon-space-before": "never",
31 | "declaration-colon-space-after": "always",
32 | "declaration-empty-line-before": null,
33 |
34 |
35 | "property-no-vendor-prefix": true,
36 | "value-no-vendor-prefix": true,
37 | "number-leading-zero": "never",
38 | "function-url-quotes": "always",
39 | "font-weight-notation": "numeric",
40 | "font-family-name-quotes": "always-unless-keyword",
41 | "comment-whitespace-inside": "always",
42 | "comment-empty-line-before": "always",
43 |
44 |
45 | "at-rule-no-vendor-prefix": true,
46 | "at-rule-no-unknown": [true, {
47 | "ignoreAtRules": ["mixin", "media", "content", "if", "else", "include", "extend", "for", "function", "return"]
48 | }],
49 | "at-rule-empty-line-before": ["always", {
50 | "except": ["after-same-name", "first-nested"],
51 | "ignore": ["after-comment", "blockless-after-same-name-blockless"],
52 | "ignoreAtRules": ["include"]
53 | }],
54 |
55 | "rule-empty-line-before": ["always-multi-line", {
56 | "except": ["first-nested"],
57 | "ignore": ["after-comment"]
58 | }],
59 |
60 |
61 | "selector-pseudo-element-colon-notation": "double",
62 | "selector-pseudo-class-parentheses-space-inside": "never",
63 |
64 |
65 | "media-feature-range-operator-space-before": "always",
66 | "media-feature-range-operator-space-after": "always",
67 | "media-feature-parentheses-space-inside": "never",
68 | "media-feature-colon-space-before": "never",
69 | "media-feature-colon-space-after": "always",
70 |
71 |
72 | "order/properties-order": [
73 | {
74 | "emptyLineBefore": "always",
75 | "properties": [
76 | "content",
77 | "counter-reset",
78 | "counter-increment"
79 | ]
80 | },
81 | {
82 | "emptyLineBefore": "always",
83 | "properties": [
84 | "display",
85 | "visibility",
86 | "order",
87 | "clear",
88 | "float",
89 | "vertical-align",
90 | "align-self",
91 | "flex",
92 | "flex-shrink",
93 | "flex-direction",
94 | "flex-wrap",
95 | "flex-basis",
96 | "justify-content",
97 | "align-items",
98 | "overflow-scrolling",
99 | "overflow",
100 | "overflow-x",
101 | "overflow-y",
102 | "overflow-scrolling",
103 | "position",
104 | "top",
105 | "right",
106 | "bottom",
107 | "left",
108 | "z-index",
109 | "width",
110 | "height",
111 | "min-width",
112 | "min-height",
113 | "max-width",
114 | "max-height",
115 | "margin",
116 | "margin-top",
117 | "margin-right",
118 | "margin-bottom",
119 | "margin-left",
120 | "padding",
121 | "padding-top",
122 | "padding-right",
123 | "padding-bottom",
124 | "padding-left"
125 | ]
126 | },
127 | {
128 | "emptyLineBefore": "always",
129 | "properties": [
130 | "font-smoothing",
131 | "text-rendering",
132 | "white-space",
133 | "text-align",
134 | "text-indent",
135 | "font",
136 | "font-family",
137 | "font-style",
138 | "font-size",
139 | "font-weight",
140 | "line-height",
141 | "text-shadow",
142 | "text-decoration",
143 | "text-transform",
144 | "letter-spacing",
145 | "color"
146 | ]
147 | },
148 | {
149 | "emptyLineBefore": "always",
150 | "properties": [
151 | "appearance",
152 | "background",
153 | "background-image",
154 | "background-repeat",
155 | "background-size",
156 | "background-position",
157 | "background-color",
158 | "border",
159 | "border-width",
160 | "border-style",
161 | "border-color",
162 | "border-top",
163 | "border-right",
164 | "border-right-width",
165 | "border-right-style",
166 | "border-right-color",
167 | "border-bottom",
168 | "border-bottom-width",
169 | "border-bottom-style",
170 | "border-bottom-color",
171 | "border-left",
172 | "border-left-width",
173 | "border-left-style",
174 | "border-left-color",
175 | "border-radius",
176 | "border-top-right-radius",
177 | "border-bottom-right-radius",
178 | "border-bottom-left-radius",
179 | "border-top-left-radius",
180 | "fill",
181 | "stroke",
182 | "stroke-width",
183 | "stroke-linecap",
184 | "outline",
185 | "box-shadow",
186 | "opacity",
187 | "clip"
188 | ]
189 | },
190 | {
191 | "emptyLineBefore": "always",
192 | "properties": [
193 | "cursor",
194 | "pointer-events"
195 | ]
196 | },
197 | {
198 | "emptyLineBefore": "always",
199 | "properties": [
200 | "will-change",
201 | "backface-visibility",
202 | "transform-origin",
203 | "transform",
204 | "animation",
205 | "transition",
206 | "transition-property",
207 | "transition-duration",
208 | "transition-delay"
209 | ]
210 | }
211 | ]
212 | }
213 | }
214 |
215 |
216 |
217 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Physics-based 3D cloth using Three.js and Cannon.js
2 |
3 | Learn how to create a physics-based 3D cloth with [Cannon.js](https://github.com/schteppe/cannon.js) and [Three.js](https://threejs.org/) and use simplex noise to create a wind effect. By [Arno Di Nunzio](https://twitter.com/aqro)
4 |
5 | 
6 |
7 | [Article on Codrops](https://tympanus.net/codrops/?p=46823)
8 |
9 | [Demo](http://tympanus.net/Tutorials/3DClothSlideshow/)
10 |
11 | ## Installation
12 | Run this command in the terminal
13 | ```
14 | npm install
15 | ```
16 |
17 | Compile the code
18 | ```
19 | npm run build
20 | ```
21 |
22 | Compile the code with a local server
23 | ```
24 | npm run watch
25 | ```
26 |
27 | ## Credits
28 |
29 | - Slideshow based on [Jesper Landberg's](https://twitter.com/Jesper_Landberg) [slider](https://codepen.io/ReGGae/pen/povjKxV)
30 | - [Three.js](https://threejs.org/)
31 | - [Cannon.js](https://github.com/schteppe/cannon.js)
32 | - [Simplex noise](https://www.npmjs.com/package/simplex-noise)
33 |
34 | ## License
35 | 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.
36 |
37 | ## Misc
38 |
39 | Follow Arno: [Twitter](https://twitter.com/aqro), [Dribbble](https://dribbble.com/Aqro), [Instagram](https://instagram.com/aqro/), [GitHub](https://github.com/Aqro)
40 |
41 | 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/)
42 |
43 |
44 | [© Codrops 2019](http://www.codrops.com)
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aqro/Physics-slideshow-threejs-cannonjs/9f934d42075033bd0b00e187bc89528b01866066/favicon.ico
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Physics 3D cloth with Three.js and Cannon.js | Codrops
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
29 |
30 |
59 |
60 |
61 | Urban
62 |
63 |
64 | jungle
65 |
66 |
67 |
68 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "build": "webpack --config webpack.config.js",
4 | "watch": "webpack --watch --config webpack.config.js"
5 | },
6 | "devDependencies": {
7 | "@babel/core": "^7.6.2",
8 | "@babel/preset-env": "^7.6.2",
9 | "babel-eslint": "^10.0.3",
10 | "babel-loader": "^8.0.6",
11 | "browser-sync": "^2.26.7",
12 | "browser-sync-webpack-plugin": "^2.2.2",
13 | "copy-webpack-plugin": "^5.0.4",
14 | "css-loader": "^3.2.0",
15 | "eslint": "^6.5.1",
16 | "eslint-config-airbnb": "^18.0.1",
17 | "eslint-loader": "^3.0.2",
18 | "eslint-plugin-import": "^2.18.2",
19 | "eslint-plugin-jsx-a11y": "^6.2.3",
20 | "eslint-plugin-react": "^7.15.0",
21 | "extract-text-webpack-plugin": "^3.0.2",
22 | "file-loader": "^4.2.0",
23 | "mini-css-extract-plugin": "^0.8.0",
24 | "node-sass": "^4.12.0",
25 | "purgecss-webpack-plugin": "^1.6.0",
26 | "raw-loader": "^3.1.0",
27 | "sass-loader": "^8.0.0",
28 | "style-loader": "^1.0.0",
29 | "stylelint": "^11.0.0",
30 | "stylelint-config-standard": "^19.0.0",
31 | "stylelint-order": "^3.1.1",
32 | "stylelint-scss": "^3.11.1",
33 | "webpack": "^4.41.0",
34 | "webpack-cli": "^3.3.9"
35 | },
36 | "dependencies": {
37 | "cannon": "^0.6.2",
38 | "glslify": "^7.0.0",
39 | "glslify-import": "^3.1.0",
40 | "glslify-loader": "^2.0.0",
41 | "gsap": "^3.0.5",
42 | "lodash": "^4.17.15",
43 | "simplex-noise": "^2.4.0",
44 | "three": "^0.109.0",
45 | "three-orbitcontrols": "^2.110.1"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/glsl/cover/bgcover.glsl:
--------------------------------------------------------------------------------
1 | vec2 cover(vec2 sz, vec2 is, vec2 uv) {
2 | float screenRatio = sz.x / sz.y;
3 | float imageRatio = is.x / is.y;
4 |
5 | vec2 newSize = screenRatio < imageRatio
6 | ? vec2(is.x * sz.y / is.y, sz.y)
7 | : vec2(sz.x, is.y * sz.x / is.x);
8 |
9 | vec2 newOffset = (screenRatio < imageRatio
10 | ? vec2((newSize.x - sz.x) / 2.0, 0.0)
11 | : vec2(0.0, (newSize.y - sz.y) / 2.0)) / newSize;
12 |
13 | return uv * sz / newSize + newOffset;
14 | }
15 |
16 | #pragma glslify: export(cover);
17 |
--------------------------------------------------------------------------------
/src/glsl/cover/fragment.glsl:
--------------------------------------------------------------------------------
1 | #pragma glslify: cover = require(./bgcover.glsl);
2 | #pragma glslify: rotateUV = require(./rotateUV.glsl);
3 | #pragma glslify: scaleUV = require(./scaleUV.glsl);
4 |
5 | varying vec2 vUv;
6 |
7 | uniform sampler2D uTexture;
8 | uniform vec2 uMeshSize;
9 | uniform vec2 uImageSize;
10 | uniform float uScale;
11 | uniform float uAngle;
12 | uniform float uAlpha;
13 |
14 | void main() {
15 |
16 | vec2 uv = vUv;
17 | // uv = scaleUV(uv, 0.7);
18 | uv -= 0.5;
19 | uv *= uScale;
20 | uv += 0.5;
21 | uv = cover(uMeshSize, uImageSize, uv);
22 |
23 | vec2 texUv = rotateUV(uv, -uAngle);
24 | vec4 img = texture2D(uTexture, texUv);
25 | gl_FragColor = vec4(img.rgb, uAlpha);
26 | }
27 |
--------------------------------------------------------------------------------
/src/glsl/cover/index.js:
--------------------------------------------------------------------------------
1 | import { ShaderMaterial, Vector2 } from 'three'
2 | import vertexShader from './vertex.glsl'
3 | import fragmentShader from './fragment.glsl'
4 |
5 | export default class CoverMaterial extends ShaderMaterial {
6 |
7 | constructor({ meshSize }) {
8 | super({
9 | vertexShader,
10 | fragmentShader,
11 | transparent: true,
12 | })
13 |
14 | this.uniforms = {
15 | uTime: { value: 0 },
16 | uAlpha: { value: 1 },
17 | uTexture: { value: 0 },
18 | uMeshSize: { value: new Vector2(meshSize.width, meshSize.height) },
19 | uImageSize: { value: new Vector2(0, 0) },
20 | uScale: { value: 1 },
21 | uVelo: { value: 0 },
22 | uAngle: { value: APP.Store.ANGLE },
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/glsl/cover/rotateUV.glsl:
--------------------------------------------------------------------------------
1 |
2 | /* Source from : https://gist.github.com/ayamflow/c06bc0c8a64f985dd431bd0ac5b557cd#file-rotate-uv-glsl-L1
3 | ---------------------------------------------------------------------------------------------------- */
4 |
5 | vec2 rotateUV(vec2 uv, float rotation)
6 | {
7 | float mid = 0.5;
8 | return vec2(
9 | cos(rotation) * (uv.x - mid) + sin(rotation) * (uv.y - mid) + mid,
10 | cos(rotation) * (uv.y - mid) - sin(rotation) * (uv.x - mid) + mid
11 | );
12 | }
13 |
14 | vec2 rotateUV(vec2 uv, float rotation, vec2 mid)
15 | {
16 | return vec2(
17 | cos(rotation) * (uv.x - mid.x) + sin(rotation) * (uv.y - mid.y) + mid.x,
18 | cos(rotation) * (uv.y - mid.y) - sin(rotation) * (uv.x - mid.x) + mid.y
19 | );
20 | }
21 |
22 | vec2 rotateUV(vec2 uv, float rotation, float mid)
23 | {
24 | return vec2(
25 | cos(rotation) * (uv.x - mid) + sin(rotation) * (uv.y - mid) + mid,
26 | cos(rotation) * (uv.y - mid) - sin(rotation) * (uv.x - mid) + mid
27 | );
28 | }
29 |
30 | #pragma glslify: export(rotateUV)
31 |
--------------------------------------------------------------------------------
/src/glsl/cover/scaleUV.glsl:
--------------------------------------------------------------------------------
1 | vec2 scaleUV(vec2 uv, float scl)
2 | {
3 | float mid = 0.5;
4 | return vec2(
5 | uv.x * scl + mid,
6 | uv.y * scl + mid
7 | );
8 | }
9 |
10 | #pragma glslify: export(scaleUV)
11 |
--------------------------------------------------------------------------------
/src/glsl/cover/vertex.glsl:
--------------------------------------------------------------------------------
1 | varying vec2 vUv;
2 |
3 | void main() {
4 | vUv = uv;
5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
6 | }
7 |
--------------------------------------------------------------------------------
/src/img/portraits/01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aqro/Physics-slideshow-threejs-cannonjs/9f934d42075033bd0b00e187bc89528b01866066/src/img/portraits/01.jpg
--------------------------------------------------------------------------------
/src/img/portraits/02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aqro/Physics-slideshow-threejs-cannonjs/9f934d42075033bd0b00e187bc89528b01866066/src/img/portraits/02.jpg
--------------------------------------------------------------------------------
/src/img/portraits/03.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aqro/Physics-slideshow-threejs-cannonjs/9f934d42075033bd0b00e187bc89528b01866066/src/img/portraits/03.jpg
--------------------------------------------------------------------------------
/src/img/portraits/04.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aqro/Physics-slideshow-threejs-cannonjs/9f934d42075033bd0b00e187bc89528b01866066/src/img/portraits/04.jpg
--------------------------------------------------------------------------------
/src/img/portraits/05.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aqro/Physics-slideshow-threejs-cannonjs/9f934d42075033bd0b00e187bc89528b01866066/src/img/portraits/05.jpg
--------------------------------------------------------------------------------
/src/img/portraits/06.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aqro/Physics-slideshow-threejs-cannonjs/9f934d42075033bd0b00e187bc89528b01866066/src/img/portraits/06.jpg
--------------------------------------------------------------------------------
/src/js/Cloth.js:
--------------------------------------------------------------------------------
1 | import C from 'cannon'
2 | import { Vector3 } from 'three'
3 | import gsap from 'gsap'
4 | import { ev } from './utils'
5 |
6 | export default class Cloth {
7 |
8 | constructor(tile, world) {
9 | this.activeTile = tile
10 | this.world = world
11 | this.totalMass = APP.Layout.isMobile ? 3 : 2
12 |
13 | const { widthSegments, heightSegments } = this.activeTile.geo.parameters
14 |
15 | this.mass = this.totalMass / (widthSegments * heightSegments)
16 |
17 | this.stitchesShape = new C.Particle()
18 | this.tempV = new Vector3()
19 |
20 | this.setStitches()
21 |
22 | this.bindEvents()
23 | }
24 |
25 |
26 | bindEvents() {
27 | document.addEventListener('stormIsCalmingDown', this.rest.bind(this))
28 | document.addEventListener('windBlowing', this.onToggleWind.bind(this))
29 | }
30 |
31 |
32 |
33 | /* Handlers
34 | --------------------------------------------------------- */
35 |
36 | onToggleWind({ detail: { windBlowing } }) {
37 | if (windBlowing) {
38 | this.stitches.forEach((s) => s.wakeUp())
39 | } else {
40 | this.stitches.forEach((s) => s.sleep())
41 | }
42 | }
43 |
44 |
45 | /* Actions
46 | --------------------------------------------------------- */
47 |
48 | update() {
49 | this.render()
50 | }
51 |
52 | forceReset() {
53 | this.stitches.forEach((stitch) => {
54 | stitch.velocity.set(0, 0, 0)
55 | stitch.position.copy(stitch.initPosition)
56 | })
57 | }
58 |
59 | render() {
60 | const { position } = this.activeTile.geo.attributes
61 | const { width, height } = this.activeTile.rect
62 |
63 | for (let i = 0; i < position.count; i++) {
64 | const p = this.tempV.copy(this.stitches[i].position)
65 |
66 | position.setXYZ(i, p.x / width, p.y / height, p.z)
67 | }
68 |
69 | position.needsUpdate = true
70 | }
71 |
72 | rest() {
73 | const positions = this.stitches.map((s) => s.position)
74 |
75 | this.stitches.forEach((s) => s.sleep())
76 |
77 | gsap.to(positions, {
78 | duration: APP.Layout.isMobile ? 0.4 : 0.8,
79 | x: (i) => this.initPositions[i].x,
80 | y: (i) => this.initPositions[i].y,
81 | z: (i) => this.initPositions[i].z,
82 | ease: 'power2.out',
83 | })
84 | }
85 |
86 | changeActiveTile(tile) {
87 | this.forceReset()
88 | this.activeTile = tile
89 | }
90 |
91 |
92 | connect(i, j) {
93 | const c = new C.DistanceConstraint(
94 | this.stitches[i],
95 | this.stitches[j],
96 | )
97 |
98 | this.world.addConstraint(c)
99 | }
100 |
101 |
102 | applyWind(wind) {
103 | if (!wind.isBlowing) return
104 |
105 | const { position } = this.activeTile.geo.attributes
106 | const tempVec = new C.Vec3()
107 |
108 | for (let i = 0; i < position.count; i++) {
109 | const stitch = this.stitches[i]
110 |
111 | const windNoise = wind.flowfield[i]
112 | const tempPosPhysic = tempVec.set(windNoise.x, windNoise.y, windNoise.z)
113 |
114 | stitch.applyLocalForce(tempPosPhysic, C.Vec3.ZERO)
115 | }
116 | }
117 |
118 |
119 | /* Values
120 | --------------------------------------------------------- */
121 |
122 | setStitches() {
123 | const { position } = this.activeTile.geo.attributes
124 | const { width, height } = this.activeTile.rect
125 |
126 | this.stitches = []
127 |
128 | for (let i = 0; i < position.count; i++) {
129 | const pos = new C.Vec3(
130 | position.getX(i) * width,
131 | position.getY(i) * height,
132 | position.getZ(i),
133 | )
134 |
135 | const stitch = new C.Body({
136 | mass: this.mass,
137 | linearDamping: 0.8,
138 | position: pos,
139 | shape: this.stitchesShape,
140 | })
141 |
142 | this.stitches.push(stitch)
143 |
144 | this.world.addBody(stitch)
145 | }
146 |
147 | this.initPositions = this.stitches.map((s) => s.initPosition)
148 |
149 | this.sewEverything()
150 | }
151 |
152 |
153 | sewEverything() {
154 | const { position } = this.activeTile.geo.attributes
155 | const { heightSegments: cols, widthSegments: rows } = this.activeTile.geo.parameters
156 |
157 | for (let i = 0; i < position.count; i++) {
158 | const col = (i % (cols + 1))
159 | const row = Math.floor(i / (rows + 1))
160 |
161 | if (col < cols) this.connect(i, i + 1)
162 | if (row < rows) this.connect(i, i + rows + 1)
163 |
164 | if (row === 0) {
165 | const pos = this.stitches[i].position.clone()
166 |
167 | pos.y += 100
168 |
169 | const b = new C.Body({
170 | mass: 0,
171 | position: pos,
172 | shape: new C.Particle(),
173 | })
174 |
175 | const cons = new C.DistanceConstraint(
176 | this.stitches[i],
177 | b,
178 | )
179 |
180 | this.world.addConstraint(cons)
181 | }
182 | }
183 | }
184 |
185 | }
186 |
187 |
188 |
189 |
190 | /* CONSTANTS & HELPERS
191 | ---------------------------------------------------------------------------------------------------- */
192 |
--------------------------------------------------------------------------------
/src/js/Layout.js:
--------------------------------------------------------------------------------
1 |
2 | export default class Layout {
3 |
4 | constructor() {
5 | this.onResize()
6 |
7 | this.bindEvents()
8 | }
9 |
10 | bindEvents() {
11 | window.addEventListener('resize', () => { this.onResize() })
12 | }
13 |
14 | onResize() {
15 | this.isMobile = window.matchMedia('(max-width: 767px)').matches
16 | this.isTablet = window.matchMedia('(max-width: 1111px)').matches
17 | this.isDesktop = !this.isMobile && !this.isTablet
18 |
19 | this.W = window.innerWidth
20 | this.H = window.innerHeight
21 |
22 | this.D = Math.ceil(Math.tan(APP.Store.ANGLE) * (Math.max(this.W, this.H) / 2))
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/js/O.js:
--------------------------------------------------------------------------------
1 | import { Object3D } from 'three'
2 |
3 | export default class O extends Object3D {
4 |
5 | init(el) {
6 | this.el = el
7 |
8 | this.resize()
9 | }
10 |
11 | resize() {
12 | const { W, H } = APP.Layout
13 |
14 | this.rect = this.el.getBoundingClientRect()
15 | const { left, top, width, height } = this.rect
16 |
17 | this.pos = {
18 | x: (left + (width / 2)) - (W / 2),
19 | y: (top + (height / 2)) - (H / 2),
20 | }
21 |
22 | this.position.x = this.pos.x
23 | this.position.y = this.pos.y
24 |
25 | this.update()
26 | }
27 |
28 | update(current) {
29 | current && (this.position.y = current + this.pos.y)
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/js/Scene.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import C from 'cannon'
3 | import OrbitControls from 'three-orbitcontrols'
4 | import gsap from 'gsap'
5 | import { ev } from './utils'
6 | import Slideshow from './Slideshow'
7 |
8 | const perspective = 800
9 |
10 | export default class Scene {
11 |
12 | constructor() {
13 | this.$stage = document.getElementById('stage')
14 |
15 | window.scrollTo(0, 0)
16 |
17 | this.setup()
18 | this.bindEvents()
19 | }
20 |
21 | bindEvents() {
22 | window.addEventListener('resize', () => { this.onResize() })
23 | window.addEventListener('wheel', (e) => { this.onScroll(e) }, { passive: false })
24 |
25 | document.addEventListener('rotateCam', this.onDelta.bind(this))
26 | document.addEventListener('toggleGravity', this.toggleGravity.bind(this))
27 | }
28 |
29 |
30 | setup() {
31 | // Init Physics world
32 | this.world = new C.World()
33 | setTimeout(() => {
34 | ev('windBlowing', { windBlowing: true })
35 | ev('toggleGravity', { shouldEverythingFalls: true })
36 | }, 1000)
37 |
38 | // Init Three components
39 | this.scene = new THREE.Scene()
40 |
41 | this.setCamera()
42 | this.setLights()
43 |
44 | this.addObjects()
45 |
46 | this.setRender()
47 | }
48 |
49 |
50 | /* Handlers
51 | --------------------------------------------------------- */
52 |
53 | onScroll(e) {
54 | e.preventDefault()
55 | }
56 |
57 | onDelta({ detail }) {
58 | const { delta } = detail
59 |
60 | gsap.to(this.camera.rotation, {
61 | duration: 1,
62 | z: this.camera.baseRotation - delta * 0.2,
63 | ease: 'strong.out',
64 | })
65 |
66 | this.camera.updateProjectionMatrix()
67 | }
68 |
69 | onResize() {
70 | const { W, H } = APP.Layout
71 |
72 | this.camera.aspect = W / H
73 |
74 | this.camera.updateProjectionMatrix()
75 | this.renderer.setSize(W, H)
76 | }
77 |
78 |
79 | /* Actions
80 | --------------------------------------------------------- */
81 |
82 | toggleGravity({ detail: { shouldEverythingFalls } }) {
83 | gsap.to(this.world.gravity, {
84 | duration: shouldEverythingFalls ? 2 : 0,
85 | y: shouldEverythingFalls ? -800 : 0,
86 | })
87 | }
88 |
89 | setCamera() {
90 | const { W, H } = APP.Layout
91 |
92 | const fov = getFov(perspective)
93 |
94 | const y = APP.Layout.isMobile ? 0 : 0
95 |
96 | this.camera = new THREE.PerspectiveCamera(fov, W / H, 1, 2000)
97 | this.camera.position.set(0, y, perspective)
98 |
99 | this.camera.baseRotation = -APP.Store.ANGLE
100 |
101 | this.camera.rotateZ(this.camera.baseRotation)
102 | }
103 |
104 | setLights() {
105 | const ambient = new THREE.AmbientLight(0xcccccc)
106 | this.scene.add(ambient)
107 | }
108 |
109 | setRender() {
110 | const { W, H } = APP.Layout
111 | this.renderer = new THREE.WebGLRenderer({
112 | antialias: true,
113 | alpha: true,
114 | canvas: this.$stage,
115 | })
116 |
117 | this.renderer.setSize(W, H)
118 | this.renderer.setPixelRatio(window.devicePixelRatio)
119 |
120 | this.renderer.setAnimationLoop(() => { this.render() })
121 | }
122 |
123 | addObjects() {
124 | this.slideshow = new Slideshow(this.scene, this.world)
125 | }
126 |
127 |
128 | setupDebug() {
129 | this.controls = new OrbitControls(this.camera, this.renderer.domElement)
130 | this.controls.enableKeys = false
131 | this.controls.update()
132 | }
133 |
134 |
135 | /* Values
136 | --------------------------------------------------------- */
137 |
138 | render() {
139 | this.updatePhysics()
140 | this.slideshow.render()
141 | this.renderer.render(this.scene, this.camera)
142 | }
143 |
144 | updatePhysics() {
145 | this.world.step(1 / 50)
146 | }
147 |
148 | }
149 |
150 |
151 |
152 |
153 | /* CONSTANTS & HELPERS
154 | ---------------------------------------------------------------------------------------------------- */
155 |
156 | const getFov = (p) => (180 * (2 * Math.atan(APP.Layout.H / 2 / p))) / Math.PI
157 |
--------------------------------------------------------------------------------
/src/js/Slideshow.js:
--------------------------------------------------------------------------------
1 | import gsap from 'gsap'
2 | import { Vector3 } from 'three'
3 |
4 | import { getStyle, ev } from '@js/utils'
5 |
6 | import Tile from './Tile'
7 | import Cloth from './Cloth'
8 | import Wind from './Wind'
9 |
10 |
11 | export default class Slideshow {
12 |
13 | constructor(scene, world) {
14 | this.scene = scene
15 | this.world = world
16 |
17 | this.$els = {
18 | el : document.querySelector('.js-slideshow'),
19 | slides : document.querySelectorAll('.js-slide'),
20 | split : document.querySelectorAll('.js-splited-text'),
21 | }
22 |
23 | this.opts = {
24 | speed: 1.5,
25 | ease: 0.065,
26 | threshold: 50,
27 | }
28 |
29 | this.states = {
30 | target : 0,
31 | current : 0,
32 | currentRounded : 0,
33 | activeSlide : 0,
34 |
35 | diff : 0,
36 |
37 | min : 0,
38 | max : 0,
39 |
40 | off : 0,
41 | flags : {
42 | hovering : false,
43 | dragging : false,
44 | // scrolling : false,
45 | // autoscroll : false,
46 | textTransition: false,
47 | },
48 | }
49 |
50 | this.hideTextTween = null
51 |
52 | this.setup()
53 |
54 | this.bindEvents()
55 | }
56 |
57 |
58 | bindEvents() {
59 | document.addEventListener('wheel', this.onScroll.bind(this))
60 | document.addEventListener('mouseleave', this.onUp.bind(this, true))
61 |
62 | document.addEventListener('mousedown', this.onDown.bind(this))
63 | document.addEventListener('mousemove', this.onMove.bind(this))
64 | document.addEventListener('mouseup', this.onUp.bind(this, false))
65 |
66 | document.addEventListener('touchstart', this.onDown.bind(this))
67 | document.addEventListener('touchmove', this.onMove.bind(this))
68 | document.addEventListener('touchend', this.onUp.bind(this, false))
69 | }
70 |
71 |
72 | setup() {
73 | const state = this.states
74 |
75 | this.getSizes()
76 |
77 | this.slides = this.getSlides()
78 |
79 | // eslint-disable-next-line prefer-destructuring
80 | state.activeSlide = this.slides[0]
81 | this.cloth = new Cloth(state.activeSlide.tile, this.world)
82 | this.wind = new Wind(state.activeSlide.tile)
83 |
84 | this.draw()
85 | }
86 |
87 | /* Handlers
88 | --------------------------------------------------------- */
89 |
90 | onScroll({ deltaY }) {
91 | const { diff } = this.states
92 |
93 | const delta = gsap.utils.clamp(-100, 100, deltaY)
94 | this.states.off += delta * this.opts.speed
95 |
96 | this.states.target = this.states.off
97 |
98 | this.hideTexts()
99 |
100 | ev('stormIsCalmingDown')
101 |
102 | if (Math.abs(diff) >= 0.01) { this.restartTimer() }
103 | if (Math.abs(diff) < 0.1) { this.restartTimerAutoScroll() }
104 |
105 | if (this.timerWind) clearTimeout(this.timerWind)
106 | }
107 |
108 |
109 | onDown(e) {
110 | this.states.flags.dragging = true
111 | this.startY = getPos(e).y
112 |
113 | ev('stormIsCalmingDown')
114 | }
115 |
116 | onMove(e) {
117 | if (!this.states.flags.dragging) return
118 |
119 | ev('stormIsCalmingDown')
120 | this.hideTexts()
121 |
122 | const { y } = getPos(e)
123 | this.dragY = (y - this.startY) * -3
124 |
125 | this.states.target = this.states.off + this.dragY * this.opts.speed
126 |
127 | if (this.timerWind) clearTimeout(this.timerWind)
128 | }
129 |
130 | onUp(isLeavingWindow = false) {
131 | const instant = !APP.Layout.isDesktop ? false : isLeavingWindow
132 |
133 | this.states.off = this.states.target
134 | this.states.flags.dragging = false
135 |
136 | if (!isLeavingWindow) ev('stormIsCalmingDown')
137 |
138 | this.restartTimerAutoScroll(instant)
139 | }
140 |
141 |
142 |
143 | /* Actions
144 | --------------------------------------------------------- */
145 |
146 | render() {
147 | this.wind.update()
148 | this.cloth.update()
149 | this.cloth.applyWind(this.wind)
150 |
151 | this.draw()
152 | }
153 |
154 |
155 | draw() {
156 | this.calc()
157 | this.transformSlides()
158 | }
159 |
160 |
161 | hideTexts() {
162 | const duration = !APP.Layout.isDesktop ? 0.2 : 0.4
163 |
164 | if (this.hideTextTween && this.hideTextTween.isActive()) return
165 |
166 | gsap.killTweensOf(this.$els.split)
167 |
168 | this.hideTextTween = gsap.to(this.$els.split, {
169 | duration,
170 | y: '100%',
171 | ease: 'power3.in',
172 | stagger: {
173 | grid: [2, 2],
174 | axis: 'x',
175 | amount: 0.16,
176 | },
177 | onStart: () => { this.states.flags.textTransition = true },
178 | onComplete: () => { this.states.flags.textTransition = false },
179 | })
180 | }
181 |
182 | revealTexts() {
183 | const title = this.states.activeSlide.title.toUpperCase()
184 |
185 | const newTitle = title.split(' ')
186 | /*.flatMap((word) => [
187 | word.substring(0, Math.ceil(word.length / 2)),
188 | word.substring(Math.ceil(word.length / 2), word.length),
189 | ])*/
190 |
191 | gsap.to(this.$els.split, {
192 | duration : 0.8,
193 | y : 0,
194 | ease : 'power3.out',
195 | stagger: {
196 | grid : [2, 2],
197 | axis : 'x',
198 | amount : 0.16,
199 | },
200 | onStart: () => {
201 | this.$els.split.forEach((s, i) => { s.innerText = newTitle[i] })
202 | },
203 | })
204 | }
205 |
206 | /* Values
207 | --------------------------------------------------------- */
208 |
209 | calc() {
210 | const state = this.states
211 | state.current += (state.target - state.current) * this.opts.ease
212 | state.currentRounded = Math.round(state.current * 100) / 100
213 | state.diff = (state.target - state.current) * 0.0005
214 |
215 | ev('rotateCam', { delta: state.diff })
216 | }
217 |
218 |
219 | transformSlides() {
220 | this.handleAlpha()
221 |
222 | this.slides.forEach((slide) => {
223 | const { translate, isVisible } = this.getTranslate(slide)
224 |
225 | slide.tile.update(translate)
226 | slide.tile.mat.uniforms.uVelo.value = this.states.diff
227 |
228 | if (isVisible) {
229 | slide.out = false
230 | } else if (!slide.out) {
231 | slide.out = true
232 | const { position, initPos } = slide.tile.geo.attributes
233 |
234 | position.copy(initPos)
235 | position.needsUpdate = true
236 | }
237 | })
238 | }
239 |
240 | getTranslate({ top, bottom, min, max }) {
241 | const { H, D } = APP.Layout
242 | const { currentRounded } = this.states
243 | const translate = gsap.utils.wrap(min, max, currentRounded)
244 |
245 | const { threshold } = this.opts
246 | const start = top + translate
247 | const end = bottom + translate
248 | const isVisible = start < (threshold + H + D) && end > -threshold
249 |
250 | return {
251 | translate,
252 | isVisible,
253 | }
254 | }
255 |
256 |
257 | handleAlpha() {
258 | const center = new Vector3()
259 | const diag = Math.hypot(APP.Layout.W, APP.Layout.H) / 2
260 | const off = !APP.Layout.isDesktop ? 50 : 200
261 |
262 |
263 | this.slides.forEach((slide) => {
264 | const dist = slide.tile.position.distanceTo(center)
265 | const value = 1 - gsap.utils.clamp(0, diag - off, dist) / diag
266 |
267 | slide.tile.mat.uniforms.uAlpha.value = value
268 | })
269 | }
270 |
271 |
272 | getSizes() {
273 | const state = this.states
274 | const { slides, el } = this.$els
275 | const {
276 | height: wrapHeight,
277 | top: wrapDiff,
278 | } = el.getBoundingClientRect()
279 |
280 | state.mb = getStyle(slides[slides.length - 1], 'margin-bottom')
281 |
282 | // Set bounds
283 | state.max = -((slides[slides.length - 1].getBoundingClientRect().bottom) - wrapHeight - wrapDiff)
284 | state.min = 0
285 | }
286 |
287 |
288 |
289 | /* Slider stuff
290 | --------------------------------------------------------- */
291 |
292 | slideTo(slide) {
293 | const state = this.states
294 | const target = state.currentRounded - slide.tile.position.y
295 |
296 | gsap.killTweensOf(state)
297 |
298 | gsap.to(state, {
299 | target,
300 | duration : !APP.Layout.isDesktop ? 0.2 : 0.4,
301 | ease : 'strong.inout',
302 |
303 | onComplete: () => {
304 | state.off = state.target
305 |
306 | if (slide !== state.activeSlide) {
307 | state.activeSlide = slide
308 | this.cloth.changeActiveTile(state.activeSlide.tile)
309 | }
310 |
311 | this.revealTexts()
312 |
313 | this.timerWind = setTimeout(() => {
314 | ev('windBlowing', { windBlowing: true })
315 | ev('toggleGravity', { shouldEverythingFalls: true })
316 | }, 400)
317 | },
318 | })
319 | }
320 |
321 |
322 | getSlides() {
323 | const { H, D } = APP.Layout
324 | const state = this.states
325 |
326 | const sizes = {
327 | max: APP.Layout.isMobile ? 0.8 : 0.83,
328 | min: APP.Layout.isMobile ? 0.2 : 0.17,
329 | }
330 |
331 | const offsets = {
332 | max: APP.Layout.isMobile ? D : D / 2,
333 | min: APP.Layout.isMobile ? state.mb * 2 : -state.mb + D / 2,
334 | }
335 |
336 |
337 | return Array.from(this.$els.slides).map((s, i) => {
338 | const { top, bottom, height } = s.getBoundingClientRect()
339 |
340 | const tile = new Tile()
341 | tile.init(s, { scene: this.scene })
342 |
343 | const maxValue = (H) * sizes.max + offsets.max
344 | const minValue = (H) * sizes.min + offsets.min
345 |
346 |
347 | return {
348 | $el: s,
349 | index: i,
350 | tile,
351 | top,
352 | bottom,
353 | height,
354 | title: s.querySelector('h3').innerText,
355 | min: top < (H) ? state.min + (maxValue) : state.min - (minValue),
356 | max: top > (H) ? state.max - (maxValue) : state.max + (minValue),
357 | out: false,
358 | }
359 | })
360 | }
361 |
362 | getClosest() {
363 | let closest = this.slides[0]
364 | const center = new Vector3()
365 |
366 | this.slides.map((s) => s.tile).forEach((tile, i) => {
367 | const closestDist = closest.tile.position.distanceTo(center)
368 | const newDist = tile.position.distanceTo(center)
369 |
370 | if (newDist < closestDist) {
371 | closest = this.slides[i]
372 | }
373 | })
374 |
375 | return closest
376 | }
377 |
378 |
379 | /* Timer
380 | --------------------------------------------------------- */
381 |
382 | restartTimer() {
383 | clearTimeout(this.timer)
384 | clearTimeout(this.timerAutoScroll)
385 |
386 | this.timer = setTimeout(() => {
387 | this.slideTo(this.getClosest())
388 | }, 1000)
389 | }
390 |
391 | restartTimerAutoScroll(instant) {
392 | // eslint-disable-next-line no-nested-ternary
393 | const delay = instant ? 0 : !APP.Layout.isDesktop ? 1000 : 100
394 | clearTimeout(this.timer)
395 | clearTimeout(this.timerAutoScroll)
396 |
397 | this.timerAutoScroll = setTimeout(() => {
398 | this.slideTo(this.getClosest())
399 | }, delay)
400 | }
401 |
402 | }
403 |
404 |
405 |
406 |
407 | /* CONSTANTS & HELPERS
408 | ---------------------------------------------------------------------------------------------------- */
409 |
410 |
411 | const getPos = ({ changedTouches, clientX, clientY, target }) => {
412 | const x = changedTouches ? changedTouches[0].clientX : clientX
413 | const y = changedTouches ? changedTouches[0].clientY : clientY
414 |
415 | return {
416 | x, y, target,
417 | }
418 | }
419 |
--------------------------------------------------------------------------------
/src/js/Tile.js:
--------------------------------------------------------------------------------
1 | import {
2 | PlaneBufferGeometry,
3 | Mesh,
4 | TextureLoader,
5 | } from 'three'
6 |
7 | import gsap from 'gsap'
8 | import O from './O'
9 | import CoverMaterial from '../glsl/cover'
10 | import { clamp } from './utils'
11 |
12 |
13 | const cols = 8
14 | const rows = 8
15 |
16 |
17 | export default class Tile extends O {
18 |
19 | init(el, { scene }) {
20 | super.init(el)
21 |
22 | const { width, height } = this.rect
23 |
24 | this.scene = scene
25 | this.geo = new PlaneBufferGeometry(1, 1, cols, rows)
26 | this.mat = new CoverMaterial({ meshSize: this.rect })
27 | this.img = this.el.querySelector('img')
28 |
29 | this.texture = loader.load(this.img.src, (texture) => {
30 | this.mat.uniforms.uTexture.value = texture
31 | this.mat.uniforms.uImageSize.value = [this.img.naturalWidth, this.img.naturalHeight]
32 | })
33 |
34 | this.mesh = new Mesh(this.geo, this.mat)
35 | this.mesh.scale.set(width, height, 1)
36 |
37 | this.mat.uniforms.uScale.value = Math.max(width, height) / Math.hypot(width, height)
38 | this.geo.attributes.initPos = this.geo.attributes.position.clone()
39 |
40 | this.add(this.mesh)
41 | this.scene.add(this)
42 |
43 | this.bindEvents()
44 | }
45 |
46 | bindEvents() {
47 | document.addEventListener('rotateCam', this.onDistort.bind(this))
48 | }
49 |
50 | /* Handlers
51 | --------------------------------------------------------- */
52 |
53 | onDistort({ detail: { delta } }) {
54 | const { width, height } = this.rect
55 | const sclX = clamp(1 - Math.abs(delta), 0.8, 1)
56 |
57 | gsap.to(this.mesh.scale, {
58 | duration: 0.5,
59 | x: sclX * width,
60 | y: height,
61 | z: 1,
62 | })
63 | }
64 |
65 | }
66 |
67 |
68 | /* CONST & HELPERS
69 | ---------------------------------------------------------------------------------------------------- */
70 |
71 | const loader = new TextureLoader()
72 |
73 |
--------------------------------------------------------------------------------
/src/js/Wind.js:
--------------------------------------------------------------------------------
1 | import SimplexNoise from 'simplex-noise'
2 | import { Clock, Vector3 } from 'three'
3 | import gsap from 'gsap'
4 |
5 | import { map } from './utils'
6 |
7 | const noise = new SimplexNoise()
8 | const off = 0.1
9 | const baseForce = 40
10 |
11 | export default class Wind {
12 |
13 | constructor(tile) {
14 | this.activeTile = tile
15 | this.clock = new Clock()
16 |
17 | this.force = APP.Layout.isMobile ? baseForce / 1000 : baseForce
18 | this.direction = new Vector3(0.5, 0, -1)
19 |
20 | this.sizes = {
21 | cols: this.activeTile.geo.parameters.widthSegments,
22 | rows: this.activeTile.geo.parameters.heightSegments,
23 | }
24 |
25 | this.flowfield = new Array(this.sizes.cols * this.sizes.rows)
26 | this.isBlowing = false
27 |
28 | this.update()
29 |
30 | this.bindEvents()
31 | }
32 |
33 |
34 | bindEvents() {
35 | window.addEventListener('mousemove', this.onMouseMove.bind(this))
36 | document.addEventListener('windBlowing', this.onWindChange.bind(this))
37 | document.addEventListener('stormIsCalmingDown', this.onWindChange.bind(this, false))
38 | }
39 |
40 |
41 |
42 | /* Handlers
43 | --------------------------------------------------------- */
44 |
45 | onMouseMove({ clientX: x, clientY: y }) {
46 | const { W, H, isMobile } = APP.Layout
47 |
48 | if (isMobile) return
49 |
50 | gsap.to(this.direction, {
51 | duration: 0.1,
52 | x: (x / W) - 0.5,
53 | y: -(y / H) + 0.5,
54 | })
55 | }
56 |
57 | onWindChange(ev) {
58 | this.isBlowing = !ev || ev.detail.windBlowing
59 |
60 | gsap.to(this, {
61 | duration: this.isBlowing ? 2 : 0,
62 | force: this.isBlowing ? baseForce : 0,
63 | })
64 | }
65 |
66 |
67 | /* Actions
68 | --------------------------------------------------------- */
69 |
70 |
71 |
72 | /* Values
73 | --------------------------------------------------------- */
74 |
75 |
76 | update() {
77 | const time = this.clock.getElapsedTime() * 2
78 |
79 | const { position } = this.activeTile.geo.attributes
80 | const { rows, cols } = this.sizes
81 |
82 |
83 | for (let i = 0; i < position.count; i++) {
84 | const col = (i % (cols))
85 | const row = Math.floor(i / (rows))
86 |
87 | const force = map(noise.noise3D(row * off, col * off, time), -1, 1, -this.force * 0.1, this.force)
88 |
89 | this.flowfield[i] = this.direction.clone().multiplyScalar(force)
90 | }
91 | }
92 |
93 |
94 | }
95 |
96 |
97 |
98 |
99 | /* CONSTANTS & HELPERS
100 | ---------------------------------------------------------------------------------------------------- */
101 |
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | import '../sass/styles.scss'
2 | import Scene from './Scene'
3 | import Layout from './Layout'
4 |
5 | const APP = window.APP || {}
6 |
7 | /*-----------------------------------------------------------------------------------*/
8 | /* 01. INIT
9 | /*-----------------------------------------------------------------------------------*/
10 |
11 | const initApp = () => {
12 | window.APP = APP
13 |
14 | APP.Store = {
15 | ANGLE : Math.PI / 6,
16 | isTransitioning : false,
17 | }
18 |
19 | APP.Layout = new Layout()
20 | APP.Scene = new Scene()
21 | }
22 |
23 | if (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) {
24 | initApp()
25 | } else {
26 | document.addEventListener('DOMContentLoaded', initApp)
27 | }
28 |
--------------------------------------------------------------------------------
/src/js/utils/CannonDebugRenderer.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-underscore-dangle */
2 | import * as THREE from 'three'
3 | import CANNON from 'cannon'
4 |
5 | /**
6 | * Adds Three.js primitives into the scene where all the Cannon bodies and shapes are.
7 | * @class CannonDebugRenderer
8 | * @param {THREE.Scene} scene
9 | * @param {CANNON.World} world
10 | * @param {object} [options]
11 | */
12 |
13 | export default class CannonDebugRenderer {
14 |
15 | constructor(scene, world, options) {
16 | this.options = options || {}
17 |
18 | this.scene = scene
19 | this.world = world
20 |
21 | this._meshes = []
22 |
23 | this._material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true })
24 | this._sphereGeometry = new THREE.SphereGeometry(1)
25 | this._boxGeometry = new THREE.BoxGeometry(1, 1, 1)
26 | this._planeGeometry = new THREE.PlaneGeometry(10, 10, 10, 10)
27 | this._cylinderGeometry = new THREE.CylinderGeometry(1, 1, 10, 10)
28 |
29 | this.tmpVec0 = new CANNON.Vec3()
30 | this.tmpVec1 = new CANNON.Vec3()
31 | this.tmpVec2 = new CANNON.Vec3()
32 | this.tmpQuat0 = new CANNON.Vec3()
33 | }
34 |
35 |
36 | update() {
37 | const { bodies } = this.world
38 | const meshes = this._meshes
39 | const shapeWorldPosition = this.tmpVec0
40 | const shapeWorldQuaternion = this.tmpQuat0
41 |
42 | let meshIndex = 0
43 |
44 | for (let i = 0; i !== bodies.length; i++) {
45 | const body = bodies[i]
46 |
47 | for (let j = 0; j !== body.shapes.length; j++) {
48 | const shape = body.shapes[j]
49 |
50 | this._updateMesh(meshIndex, body, shape)
51 |
52 | const mesh = meshes[meshIndex]
53 |
54 | if (mesh) {
55 | // Get world position
56 | body.quaternion.vmult(body.shapeOffsets[j], shapeWorldPosition)
57 | body.position.vadd(shapeWorldPosition, shapeWorldPosition)
58 |
59 | // Get world quaternion
60 | body.quaternion.mult(body.shapeOrientations[j], shapeWorldQuaternion)
61 |
62 | // Copy to meshes
63 | mesh.position.copy(shapeWorldPosition)
64 | mesh.quaternion.copy(shapeWorldQuaternion)
65 | }
66 |
67 | meshIndex += 1
68 | }
69 | }
70 |
71 | for (let i = meshIndex; i < meshes.length; i++) {
72 | const mesh = meshes[i]
73 | if (mesh) {
74 | this.scene.remove(mesh)
75 | }
76 | }
77 |
78 | meshes.length = meshIndex
79 | }
80 |
81 |
82 | _updateMesh(index, body, shape) {
83 | let mesh = this._meshes[index]
84 | if (!_typeMatch(mesh, shape)) {
85 | if (mesh) {
86 | this.scene.remove(mesh)
87 | }
88 | mesh = this._meshes[index] = this._createMesh(shape)
89 | }
90 | _scaleMesh(mesh, shape)
91 | }
92 |
93 |
94 | _createMesh(shape) {
95 | let mesh
96 | const material = this._material
97 |
98 | const geo = new THREE.Geometry()
99 |
100 | switch (shape.type) {
101 |
102 | case CANNON.Shape.types.SPHERE:
103 | mesh = new THREE.Mesh(this._sphereGeometry, material)
104 | break
105 |
106 | case CANNON.Shape.types.BOX:
107 | mesh = new THREE.Mesh(this._boxGeometry, material)
108 | break
109 |
110 | case CANNON.Shape.types.PLANE:
111 | mesh = new THREE.Mesh(this._planeGeometry, material)
112 | break
113 |
114 | case CANNON.Shape.types.CONVEXPOLYHEDRON:
115 | // Create mesh
116 |
117 | // Add vertices
118 | for (let i = 0; i < shape.vertices.length; i++) {
119 | const v = shape.vertices[i]
120 | geo.vertices.push(new THREE.Vector3(v.x, v.y, v.z))
121 | }
122 |
123 | for (let i = 0; i < shape.faces.length; i++) {
124 | const face = shape.faces[i]
125 |
126 | // add triangles
127 | const a = face[0]
128 | for (let j = 1; j < face.length - 1; j++) {
129 | const b = face[j]
130 | const c = face[j + 1]
131 | geo.faces.push(new THREE.Face3(a, b, c))
132 | }
133 | }
134 | geo.computeBoundingSphere()
135 | geo.computeFaceNormals()
136 |
137 | mesh = new THREE.Mesh(geo, material)
138 | shape.geometryId = geo.id
139 | break
140 |
141 | case CANNON.Shape.types.TRIMESH:
142 | for (let i = 0; i < shape.indices.length / 3; i++) {
143 | shape.getTriangleVertices(i, this.tmpVec0, this.tmpVec1, this.tmpVec2)
144 | geo.vertices.push(
145 | new THREE.Vector3(this.tmpVec0.x, this.tmpVec0.y, this.tmpVec0.z),
146 | new THREE.Vector3(this.tmpVec1.x, this.tmpVec1.y, this.tmpVec1.z),
147 | new THREE.Vector3(this.tmpVec2.x, this.tmpVec2.y, this.tmpVec2.z),
148 | )
149 | const j = geo.vertices.length - 3
150 | geo.faces.push(new THREE.Face3(j, j + 1, j + 2))
151 | }
152 | geo.computeBoundingSphere()
153 | geo.computeFaceNormals()
154 | mesh = new THREE.Mesh(geo, material)
155 | shape.geometryId = geo.id
156 | break
157 |
158 | case CANNON.Shape.types.HEIGHTFIELD:
159 | for (let xi = 0; xi < shape.data.length - 1; xi++) {
160 | for (let yi = 0; yi < shape.data[xi].length - 1; yi++) {
161 | for (let k = 0; k < 2; k++) {
162 | shape.getConvexTrianglePillar(xi, yi, k === 0)
163 | this.tmpVec0.copy(shape.pillarConvex.vertices[0])
164 | this.tmpVec1.copy(shape.pillarConvex.vertices[1])
165 | this.tmpVec2.copy(shape.pillarConvex.vertices[2])
166 | this.tmpVec0.vadd(shape.pillarOffset, this.tmpVec0)
167 | this.tmpVec1.vadd(shape.pillarOffset, this.tmpVec1)
168 | this.tmpVec2.vadd(shape.pillarOffset, this.tmpVec2)
169 | geo.vertices.push(
170 | new THREE.Vector3(this.tmpVec0.x, this.tmpVec0.y, this.tmpVec0.z),
171 | new THREE.Vector3(this.tmpVec1.x, this.tmpVec1.y, this.tmpVec1.z),
172 | new THREE.Vector3(this.tmpVec2.x, this.tmpVec2.y, this.tmpVec2.z),
173 | )
174 | const ii = geo.vertices.length - 3
175 | geo.faces.push(new THREE.Face3(ii, ii + 1, ii + 2))
176 | }
177 | }
178 | }
179 | geo.computeBoundingSphere()
180 | geo.computeFaceNormals()
181 | mesh = new THREE.Mesh(geo, material)
182 | shape.geometryId = geo.id
183 | break
184 | default:
185 | break
186 | }
187 |
188 | if (mesh) {
189 | this.scene.add(mesh)
190 | }
191 |
192 | return mesh
193 | }
194 |
195 |
196 |
197 |
198 | }
199 |
200 | const _typeMatch = (mesh, shape) => {
201 | if (!mesh) {
202 | return false
203 | }
204 | const geo = mesh.geometry
205 | return (
206 | (geo instanceof THREE.SphereGeometry && shape instanceof CANNON.Sphere)
207 | || (geo instanceof THREE.BoxGeometry && shape instanceof CANNON.Box)
208 | || (geo instanceof THREE.PlaneGeometry && shape instanceof CANNON.Plane)
209 | || (geo.id === shape.geometryId && shape instanceof CANNON.ConvexPolyhedron)
210 | || (geo.id === shape.geometryId && shape instanceof CANNON.Trimesh)
211 | || (geo.id === shape.geometryId && shape instanceof CANNON.Heightfield)
212 | )
213 | }
214 |
215 | const _scaleMesh = (mesh, shape) => {
216 | const { radius } = shape
217 |
218 | switch (shape.type) {
219 |
220 | case CANNON.Shape.types.SPHERE:
221 | mesh.scale.set(radius, radius, radius)
222 | break
223 |
224 | case CANNON.Shape.types.BOX:
225 | mesh.scale.copy(shape.halfExtents)
226 | mesh.scale.multiplyScalar(2)
227 | break
228 |
229 | case CANNON.Shape.types.CONVEXPOLYHEDRON:
230 | mesh.scale.set(1, 1, 1)
231 | break
232 |
233 | case CANNON.Shape.types.TRIMESH:
234 | mesh.scale.copy(shape.scale)
235 | break
236 |
237 | case CANNON.Shape.types.HEIGHTFIELD:
238 | mesh.scale.set(1, 1, 1)
239 | break
240 | default:
241 | break
242 |
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/src/js/utils/index.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 |
3 | // eslint-disable-next-line import/prefer-default-export
4 | export const clamp = (val, min = 0, max = 1) => Math.max(min, Math.min(max, val))
5 |
6 | export const pick = (arr) => arr[Math.floor(Math.random() * arr.length)]
7 |
8 | export const map = (value, min1, max1, min2, max2) => min2 + (max2 - min2) * (value - min1) / (max1 - min1)
9 |
10 | export const getRatio = ({ x: w, y: h }, { width, height }, r = 0) => {
11 | const m = multiplyMatrixAndPoint(rotateMatrix(THREE.Math.degToRad(r)), [w, h])
12 | const originalRatio = {
13 | w: m[0] / width,
14 | h: m[1] / height,
15 | }
16 |
17 | const coverRatio = 1 / Math.max(originalRatio.w, originalRatio.h)
18 |
19 | return new THREE.Vector2(
20 | originalRatio.w * coverRatio,
21 | originalRatio.h * coverRatio,
22 | )
23 | }
24 |
25 |
26 | const rotateMatrix = (a) => [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a)]
27 |
28 | const multiplyMatrixAndPoint = (matrix, point) => {
29 | const c0r0 = matrix[0]
30 | const c1r0 = matrix[1]
31 | const c0r1 = matrix[2]
32 | const c1r1 = matrix[3]
33 | const x = point[0]
34 | const y = point[1]
35 | return [Math.abs(x * c0r0 + y * c0r1), Math.abs(x * c1r0 + y * c1r1)]
36 | }
37 |
38 | export const wrap = (el, wrapper) => {
39 | el.parentNode.insertBefore(wrapper, el)
40 | wrapper.appendChild(el)
41 | }
42 |
43 |
44 | export const unwrap = (content) => {
45 | for (let i = 0; i < content.length; i++) {
46 | const el = content[i]
47 | const parent = el.parentNode
48 |
49 | if (parent.parentNode) parent.outerHTML = el.innerHTML
50 | }
51 | }
52 |
53 | export const ev = (eventName, data, once = false) => {
54 | const e = new CustomEvent(eventName, { detail: data }, { once })
55 | document.dispatchEvent(e)
56 | }
57 |
58 | export const getStyle = (el, property) => parseFloat(window.getComputedStyle(el).getPropertyValue(property))
59 |
--------------------------------------------------------------------------------
/src/sass/_base.scss:
--------------------------------------------------------------------------------
1 | *,
2 | *::after,
3 | *::before {
4 | box-sizing: border-box;
5 | }
6 |
7 | :root {
8 | font-size: 16px;
9 | }
10 |
11 | html, body {
12 | height: 100%;
13 | }
14 |
15 | body {
16 | margin: 0;
17 | --color-text: #38201e;
18 | --color-bg: #fff2e1;
19 | --color-deco: rgba(212, 169, 126, 0.12);
20 | --color-title: #292213;
21 | --color-link: #c37646;
22 | --color-link-hover: #000;
23 | color: var(--color-text);
24 | background-color: var(--color-bg);
25 | font-family: niveau-grotesk, sans-serif;
26 | -webkit-font-smoothing: antialiased;
27 | -moz-osx-font-smoothing: grayscale;
28 | }
29 |
30 | /* Page Loader */
31 | .js .loading::before,
32 | .js .loading::after {
33 | content: '';
34 | position: fixed;
35 | z-index: 1000;
36 | }
37 |
38 | .js .loading::before {
39 | top: 0;
40 | left: 0;
41 | width: 100%;
42 | height: 100%;
43 | background: var(--color-bg);
44 | }
45 |
46 | .js .loading::after {
47 | top: 50%;
48 | left: 50%;
49 | width: 60px;
50 | height: 60px;
51 | margin: -30px 0 0 -30px;
52 | border-radius: 50%;
53 | opacity: 0.4;
54 | background: var(--color-link);
55 | animation: loaderAnim 0.7s linear infinite alternate forwards;
56 |
57 | }
58 |
59 | @keyframes loaderAnim {
60 | to {
61 | opacity: 1;
62 | transform: scale3d(0.5,0.5,1);
63 | }
64 | }
65 |
66 | a {
67 | text-decoration: none;
68 | color: var(--color-link);
69 | outline: none;
70 |
71 | transition: color .2s;
72 | }
73 |
74 | a:hover,
75 | a:focus {
76 | color: var(--color-link-hover);
77 | outline: none;
78 | }
79 |
80 | .frame {
81 | padding: 2rem;
82 | text-align: center;
83 | position: relative;
84 | z-index: 1200;
85 | line-height: 1.5;
86 | }
87 |
88 | .frame__title {
89 | margin: 0;
90 | font-size: 1rem;
91 | font-weight: normal;
92 | }
93 |
94 | .frame__links a:not(:last-child) {
95 | margin-right: .8rem;
96 | }
97 |
98 | .content {
99 | display: flex;
100 | flex-direction: column;
101 | width: 100vw;
102 | height: calc(100vh - 18rem);
103 | position: relative;
104 | align-items: center;
105 | justify-content: center;
106 | }
107 |
108 | .frame__tagline {
109 | display: block;
110 | position: relative;
111 | margin: 0;
112 | }
113 |
114 | @media screen and (min-width: 53em) {
115 | .message {
116 | display: none;
117 | }
118 | .frame {
119 | position: fixed;
120 | text-align: left;
121 | z-index: 100;
122 | top: 0;
123 | left: 0;
124 | line-height: 1;
125 | display: grid;
126 | align-content: space-between;
127 | justify-content: space-between;
128 | width: 100%;
129 | max-width: none;
130 | padding: 2rem 3rem;
131 | pointer-events: none;
132 | grid-template-areas: 'title links';
133 | }
134 | .frame__title-wrap {
135 | grid-area: title;
136 | line-height: 1.5;
137 | }
138 | .frame__title {
139 | margin: 0;
140 | }
141 | .frame__links {
142 | grid-area: links;
143 | padding: 0;
144 | }
145 | .frame a {
146 | pointer-events: auto;
147 | }
148 | .content {
149 | height: 100vh;
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/sass/_deco.scss:
--------------------------------------------------------------------------------
1 |
2 | /* Deco elements
3 | ---------------------------------------------------------------------------------------------------- */
4 |
5 |
6 | .deco {
7 | position: fixed;
8 | z-index: -1;
9 | transform: rotate(-30deg);
10 |
11 | &:nth-child(1) {
12 | top: 15%;
13 | left: 53%;
14 | width: 30vmax;
15 | height: 30vmax;
16 |
17 | background-color: var(--color-deco);
18 | }
19 |
20 | &:nth-child(2) {
21 | top: 35%;
22 | left: 14%;
23 |
24 | width: 26vmax;
25 | height: 26vmax;
26 |
27 | border: 100px solid var(--color-deco);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/sass/_main.scss:
--------------------------------------------------------------------------------
1 | /* MAIN
2 | --------------------------------------------------------- */
3 | body {
4 | overflow: hidden;
5 | }
6 |
7 | #stage {
8 | position: fixed;
9 | top: 0;
10 | left: 0;
11 | z-index: 3;
12 | width: 100%;
13 | height: 100vh;
14 |
15 | pointer-events: none;
16 | }
17 |
18 | * {
19 | @include maxw($bp-mobile) {
20 | -moz-user-select: none; /* Firefox */
21 | -ms-user-select: none; /* Internet Explorer */
22 | -khtml-user-select: none; /* KHTML browsers (e.g. Konqueror) */
23 | -webkit-user-select: none; /* Chrome, Safari, and Opera */
24 | -webkit-touch-callout: none; /* Disable Android and iOS callouts*/
25 | }
26 | }
27 |
28 | .frame {
29 | user-select: none;
30 | }
31 |
--------------------------------------------------------------------------------
/src/sass/_reset.scss:
--------------------------------------------------------------------------------
1 |
2 | /* RESET
3 | ---------------------------------------------------------------------------------------------------- */
4 |
5 | html, body, div, span, object, iframe,
6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
7 | abbr, address, cite, code,
8 | del, dfn, em, img, ins, kbd, q, samp,
9 | small, strong, sub, sup, var,
10 | b, i,
11 | dl, dt, dd, ol, ul, li,
12 | fieldset, form, label, legend,
13 | table, caption, tbody, tfoot, thead, tr, th, td,
14 | article, aside, canvas, details, figcaption, figure,
15 | footer, header, hgroup, menu, nav, section, summary,
16 | time, mark, audio, video {
17 | vertical-align: baseline;
18 | margin: 0;
19 | padding: 0;
20 |
21 | font-size: 100%;
22 |
23 | background: transparent;
24 | border: 0;
25 | outline: 0;
26 | }
27 |
28 | html { font-size: 62.5%; } // = 10px
29 |
30 | body { line-height: 1; }
31 |
32 | article, aside, details, figcaption, figure,
33 | footer, header, hgroup, menu, nav, section, main { display: block; }
34 |
35 | ul,
36 | ol { list-style: none; }
37 |
38 | blockquote,
39 | q { quotes: none; }
40 |
41 | blockquote::before,
42 | blockquote::after,
43 | q::before,
44 | q::after {
45 | content: '';
46 | content: none;
47 | }
48 |
49 | a,
50 | input,
51 | button,
52 | select,
53 | textarea {
54 | appearance: none;
55 | text-decoration: none;
56 | outline: none;
57 | }
58 |
59 | a {
60 | vertical-align: baseline;
61 | margin: 0;
62 | padding: 0;
63 |
64 | font-size: 100%;
65 | color: inherit;
66 |
67 | background: transparent;
68 | outline: none;
69 | }
70 |
71 | button {
72 | padding: 0;
73 | cursor: pointer;
74 | }
75 |
76 | ins {
77 | text-decoration: none;
78 | color: #000;
79 |
80 | background-color: #ff9;
81 | }
82 |
83 | mark {
84 | font-style: italic;
85 | font-weight: 700;
86 | color: #000;
87 |
88 | background-color: #ff9;
89 | }
90 |
91 | del { text-decoration: line-through; }
92 |
93 | abbr[title],
94 | dfn[title] {
95 | border-bottom: 1px dotted;
96 | cursor: help;
97 | }
98 |
99 | table {
100 | border-collapse: collapse;
101 | border-spacing: 0;
102 | }
103 |
104 | hr {
105 | display: block;
106 | height: 1px;
107 | padding: 0;
108 |
109 | border: none;
110 | }
111 |
112 | input,
113 | select { vertical-align: middle; }
114 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/src/sass/_slider.scss:
--------------------------------------------------------------------------------
1 |
2 | /* Slider
3 | ---------------------------------------------------------------------------------------------------- */
4 |
5 | .slideshow {
6 | display: flex;
7 | justify-content: center;
8 | position: fixed;
9 | top: 0;
10 | left: 0;
11 | width: 100%;
12 | height: 100%;
13 | padding: 17vh 0;
14 |
15 | @include maxw($bp-mobile) {
16 | padding: 20vh 20vw;
17 | }
18 | }
19 |
20 | .slideshow__inner {
21 | width: 100%;
22 | max-width: 520px;
23 |
24 | @include minw($bp-tablet) {
25 | width: 40vw;
26 | max-width: 456px;
27 | }
28 | }
29 |
30 | .slide {
31 | @include ratio(144.7%, 'img');
32 | overflow: hidden;
33 | width: 100%;
34 | height: 100%;
35 | margin-bottom: 15%;
36 |
37 | h3 {
38 | opacity: 0;
39 | user-select: none;
40 |
41 | pointer-events: none;
42 | }
43 |
44 | img {
45 | display: block;
46 | width: 100%;
47 | height: 100%;
48 |
49 | object-fit: cover;
50 |
51 | opacity: 0;
52 | user-select: none;
53 |
54 | pointer-events: none;
55 | }
56 |
57 | @include maxw($bp-mobile) {
58 | margin-bottom: 75px;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/sass/_typography.scss:
--------------------------------------------------------------------------------
1 |
2 | /* TYPOGRAPHY
3 | ---------------------------------------------------------------------------------------------------- */
4 | /* Styles
5 | --------------------------------------------------------- */
6 | .title-wrap {
7 | position: relative;
8 | z-index: 100;
9 | //mix-blend-mode: difference;
10 | }
11 |
12 | .title {
13 | font-family: quiche-sans, sans-serif;
14 | font-weight: 900;
15 | text-transform: uppercase;
16 | font-size: 11vw;
17 | line-height: 0.9;
18 | color: var(--color-title);
19 | pointer-events: none;
20 | -webkit-touch-callout: none;
21 | -webkit-user-select: none;
22 | -ms-user-select: none;
23 | user-select: none;
24 | }
25 |
26 |
27 | /* Split
28 | --------------------------------------------------------- */
29 |
30 | .split {
31 | display: inline-block;
32 | overflow: hidden;
33 |
34 | &:nth-child(1) {
35 | transform: translate3d(1ch, -50%, 0);
36 | }
37 |
38 | &:nth-child(2) {
39 | transform: translate3d(-1ch, 50%, 0);
40 | }
41 | }
42 |
43 | .split__text {
44 | display: block;
45 | }
46 |
--------------------------------------------------------------------------------
/src/sass/_utils.scss:
--------------------------------------------------------------------------------
1 | /* Media Queries
2 | --------------------------------------------------------- */
3 |
4 | // Minimum WIDTH
5 | @mixin minw($val) {
6 | @media only screen and (min-width: $val) {
7 | @content;
8 | }
9 | }
10 |
11 | // Maximum WIDTH
12 | @mixin maxw($val) {
13 | @media only screen and (max-width: #{$val - 1}) {
14 | @content;
15 | }
16 | }
17 |
18 | // Minimum HEIGHT
19 | @mixin minh($val) {
20 | @media only screen and (min-height: $val) {
21 | @content;
22 | }
23 | }
24 |
25 | // Maximum HEIGHT
26 | @mixin maxh($val) {
27 | @media only screen and (max-height: #{$val - 1}) {
28 | @content;
29 | }
30 | }
31 |
32 | // Minimum WIDTH & Maximum WIDTH
33 | @mixin minw-maxw($minw, $maxw) {
34 | @media only screen and (min-width: $minw) and (max-width: #{$val - 1}) {
35 | @content;
36 | }
37 | }
38 |
39 | $bp-mobile: 768px;
40 | $bp-tablet: 1112px;
41 |
42 |
43 | /* HOVER
44 | ---------------------------------------------------------------------------------------------------- */
45 | /// Mixin for handling hover states
46 | /// 'mouse' : Handle mouse, touch pad
47 | /// 'touch' : Handle smartphones, touchscreens
48 | /// 'stylus' : Handle stylus-based screens
49 | /// 'controller' : Handle Nintendo Wii controller, Microsoft Kinect
50 |
51 | $mq-input: (
52 | 'mouse' : '(hover: hover) and (pointer: fine)',
53 | 'touch' : '(hover: none) and (pointer: coarse)',
54 | 'stylus' : '(hover: none) and (pointer: fine)',
55 | 'controller' : '(hover: hover) and (pointer: coarse)'
56 | );
57 |
58 | @mixin hover($preset: false, $device: 'mouse', $active: false, $handleIE: false) {
59 | $device: if($device == null, 'mouse', $device);
60 | $mediaquery : map-get($mq-input, $device);
61 |
62 | $activeState : if($active, '&:active, ', '');
63 | $ieState : if($handleIE, ', .ie &:hover, .ie &:focus', '');
64 | $selectors : $activeState + '&:hover, &:focus' + $ieState;
65 |
66 | @media #{$mediaquery} {
67 | @if $preset {
68 | #{$selectors} {
69 | @content;
70 | }
71 | }
72 |
73 | @else {
74 | @content;
75 | }
76 | }
77 | }
78 |
79 |
80 | @mixin ratio($val, $element: div, $offset: 0, $after: false) {
81 | position: relative;
82 |
83 | @if $element != none {
84 | > #{$element} {
85 | position: absolute;
86 | top: 0;
87 | right: 0;
88 | bottom: 0;
89 | left: 0;
90 | }
91 | }
92 |
93 | @if $after == false {
94 | &::before {
95 | content: '';
96 |
97 | display: block;
98 | padding-top: $val;
99 | }
100 | }
101 |
102 | @else {
103 | &::after {
104 | content: '';
105 |
106 | display: block;
107 | padding-top: $val;
108 | }
109 | }
110 | }
111 |
112 | /* MISC
113 | ---------------------------------------------------------------------------------------------------- */
114 |
115 | .visually-hidden {
116 | overflow: hidden !important;
117 | position: absolute !important;
118 | width: 1px !important;
119 | height: 1px !important;
120 | margin: -1px;
121 | padding: 0;
122 |
123 | border: 0;
124 | clip: rect(0 0 0 0);
125 | }
126 |
--------------------------------------------------------------------------------
/src/sass/styles.scss:
--------------------------------------------------------------------------------
1 | @import './utils';
2 |
3 | @import './reset';
4 | @import './base';
5 |
6 |
7 | @import './main';
8 | @import './typography';
9 |
10 |
11 | @import './slider';
12 | @import './deco';
13 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
3 | const BrowserSyncPlugin = require('browser-sync-webpack-plugin')
4 | const CopyPlugin = require('copy-webpack-plugin')
5 |
6 | const finalPath = path.resolve(__dirname, 'dist')
7 |
8 | module.exports = {
9 | mode: 'development',
10 | entry: './src/js/index.js',
11 | output: {
12 | path: finalPath,
13 | filename: 'app.js',
14 | },
15 | resolve: {
16 | alias: {
17 | img: path.resolve(__dirname, 'src/img'),
18 | '@js': path.resolve(__dirname, 'src/js'),
19 | font: path.resolve(__dirname, 'src/fonts'),
20 | },
21 | },
22 | module: {
23 | rules: [
24 | {
25 | test: /\.js$/,
26 | exclude: /node_modules/,
27 | use: {
28 | loader: 'babel-loader',
29 | options: {
30 | presets: ['@babel/preset-env'],
31 | },
32 | },
33 | },
34 | {
35 | test: /\.scss$/,
36 | use: [
37 | MiniCssExtractPlugin.loader,
38 | 'css-loader',
39 | 'sass-loader',
40 | ],
41 | },
42 | {
43 | test: /\.(png|jpe?g|gif|svg)$/,
44 | use: [
45 | {
46 | loader: 'file-loader',
47 | options: {
48 | name: '[name].[ext]',
49 | outputPath: 'img',
50 | },
51 | },
52 | ],
53 | },
54 | {
55 | test: /\.(woff|woff2|ttf|otf|eot)$/,
56 | use: [
57 | {
58 | loader: 'file-loader',
59 | options: {
60 | name: '[name].[ext]',
61 | outputPath: 'fonts',
62 | },
63 | },
64 | ],
65 | },
66 | {
67 | test: /\.(glsl|vs|fs|vert|frag)$/,
68 | exclude: /node_modules/,
69 | use: [
70 | 'raw-loader',
71 | 'glslify-loader',
72 | ],
73 | },
74 | ],
75 | },
76 | plugins: [
77 | new CopyPlugin([
78 | { from: './src/fonts', to: path.join(finalPath, '/fonts'), force: true },
79 | { from: './src/img', to: path.join(finalPath, '/img'), force: true },
80 | ]),
81 | new MiniCssExtractPlugin({
82 | filename: 'app.css',
83 | }),
84 | new BrowserSyncPlugin({
85 | host: 'localhost',
86 | port: 3000,
87 | server: { baseDir: '.' },
88 | browser: 'google chrome',
89 | }),
90 | ],
91 | }
92 |
--------------------------------------------------------------------------------