├── .babelrc.json ├── .env_sample ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.html ├── js │ ├── components │ │ └── scene.js │ ├── glsl │ │ ├── main.frag │ │ └── main.vert │ ├── index.js │ ├── managers │ │ └── LoaderManager.js │ └── utils │ │ └── isTouch.js ├── public │ └── img │ │ ├── favicon.svg │ │ └── matcap.png └── scss │ ├── components │ ├── footer.scss │ ├── scene.scss │ └── title.scss │ ├── imports │ ├── _colors.scss │ ├── _easings.scss │ ├── _fonts.scss │ ├── _media-queries.scss │ └── index.scss │ ├── includes │ ├── reset.scss │ └── root.scss │ └── style.scss └── vite.config.js /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /.env_sample: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Robpayot/vite-threejs-template/ba3c6cf748e3c4a7541a82e742ea19a1bd997473/.env_sample -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "rules": { 4 | "prefer-template": "off", 5 | "no-var": 1, 6 | "no-unused-vars": 1, 7 | "camelcase": 1, 8 | "no-nested-ternary": 1, 9 | "no-console": 1, 10 | "no-template-curly-in-string": 1, 11 | "no-self-compare": 1, 12 | "import/prefer-default-export": 0, 13 | "arrow-body-style": 1, 14 | "import/no-extraneous-dependencies": ["off", { "devDependencies": false }] 15 | }, 16 | "ignorePatterns": ["dist", "node_modules", "vite.config.js"], 17 | "env": { 18 | "browser": true, 19 | "es6": true 20 | }, 21 | "extends": ["eslint:recommended", "prettier"], 22 | "parserOptions": { 23 | "ecmaVersion": 2021, 24 | "sourceType": "module" 25 | }, 26 | "plugins": ["prettier"] 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "printWidth": 120, 6 | "semi": false 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [Ron Waller] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Capture d'écran 2023-03-08 111345](https://user-images.githubusercontent.com/5593293/223686025-03039501-6210-4032-98e9-cc2cca840a7f.png) 2 | 3 | 4 | # Vite template builder to quickly generate Threejs code in the browser. 5 | 6 | This repository will help creating 3D environment using [Threejs](https://threejs.org/examples/#webgl_animation_keyframes), a powerful WebGL library. It is powered by [Vite](https://vitejs.dev/guide/why.html) ⚡️ that quickly compiles anything you need, it is also including [Sass](https://sass-lang.com/guide), [Babel](https://babeljs.io/), [Eslint](https://eslint.org/), [Prettier](https://prettier.io/), [lil-gui](https://www.npmjs.com/package/lil-gui) and [GSAP](https://greensock.com/docs/) for animations. 7 | 8 | It is also including a LoaderManager JS file to easily load your assets: (Image, Textures, 3D models...) in one function. 9 | 10 | ## [See it live](https://robpayot.github.io/vite-threejs-template/) 11 | 12 | ## How to install 13 | 14 | Clone the repository or download it in zip format, then 15 | 16 | ### Open Terminial 17 | 18 | Navigate to projects folder 19 | 20 | Install dependencies 21 | 22 | ```bash 23 | npm install 24 | ``` 25 | 26 | Start the dev server 27 | 28 | ```bash 29 | npm run dev 30 | ``` 31 | 32 | ### Build Project 33 | 34 | To build for production 35 | 36 | ```bash 37 | npm run build 38 | ``` 39 | 40 | ## How to Use 41 | 42 | - Use the 'src' folder for all project files. 43 | - HTML 44 | - JS 45 | - SCSS 46 | - vite.config.js file sets up project input to 'src' folder. 47 | - Use eslintrc file to configure linting rules 48 | - Use prettierrc file to configure formatting rules 49 | 50 | ## Useful links 51 | - [Threejs docs](https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene) 52 | - [GSAP docs](https://greensock.com/docs/) 53 | - [Vite docs](https://vitejs.dev/config/) 54 | - [Eslint](https://eslint.org/) 55 | - [Prettier](https://prettier.io/) 56 | 57 | ## License 58 | 59 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "vite-2d-shader-template", 4 | "version": "0.0.0", 5 | "description": "Template to quickly code in Threejs, powered by Vite.", 6 | "author": "Robin Payot ", 7 | "license": "MIT", 8 | "type": "module", 9 | "keywords": [ 10 | "threejs", 11 | "vite", 12 | "useless", 13 | "fun", 14 | "code" 15 | ], 16 | "contributors": [ 17 | "Robin Payot " 18 | ], 19 | "scripts": { 20 | "dev": "vite", 21 | "build": "vite build --emptyOutDir", 22 | "preview": "vite preview", 23 | "deploy": "vite build --emptyOutDir && gh-pages -d dist" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.19.3", 27 | "@babel/eslint-parser": "^7.19.1", 28 | "@babel/plugin-proposal-class-properties": "^7.14.5", 29 | "@babel/preset-env": "^7.15.8", 30 | "@types/node": "^16.11.26", 31 | "eslint": "^8.10.0", 32 | "eslint-config-airbnb": "^19.0.4", 33 | "eslint-config-prettier": "^8.5.0", 34 | "eslint-config-wesbos": "^3.0.2", 35 | "eslint-plugin-html": "^6.2.0", 36 | "eslint-plugin-import": "^2.25.4", 37 | "eslint-plugin-prettier": "^4.0.0", 38 | "gh-pages": "^5.0.0", 39 | "glsl-noise": "^0.0.0", 40 | "glslify": "^7.1.1", 41 | "prettier": "^2.5.1", 42 | "rollup-plugin-glslify": "^1.3.1", 43 | "sass": "^1.58.0", 44 | "stats-js": "^1.0.1", 45 | "vite": "^4.0.1" 46 | }, 47 | "dependencies": { 48 | "gsap": "^3.11.3", 49 | "lil-gui": "^0.17.0", 50 | "three": "^0.149.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Vite Threejs template 11 | 12 | 19 | 20 | 21 |
22 |

How to code that?

23 | 24 | 33 |
34 | 35 | -------------------------------------------------------------------------------- /src/js/components/scene.js: -------------------------------------------------------------------------------- 1 | import { 2 | Color, 3 | WebGLRenderer, 4 | Scene, 5 | PerspectiveCamera, 6 | Mesh, 7 | SphereGeometry, 8 | MeshMatcapMaterial, 9 | AxesHelper, 10 | } from 'three' 11 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 12 | import Stats from 'stats-js' 13 | import LoaderManager from '@/js/managers/LoaderManager' 14 | import GUI from 'lil-gui' 15 | 16 | export default class MainScene { 17 | #canvas 18 | #renderer 19 | #scene 20 | #camera 21 | #controls 22 | #stats 23 | #width 24 | #height 25 | #mesh 26 | #guiObj = { 27 | y: 0, 28 | showTitle: true, 29 | } 30 | 31 | constructor() { 32 | this.#canvas = document.querySelector('.scene') 33 | 34 | this.init() 35 | } 36 | 37 | init = async () => { 38 | // Preload assets before initiating the scene 39 | const assets = [ 40 | { 41 | name: 'matcap', 42 | texture: './img/matcap.png', 43 | }, 44 | ] 45 | 46 | await LoaderManager.load(assets) 47 | 48 | this.setStats() 49 | this.setGUI() 50 | this.setScene() 51 | this.setRender() 52 | this.setCamera() 53 | this.setControls() 54 | this.setAxesHelper() 55 | 56 | this.setSphere() 57 | 58 | this.handleResize() 59 | 60 | // start RAF 61 | this.events() 62 | } 63 | 64 | /** 65 | * Our Webgl renderer, an object that will draw everything in our canvas 66 | * https://threejs.org/docs/?q=rend#api/en/renderers/WebGLRenderer 67 | */ 68 | setRender() { 69 | this.#renderer = new WebGLRenderer({ 70 | canvas: this.#canvas, 71 | antialias: true, 72 | }) 73 | } 74 | 75 | /** 76 | * This is our scene, we'll add any object 77 | * https://threejs.org/docs/?q=scene#api/en/scenes/Scene 78 | */ 79 | setScene() { 80 | this.#scene = new Scene() 81 | this.#scene.background = new Color(0xffffff) 82 | } 83 | 84 | /** 85 | * Our Perspective camera, this is the point of view that we'll have 86 | * of our scene. 87 | * A perscpective camera is mimicing the human eyes so something far we'll 88 | * look smaller than something close 89 | * https://threejs.org/docs/?q=pers#api/en/cameras/PerspectiveCamera 90 | */ 91 | setCamera() { 92 | const aspectRatio = this.#width / this.#height 93 | const fieldOfView = 60 94 | const nearPlane = 0.1 95 | const farPlane = 10000 96 | 97 | this.#camera = new PerspectiveCamera(fieldOfView, aspectRatio, nearPlane, farPlane) 98 | this.#camera.position.y = 5 99 | this.#camera.position.x = 5 100 | this.#camera.position.z = 5 101 | this.#camera.lookAt(0, 0, 0) 102 | 103 | this.#scene.add(this.#camera) 104 | } 105 | 106 | /** 107 | * Threejs controls to have controls on our scene 108 | * https://threejs.org/docs/?q=orbi#examples/en/controls/OrbitControls 109 | */ 110 | setControls() { 111 | this.#controls = new OrbitControls(this.#camera, this.#renderer.domElement) 112 | this.#controls.enableDamping = true 113 | // this.#controls.dampingFactor = 0.04 114 | } 115 | 116 | /** 117 | * Axes Helper 118 | * https://threejs.org/docs/?q=Axesh#api/en/helpers/AxesHelper 119 | */ 120 | setAxesHelper() { 121 | const axesHelper = new AxesHelper(3) 122 | this.#scene.add(axesHelper) 123 | } 124 | 125 | /** 126 | * Create a SphereGeometry 127 | * https://threejs.org/docs/?q=box#api/en/geometries/SphereGeometry 128 | * with a Basic material 129 | * https://threejs.org/docs/?q=mesh#api/en/materials/MeshBasicMaterial 130 | */ 131 | setSphere() { 132 | const geometry = new SphereGeometry(1, 32, 32) 133 | const material = new MeshMatcapMaterial({ matcap: LoaderManager.assets['matcap'].texture }) 134 | 135 | this.#mesh = new Mesh(geometry, material) 136 | this.#scene.add(this.#mesh) 137 | } 138 | 139 | /** 140 | * Build stats to display fps 141 | */ 142 | setStats() { 143 | this.#stats = new Stats() 144 | this.#stats.showPanel(0) 145 | document.body.appendChild(this.#stats.dom) 146 | } 147 | 148 | setGUI() { 149 | const titleEl = document.querySelector('.main-title') 150 | 151 | const handleChange = () => { 152 | this.#mesh.position.y = this.#guiObj.y 153 | titleEl.style.display = this.#guiObj.showTitle ? 'block' : 'none' 154 | } 155 | 156 | const gui = new GUI() 157 | gui.add(this.#guiObj, 'y', -3, 3).onChange(handleChange) 158 | gui.add(this.#guiObj, 'showTitle').name('show title').onChange(handleChange) 159 | } 160 | /** 161 | * List of events 162 | */ 163 | events() { 164 | window.addEventListener('resize', this.handleResize, { passive: true }) 165 | this.draw(0) 166 | } 167 | 168 | // EVENTS 169 | 170 | /** 171 | * Request animation frame function 172 | * This function is called 60/time per seconds with no performance issue 173 | * Everything that happens in the scene is drawed here 174 | * @param {Number} now 175 | */ 176 | draw = () => { 177 | // now: time in ms 178 | this.#stats.begin() 179 | 180 | if (this.#controls) this.#controls.update() // for damping 181 | this.#renderer.render(this.#scene, this.#camera) 182 | 183 | this.#stats.end() 184 | this.raf = window.requestAnimationFrame(this.draw) 185 | } 186 | 187 | /** 188 | * On resize, we need to adapt our camera based 189 | * on the new window width and height and the renderer 190 | */ 191 | handleResize = () => { 192 | this.#width = window.innerWidth 193 | this.#height = window.innerHeight 194 | 195 | // Update camera 196 | this.#camera.aspect = this.#width / this.#height 197 | this.#camera.updateProjectionMatrix() 198 | 199 | const DPR = window.devicePixelRatio ? window.devicePixelRatio : 1 200 | 201 | this.#renderer.setPixelRatio(DPR) 202 | this.#renderer.setSize(this.#width, this.#height) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/js/glsl/main.frag: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() { 4 | vec3 color = vec3(1.); 5 | gl_FragColor.a = vec4(color, 1.); 6 | } -------------------------------------------------------------------------------- /src/js/glsl/main.vert: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() { 4 | vUv = uv; 5 | 6 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 7 | } 8 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | // Test import of a JavaScript module 2 | import Scene from '@/js/components/scene' 3 | 4 | (() => { 5 | // scene 6 | new Scene() 7 | })() 8 | -------------------------------------------------------------------------------- /src/js/managers/LoaderManager.js: -------------------------------------------------------------------------------- 1 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' 2 | import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js' 3 | import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' 4 | import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js' 5 | import { TextureLoader } from 'three' 6 | 7 | class LoaderManager { 8 | #assets 9 | #textureLoader = new TextureLoader() 10 | #GLTFLoader = new GLTFLoader() 11 | #OBJLoader = new OBJLoader() 12 | #DRACOLoader = new DRACOLoader() 13 | #FontLoader = new FontLoader() 14 | 15 | constructor() { 16 | this.#assets = {} // Dictionary of assets, can be different type, gltf, texture, img, font, feel free to make a Enum if using TypeScript 17 | } 18 | 19 | get assets() { 20 | return this.#assets 21 | } 22 | 23 | set assets(value) { 24 | this.#assets = value 25 | } 26 | 27 | /** 28 | * Public method 29 | */ 30 | 31 | get(name) { 32 | return this.#assets[name] 33 | } 34 | 35 | load = (data) => 36 | new Promise((resolve) => { 37 | const promises = [] 38 | for (let i = 0; i < data.length; i++) { 39 | const { name, gltf, texture, img, font, obj } = data[i] 40 | 41 | if (!this.#assets[name]) { 42 | this.#assets[name] = {} 43 | } 44 | 45 | if (gltf) { 46 | promises.push(this.loadGLTF(gltf, name)) 47 | } 48 | 49 | if (texture) { 50 | promises.push(this.loadTexture(texture, name)) 51 | } 52 | 53 | if (img) { 54 | promises.push(this.loadImage(img, name)) 55 | } 56 | 57 | if (font) { 58 | promises.push(this.loadFont(font, name)) 59 | } 60 | 61 | if (obj) { 62 | promises.push(this.loadObj(obj, name)) 63 | } 64 | } 65 | 66 | Promise.all(promises).then(() => resolve()) 67 | }) 68 | 69 | loadGLTF(url, name) { 70 | return new Promise((resolve) => { 71 | this.#DRACOLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/') 72 | this.#GLTFLoader.setDRACOLoader(this.#DRACOLoader) 73 | 74 | this.#GLTFLoader.load( 75 | url, 76 | (result) => { 77 | this.#assets[name].gltf = result 78 | resolve(result) 79 | }, 80 | undefined, 81 | (e) => { 82 | console.log(e) 83 | } 84 | ) 85 | }) 86 | } 87 | 88 | loadTexture(url, name) { 89 | if (!this.#assets[name]) { 90 | this.#assets[name] = {} 91 | } 92 | return new Promise((resolve) => { 93 | this.#textureLoader.load(url, (result) => { 94 | this.#assets[name].texture = result 95 | resolve(result) 96 | }) 97 | }) 98 | } 99 | 100 | loadImage(url, name) { 101 | return new Promise((resolve) => { 102 | const image = new Image() 103 | 104 | image.onload = () => { 105 | this.#assets[name].img = image 106 | resolve(image) 107 | } 108 | 109 | image.src = url 110 | }) 111 | } 112 | 113 | loadFont(url, name) { 114 | // you can convert font to typeface.json using https://gero3.github.io/facetype.js/ 115 | return new Promise((resolve) => { 116 | this.#FontLoader.load( 117 | url, 118 | 119 | // onLoad callback 120 | (font) => { 121 | this.#assets[name].font = font 122 | resolve(font) 123 | }, 124 | 125 | // onProgress callback 126 | () => 127 | // xhr 128 | { 129 | // console.log((xhr.loaded / xhr.total) * 100 + '% loaded') 130 | }, 131 | 132 | // onError callback 133 | (err) => { 134 | console.log('An error happened', err) 135 | } 136 | ) 137 | }) 138 | } 139 | 140 | // https://threejs.org/docs/#examples/en/loaders/OBJLoader 141 | loadObj(url, name) { 142 | return new Promise((resolve) => { 143 | // load a resource 144 | this.#OBJLoader.load( 145 | // resource URL 146 | url, 147 | // called when resource is loaded 148 | (object) => { 149 | this.#assets[name].obj = object 150 | resolve(object) 151 | }, 152 | // onProgress callback 153 | () => 154 | // xhr 155 | { 156 | // console.log((xhr.loaded / xhr.total) * 100 + '% loaded') 157 | }, 158 | // called when loading has errors 159 | (err) => { 160 | console.log('An error happened', err) 161 | } 162 | ) 163 | }) 164 | } 165 | } 166 | 167 | export default new LoaderManager() 168 | -------------------------------------------------------------------------------- /src/js/utils/isTouch.js: -------------------------------------------------------------------------------- 1 | export function isTouch() { 2 | if ('standalone' in navigator) { 3 | return true // iOS devices 4 | } 5 | const hasCoarse = window.matchMedia('(pointer: coarse)').matches 6 | if (hasCoarse) { 7 | return true 8 | } 9 | const hasPointer = window.matchMedia('(pointer: fine)').matches 10 | if (hasPointer) { 11 | return false // prioritize mouse control 12 | } 13 | 14 | // Otherwise, fall-back to older style mechanisms. 15 | return 'ontouchstart' in window || navigator.maxTouchPoints > 0 16 | } 17 | -------------------------------------------------------------------------------- /src/public/img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/public/img/matcap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Robpayot/vite-threejs-template/ba3c6cf748e3c4a7541a82e742ea19a1bd997473/src/public/img/matcap.png -------------------------------------------------------------------------------- /src/scss/components/footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | bottom: 50px; 3 | z-index: 200; 4 | width: 100%; 5 | padding: 0px 40px; 6 | pointer-events: none; 7 | display: flex; 8 | align-items: center; 9 | pointer-events: auto; 10 | position: absolute; 11 | justify-content: end; 12 | 13 | @include respond-to(smartphone) { 14 | bottom: 100px; 15 | padding: 0px 20px; 16 | } 17 | 18 | &__link { 19 | display: flex; 20 | align-items: center; 21 | justify-self: center; 22 | margin: 0 8px; 23 | transform: scale(0.8); 24 | transition: transform 0.3s var(--ease-out-bounce); 25 | 26 | @include respond-to(smartphone) { 27 | margin: 0 5px; 28 | } 29 | 30 | &:first-child { 31 | margin-left: 0; 32 | } 33 | 34 | &:last-child { 35 | margin-right: 0; 36 | } 37 | 38 | &--learn { 39 | font-size: 27px; 40 | color: var(--color-footer); 41 | @include respond-to(smartphone) { 42 | margin-right: auto; 43 | font-size: 22px; 44 | } 45 | } 46 | 47 | &:hover { 48 | transform: none; 49 | transition: transform 0.3s var(--ease-out-bounce); 50 | } 51 | } 52 | 53 | &__icon { 54 | width: 24px; 55 | height: 24px; 56 | 57 | @include respond-to(smartphone) { 58 | width: 18px; 59 | height: 18px; 60 | } 61 | 62 | path { 63 | fill: var(--color-footer); 64 | stroke: var(--color-footer); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/scss/components/scene.scss: -------------------------------------------------------------------------------- 1 | .scene { 2 | height: 100%; 3 | width: 100%; 4 | position: relative; 5 | } 6 | -------------------------------------------------------------------------------- /src/scss/components/title.scss: -------------------------------------------------------------------------------- 1 | .main-title { 2 | position: absolute; 3 | text-align: center; 4 | font-size: 7vw; 5 | width: 100%; 6 | top: 50%; 7 | left: 0; 8 | z-index: 10; 9 | transform: translateY(-50%); 10 | color: var(--color-orange); 11 | } -------------------------------------------------------------------------------- /src/scss/imports/_colors.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-white: #ffffff; 3 | --color-footer: #f8c291; 4 | --color-orange: #f8c291; 5 | } 6 | -------------------------------------------------------------------------------- /src/scss/imports/_easings.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | // SINE 3 | --ease-in-sine: cubic-bezier(0.47, 0, 0.745, 0.715); 4 | --ease-out-sine: cubic-bezier(0.39, 0.575, 0.565, 1); 5 | --ease-in-out-sine: cubic-bezier(0.445, 0.05, 0.55, 0.95); 6 | 7 | // QUAD 8 | --ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53); 9 | --ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94); 10 | --ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955); 11 | 12 | // CUBIC 13 | --ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19); 14 | --ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1); 15 | --ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1); 16 | 17 | // QUART 18 | --ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22); 19 | --ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1); 20 | --ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1); 21 | 22 | // QUINT 23 | --ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06); 24 | --ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1); 25 | --ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1); 26 | 27 | // EXPO 28 | --ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035); 29 | --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1); 30 | --ease-in-out-expo: cubic-bezier(1, 0, 0, 1); 31 | 32 | // CIRC 33 | --ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335); 34 | --ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1); 35 | --ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86); 36 | 37 | // BOUNCE 38 | --ease-out-bounce: cubic-bezier(0.22, 1.45, 0.36, 1); 39 | } 40 | -------------------------------------------------------------------------------- /src/scss/imports/_fonts.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@600&display=swap'); -------------------------------------------------------------------------------- /src/scss/imports/_media-queries.scss: -------------------------------------------------------------------------------- 1 | $breakpoint-mobile: 768px; 2 | $breakpoint-tablet: 1024px; 3 | 4 | @mixin respond-to($point) { 5 | @if $point == desktop { 6 | @media only screen and (min-width: $breakpoint-tablet + 1) { 7 | @content; 8 | } 9 | } @else if $point == tablet { 10 | @media only screen and (max-width: $breakpoint-tablet) { 11 | @content; 12 | } 13 | } @else if $point == smartphone { 14 | @media only screen and (max-width: $breakpoint-mobile) { 15 | @content; 16 | } 17 | } @else if $point == above-smartphone { 18 | @media only screen and (min-width: $breakpoint-mobile + 1) { 19 | @content; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/scss/imports/index.scss: -------------------------------------------------------------------------------- 1 | @import './media-queries'; 2 | @import './colors'; 3 | @import './fonts'; 4 | @import './easings'; 5 | -------------------------------------------------------------------------------- /src/scss/includes/reset.scss: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | border: 0; 15 | font: inherit; 16 | font-size: 100%; 17 | margin: 0; 18 | padding: 0; 19 | vertical-align: baseline; 20 | } 21 | 22 | html { 23 | line-height: 1; 24 | } 25 | 26 | ol, ul { 27 | list-style: none; 28 | } 29 | 30 | table { 31 | border-collapse: collapse; 32 | border-spacing: 0; 33 | } 34 | 35 | caption, th, td { 36 | font-weight: normal; 37 | text-align: left; 38 | vertical-align: middle; 39 | } 40 | 41 | q, blockquote { 42 | quotes: none; 43 | } 44 | q:before, q:after, blockquote:before, blockquote:after { 45 | content: ''; 46 | } 47 | 48 | a img { 49 | border: 0; 50 | } 51 | 52 | article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { 53 | display: block; 54 | } 55 | -------------------------------------------------------------------------------- /src/scss/includes/root.scss: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | overflow: hidden; 4 | 5 | &.scroll { 6 | overflow: visible; 7 | } 8 | } 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: inherit; 14 | } 15 | 16 | body { 17 | background-color: black; 18 | color: white; 19 | line-height: 1.3; 20 | position: relative; 21 | font-family: 'Roboto Slab', serif; 22 | } 23 | 24 | a { 25 | color: inherit; 26 | text-decoration: none; 27 | } 28 | -------------------------------------------------------------------------------- /src/scss/style.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/index'; 2 | 3 | @import 'includes/reset'; 4 | @import 'includes/root'; 5 | 6 | @import 'components/scene'; 7 | @import 'components/title'; 8 | @import 'components/footer'; 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import glslify from 'rollup-plugin-glslify' 3 | import * as path from 'path' 4 | 5 | export default defineConfig({ 6 | root: 'src', 7 | base: '/vite-threejs-template/', // for Github pages, otherwise use './' 8 | build: { 9 | outDir: '../dist', 10 | }, 11 | server: { 12 | host: true, // to test on other devices with IP address 13 | }, 14 | resolve: { 15 | alias: { 16 | '@': path.resolve(__dirname, './src'), 17 | }, 18 | }, 19 | plugins: [glslify()], 20 | }) 21 | --------------------------------------------------------------------------------