├── .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 | ![Physics-based 3D cloth with cannon.js and three.js](https://tympanus.net/codrops/wp-content/uploads/2020/02/3DCloth_featured.jpg) 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 |
19 |
20 |

3D Cloth Slideshow

21 |

with Three.js & Cannon.js

22 |
23 | 28 |
29 |
30 | 59 | 67 | 68 |
69 |
70 |
71 |
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 | --------------------------------------------------------------------------------