├── .gitignore
├── Experience
├── Camera.js
├── Controls.js
├── Experience.js
├── Preloader.js
├── Renderer.js
├── Theme.js
├── Utils
│ ├── Debug.js
│ ├── Resources.js
│ ├── Sizes.js
│ ├── Time.js
│ ├── assets.js
│ └── convertDivsToSpans.js
└── World
│ ├── Bike.js
│ ├── Environment.js
│ ├── Floor.js
│ └── World.js
├── README.md
├── favicon.svg
├── index.html
├── main.js
├── package-lock.json
├── package.json
├── public
├── draco
│ ├── README.md
│ ├── draco_decoder.js
│ ├── draco_decoder.wasm
│ ├── draco_encoder.js
│ ├── draco_wasm_wrapper.js
│ └── gltf
│ │ ├── draco_decoder.js
│ │ ├── draco_decoder.wasm
│ │ ├── draco_encoder.js
│ │ └── draco_wasm_wrapper.js
├── fonts
│ ├── FormulaCondensed-Black.otf
│ ├── FormulaCondensed-Bold.otf
│ ├── FormulaCondensed-Light.otf
│ └── FormulaCondensed-Regular.otf
├── images
│ ├── bike.jpeg
│ ├── bike.webp
│ ├── cockpit.jpeg
│ ├── cockpit.webp
│ ├── drivetrain.jpeg
│ └── drivetrain.webp
├── models
│ └── bike-model.glb
└── textures
│ ├── environmentMap
│ ├── nx.png
│ ├── ny.png
│ ├── nz.png
│ ├── px.png
│ ├── py.png
│ └── pz.png
│ └── kda.mp4
└── style.css
/.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 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/Experience/Camera.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
3 |
4 | import Experience from './Experience.js'
5 |
6 | export default class Camera {
7 | constructor() {
8 | this.experience = new Experience()
9 | this.sizes = this.experience.sizes
10 | this.scene = this.experience.scene
11 | this.canvas = this.experience.canvas
12 | this.debug = this.experience.debug
13 |
14 | // Debug
15 | if(this.debug.active) {
16 | this.debugFolder = this.debug.ui.addFolder('camera')
17 | }
18 |
19 | // // Grid Helper
20 | // const size = 20;
21 | // const divisions = 20;
22 |
23 | // const gridHelper = new THREE.GridHelper( size, divisions );
24 | // this.scene.add(gridHelper)
25 |
26 | // // Axes Helper
27 | // const axesHelper = new THREE.AxesHelper(10);
28 | // this.scene.add(axesHelper);
29 |
30 | // Setup
31 | this.createPerspectiveCamera()
32 | this.createOrthographicCamera()
33 | // this.setOrbitControls()
34 | }
35 |
36 | createPerspectiveCamera() {
37 | this.perspectiveCamera = new THREE.PerspectiveCamera(
38 | 35,
39 | this.sizes.aspect,
40 | 0.1,
41 | 1000
42 | )
43 | this.scene.add(this.perspectiveCamera)
44 |
45 | this.perspectiveCamera.position.y = 0.5
46 | this.perspectiveCamera.position.z = 4
47 | this.perspectiveCamera.lookAt(0, 0.65, 0);
48 |
49 | // // Perspective Camera Helper
50 | // this.perspectiveCameraHelper = new THREE.CameraHelper(this.perspectiveCamera)
51 | // this.scene.add(this.perspectiveCameraHelper)
52 |
53 | // Debug
54 | if(this.debug.active) {
55 | this.debugFolder
56 | .add(this.perspectiveCamera.position, 'x')
57 | .name('camPosX')
58 | .min(-30)
59 | .max(30)
60 | .step(0.001)
61 |
62 | this.debugFolder
63 | .add(this.perspectiveCamera.position, 'y')
64 | .name('camPosY')
65 | .min(-30)
66 | .max(30)
67 | .step(0.001)
68 |
69 | this.debugFolder
70 | .add(this.perspectiveCamera.position, 'z')
71 | .name('camPosZ')
72 | .min(-30)
73 | .max(30)
74 | .step(0.001)
75 | }
76 | }
77 |
78 | createOrthographicCamera() {
79 | this.orthographicCamera = new THREE.OrthographicCamera(
80 | (-this.sizes.aspect * this.sizes.frustum) / 2,
81 | (this.sizes.aspect * this.sizes.frustum) / 2,
82 | this.sizes.frustum / 2,
83 | -this.sizes.frustum / 2,
84 | -10,
85 | 10
86 | )
87 |
88 | this.orthographicCamera.position.y = 1.25
89 | this.orthographicCamera.rotation.x = -Math.PI / 24
90 |
91 | this.scene.add(this.orthographicCamera)
92 |
93 | // // Orthographic Camera Helper
94 | // this.orthographicCameraHelper = new THREE.CameraHelper(this.orthographicCamera)
95 | // this.scene.add(this.orthographicCameraHelper)
96 | }
97 |
98 | // setOrbitControls() {
99 | // this.controls = new OrbitControls(this.orthographicCamera, this.canvas)
100 | // this.controls.enableDamping = true
101 | // this.controls.enableZoom = true
102 | // }
103 |
104 | resize() {
105 | // Updating Perspective Camera on Resize
106 | this.perspectiveCamera.aspect = this.sizes.aspect
107 | this.perspectiveCamera.updateProjectionMatrix()
108 |
109 | // Updating Orthographic Camera on Resize
110 | this.orthographicCamera.left = (-this.sizes.aspect * this.sizes.frustum) / 2
111 | this.orthographicCamera.right = (this.sizes.aspect * this.sizes.frustum) / 2
112 | this.orthographicCamera.top = this.sizes.frustum / 2
113 | this.orthographicCamera.bottom = -this.sizes.frustum / 2
114 | this.orthographicCamera.updateProjectionMatrix()
115 | }
116 |
117 | update() {
118 | // this.controls.update()
119 |
120 | // // Updating Orthographic Camera Helper
121 | // this.orthographicCameraHelper.matrixWorldNeedsUpdate = true
122 | // this.orthographicCameraHelper.update()
123 | // this.orthographicCameraHelper.position.copy(this.orthographicCamera.position)
124 | // this.orthographicCameraHelper.position.copy(this.orthographicCamera.rotation)
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Experience/Controls.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import GSAP from 'gsap'
3 | import { ScrollTrigger } from 'gsap/ScrollTrigger.js'
4 | import ASScroll from '@ashthornton/asscroll'
5 | import Experience from './Experience.js'
6 |
7 | export default class Controls {
8 | constructor() {
9 | this.experience = new Experience()
10 | this.scene = this.experience.scene
11 | this.resources = this.experience.resources
12 | this.sizes = this.experience.sizes
13 | this.time = this.experience.time
14 | this.camera = this.experience.camera
15 | this.actualBike = this.experience.world.bike.actualBike
16 | this.bikeChildren = this.experience.world.bike.bikeChildren
17 | this.zoom = {
18 | zoomValue: this.camera.perspectiveCamera.zoom
19 | }
20 |
21 | GSAP.registerPlugin(ScrollTrigger)
22 |
23 | document.querySelector('.page').style.overflow = 'visible'
24 |
25 | if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
26 | console.log('desktop')
27 | this.setSmoothScroll()
28 | }
29 | this.setScrollTrigger()
30 | }
31 |
32 | setupASScroll() {
33 | // https://github.com/ashthornton/asscroll
34 | const asscroll = new ASScroll({
35 | // ease: 0.5,
36 | disableRaf: true
37 | })
38 |
39 |
40 | GSAP.ticker.add(asscroll.update)
41 |
42 | ScrollTrigger.defaults({
43 | scroller: asscroll.containerElement })
44 |
45 |
46 | ScrollTrigger.scrollerProxy(asscroll.containerElement, {
47 | scrollTop(value) {
48 | if (arguments.length) {
49 | asscroll.currentPos = value
50 | return
51 | }
52 | return asscroll.currentPos
53 | },
54 | getBoundingClientRect() {
55 | return {
56 | top: 0,
57 | left: 0,
58 | width: window.innerWidth,
59 | height: window.innerHeight
60 | }
61 | },
62 | fixedMarkers: true })
63 |
64 |
65 | asscroll.on('update', ScrollTrigger.update)
66 | ScrollTrigger.addEventListener('refresh', asscroll.resize)
67 |
68 | requestAnimationFrame(() => {
69 | asscroll.enable({
70 | newScrollElements: document.querySelectorAll('.gsap-marker-start, .gsap-marker-end, [asscroll]')
71 | })
72 | })
73 | return asscroll;
74 | }
75 |
76 | setSmoothScroll() {
77 | this.asscroll = this.setupASScroll()
78 | }
79 |
80 | reset() {
81 | this.actualBike.scale.set(0.65, 0.65, 0.65)
82 | this.actualBike.position.set(0, 0, 0)
83 | this.actualBike.rotation.y = 0
84 | this.camera.perspectiveCamera.position.x = 0
85 | this.camera.perspectiveCamera.position.y = 0.5
86 | this.camera.perspectiveCamera.position.z = 4
87 | this.camera.perspectiveCamera.zoom = 1
88 | this.zoom.zoomValue = 1
89 | }
90 |
91 | resetMobile() {
92 | this.actualBike.scale.set(0.5, 0.5, 0.5)
93 | this.actualBike.rotation.set(0, -Math.PI/2 , 0)
94 | this.actualBike.position.set(0, 0, 0)
95 | this.camera.perspectiveCamera.position.x = 0
96 | this.camera.perspectiveCamera.position.y = 0.4
97 | this.camera.perspectiveCamera.position.z = 4
98 | this.camera.perspectiveCamera.zoom = 1
99 | this.zoom.zoomValue = 1
100 | }
101 |
102 | setScrollTrigger() {
103 | ScrollTrigger.matchMedia({
104 | // Desktop
105 | '(min-width: 969px)': () => {
106 |
107 | // Resets
108 | this.reset()
109 |
110 | // First Section
111 | this.firstMoveTimeline = new GSAP.timeline({
112 | scrollTrigger: {
113 | trigger: '.first-move',
114 | start: 'top top',
115 | end: 'bottom bottom',
116 | scrub: 0.6,
117 | invalidateOnRefresh: true
118 | }
119 | })
120 | .fromTo(this.actualBike.rotation, {
121 | y: 0
122 | },
123 | {
124 | y: Math.PI
125 | }, 'same')
126 | .fromTo(this.camera.perspectiveCamera.position, {
127 | x: 0,
128 | y: 0.5,
129 | z: 4
130 | },
131 | {
132 | x: () => {
133 | if(this.sizes.width > 1300 && this.sizes.height < 1000) {
134 | return -5.2
135 | } else {
136 | return -5
137 | }
138 | },
139 | y: 6,
140 | }, 'same')
141 | .fromTo(this.camera.perspectiveCamera.rotation, {
142 | x: 0.0374824366916615,
143 | y: 0,
144 | z: -0
145 | },
146 | {
147 | x: -0.81,
148 | y: -0.5324252706006514,
149 | z: -0.45011986145587835
150 | }, 'same')
151 | .to(this.zoom, {
152 | zoomValue: 3,
153 | onUpdate: () => {
154 | this.camera.perspectiveCamera.zoom = this.zoom.zoomValue
155 | this.camera.perspectiveCamera.updateProjectionMatrix()
156 | }
157 | }, 'same')
158 |
159 | // Second Section
160 | this.secondMoveTimeline = new GSAP.timeline({
161 | scrollTrigger: {
162 | trigger: '.second-move',
163 | start: 'top top',
164 | end: 'bottom bottom',
165 | scrub: 0.6,
166 | invalidateOnRefresh: true,
167 | },
168 | })
169 | .to(this.actualBike.rotation, {
170 | y: -Math.PI / 4,
171 | }, 'same')
172 | .to(this.camera.perspectiveCamera.position, {
173 | x: () => {
174 | if(this.sizes.width > 1300 && this.sizes.height < 1000) {
175 | return -6.7
176 | } else {
177 | return -7
178 | }
179 | },
180 | y: 2,
181 | }, 'same')
182 | .to(this.camera.perspectiveCamera.rotation, {
183 | x: -0.3340156231020234,
184 | y: -1.0505564481189775,
185 | z: -0.2924724024454449,
186 | }, 'same')
187 | .to(this.zoom, {
188 | zoomValue: 3,
189 | onUpdate: () => {
190 | this.camera.perspectiveCamera.zoom = this.zoom.zoomValue;
191 | this.camera.perspectiveCamera.updateProjectionMatrix();
192 | },
193 | }, 'same')
194 |
195 | // Third Section
196 | this.thirdMoveTimeline = new GSAP.timeline({
197 | scrollTrigger: {
198 | trigger: '.third-move',
199 | start: 'top top',
200 | end: 'bottom bottom',
201 | scrub: 0.6,
202 | invalidateOnRefresh: true
203 | },
204 | })
205 | .to(this.actualBike.rotation, {
206 | y: -Math.PI,
207 | }, 'same')
208 | .to(this.camera.perspectiveCamera.position, {
209 | x: () => {
210 | if(this.sizes.width > 1300 && this.sizes.height < 1000) {
211 | return -4.25
212 | } else {
213 | return -4.1
214 | }
215 | },
216 | y: 3
217 | }, 'same')
218 | .to(this.camera.perspectiveCamera.rotation, {
219 | x: -0.33669463959268153,
220 | y: -0.700986700755924,
221 | z: -0.22203253193071731,
222 | }, 'same')
223 | .to(this.zoom, {
224 | zoomValue: 2.5,
225 | onUpdate: () => {
226 | this.camera.perspectiveCamera.zoom = this.zoom.zoomValue
227 | this.camera.perspectiveCamera.updateProjectionMatrix()
228 | },
229 | }, 'same')
230 |
231 | // Fourth Section
232 | this.fourthMoveTimeline = new GSAP.timeline({
233 | scrollTrigger: {
234 | trigger: '.fourth-move',
235 | start: 'top top',
236 | end: 'bottom bottom',
237 | scrub: 0.6,
238 | invalidateOnRefresh: true
239 | }
240 | })
241 | .to(this.actualBike.rotation, {
242 | y: -Math.PI / 2,
243 | }, 'same')
244 | .to(this.camera.perspectiveCamera.position, {
245 | x: () => {
246 | if(this.sizes.width > 1300 && this.sizes.height < 1000) {
247 | return 2.2
248 | } else {
249 | return 2
250 | }
251 | },
252 | y: 1,
253 | z: 4,
254 | }, 'same')
255 | .to(this.camera.perspectiveCamera.rotation, {
256 | x: -0.02845135092188762,
257 | y: 0.29416856071633857,
258 | z: 0.008251344278639
259 | }, 'same')
260 | .to(this.zoom, {
261 | zoomValue: 1,
262 | onUpdate: () => {
263 | this.camera.perspectiveCamera.zoom = this.zoom.zoomValue
264 | this.camera.perspectiveCamera.updateProjectionMatrix()
265 | },
266 | }, 'same')
267 | },
268 |
269 | // Mobile
270 | '(max-width: 968px)': () => {
271 |
272 | // Resets
273 | this.resetMobile()
274 |
275 | // First Section - Mobile
276 | this.firstMoveTimeline = new GSAP.timeline({
277 | scrollTrigger: {
278 | trigger: '.first-move',
279 | start: 'top top',
280 | end: 'bottom bottom',
281 | scrub: 0.6,
282 | invalidateOnRefresh: true
283 | }
284 | })
285 | .fromTo(this.actualBike.rotation, {
286 | y: -Math.PI / 2
287 | },
288 | {
289 | y: Math.PI / 1,
290 | }, 'same')
291 | .fromTo(this.camera.perspectiveCamera.position, {
292 | x: 0,
293 | y: 0.4,
294 | z: 4,
295 | },
296 | {
297 | x: -4.82,
298 | y: 3,
299 | z: 4,
300 | }, 'same')
301 | .fromTo(this.camera.perspectiveCamera.rotation, {
302 | x: 0.0374824366916615,
303 | y: 0,
304 | z: -0
305 | },
306 | {
307 | x: -0.4826867099146418,
308 | y: -0.7487373908008822,
309 | z: -0.3426445418872183
310 | }, 'same')
311 | .to(this.zoom, {
312 | zoomValue: 2.3,
313 | onUpdate: () => {
314 | this.camera.perspectiveCamera.zoom = this.zoom.zoomValue
315 | this.camera.perspectiveCamera.updateProjectionMatrix()
316 | }
317 | }, 'same')
318 |
319 | // Second Section - Mobile
320 | this.secondMoveTimeline = new GSAP.timeline({
321 | scrollTrigger: {
322 | trigger: '.second-move',
323 | start: 'top top',
324 | end: 'bottom bottom',
325 | scrub: 0.6,
326 | invalidateOnRefresh: true
327 | }
328 | })
329 | .to(this.actualBike.rotation, {
330 | y: -Math.PI / 4,
331 | }, 'same')
332 | .to(this.camera.perspectiveCamera.position, {
333 | x: -7,
334 | y: 2
335 | }, 'same')
336 | .to(this.camera.perspectiveCamera.rotation, {
337 | x: -0.36830437274233147,
338 | y: -0.975248930241726,
339 | z: -0.30922701986576173,
340 | }, 'same')
341 | .to(this.zoom, {
342 | zoomValue: 2.5,
343 | onUpdate: () => {
344 | this.camera.perspectiveCamera.zoom = this.zoom.zoomValue
345 | this.camera.perspectiveCamera.updateProjectionMatrix()
346 | }
347 | }, 'same')
348 |
349 | // Third Section - Mobile
350 | this.thirdMoveTimeline = new GSAP.timeline({
351 | scrollTrigger: {
352 | trigger: '.third-move',
353 | start: 'top top',
354 | end: 'bottom bottom',
355 | // markers: true,
356 | scrub: 0.6,
357 | invalidateOnRefresh: true
358 | }
359 | })
360 | .to(this.actualBike.rotation, {
361 | y: -Math.PI,
362 | }, 'same')
363 | .to(this.camera.perspectiveCamera.position, {
364 | x: -4.15,
365 | y: 1.7,
366 | }, 'same')
367 | .to(this.camera.perspectiveCamera.rotation, {
368 | x: -0.15,
369 | y: -0.82433683382151,
370 | z: -0.17595910659449646,
371 | }, 'same')
372 | .to(this.zoom, {
373 | zoomValue: 3.5,
374 | onUpdate: () => {
375 | this.camera.perspectiveCamera.zoom = this.zoom.zoomValue
376 | this.camera.perspectiveCamera.updateProjectionMatrix()
377 | }
378 | }, 'same')
379 |
380 | // Fourth Section - Mobile
381 | this.fourthMoveTimeline = new GSAP.timeline({
382 | scrollTrigger: {
383 | trigger: '.fourth-move',
384 | start: 'top top',
385 | end: 'bottom bottom',
386 | // markers: true,
387 | scrub: 0.6,
388 | invalidateOnRefresh: true
389 | }
390 | })
391 | .to(this.actualBike.rotation, {
392 | y: -Math.PI / 1.5,
393 | }, 'same')
394 | .to(this.camera.perspectiveCamera.position, {
395 | x: -4.1,
396 | y: 0.5,
397 | }, 'same')
398 | .to(this.zoom, {
399 | zoomValue: 1.5,
400 | onUpdate: () => {
401 | this.camera.perspectiveCamera.zoom = this.zoom.zoomValue
402 | this.camera.perspectiveCamera.updateProjectionMatrix()
403 | }
404 | }, 'same')
405 | .to(this.camera.perspectiveCamera.rotation, {
406 | x: -0.0212333950806064,
407 | y: -0.81785674319681,
408 | z: -0.015494714435393457,
409 | }, 'same')
410 | },
411 |
412 | // all
413 | 'all': () => {
414 | this.sections = document.querySelectorAll('.section')
415 | this.sections.forEach(section => {
416 | this.progressWrapper = section.querySelector('.progress-wrapper')
417 | this.progressBar = section.querySelector('.progress-bar')
418 |
419 | if(section.classList.contains('right')) {
420 | GSAP.to(section, {
421 | borderTopLeftRadius: 10,
422 | scrollTrigger: {
423 | trigger: section,
424 | start: 'top bottom',
425 | end: 'top top',
426 | scrub: 0.6
427 | }
428 | })
429 | GSAP.to(section, {
430 | borderBottomLeftRadius: 700,
431 | scrollTrigger: {
432 | trigger: section,
433 | start: 'bottom bottom',
434 | end: 'bottom top',
435 | scrub: 0.6
436 | }
437 | })
438 | } else {
439 | GSAP.to(section, {
440 | borderTopRightRadius: 10,
441 | scrollTrigger: {
442 | trigger: section,
443 | start: 'top bottom',
444 | end: 'top top',
445 | scrub: 0.6
446 | }
447 | })
448 | GSAP.to(section, {
449 | borderBottomRightRadius: 700,
450 | scrollTrigger: {
451 | trigger: section,
452 | start: 'bottom bottom',
453 | end: 'bottom top',
454 | scrub: 0.6
455 | }
456 | })
457 | }
458 |
459 | GSAP.from(this.progressBar, {
460 | scaleY: 0,
461 | scrollTrigger: {
462 | trigger: section,
463 | start: 'top top',
464 | end: 'bottom bottom',
465 | scrub: 0.4,
466 | pin: this.progressWrapper,
467 | pinSpacing: false
468 | }
469 | })
470 | })
471 | }
472 |
473 | });
474 | }
475 |
476 | resize() {}
477 |
478 | update() {}
479 | }
480 |
--------------------------------------------------------------------------------
/Experience/Experience.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 |
3 | import Sizes from './Utils/Sizes.js'
4 | import Time from './Utils/Time.js'
5 | import Resources from './Utils/Resources.js'
6 | import Debug from './Utils/Debug.js'
7 | import assets from './Utils/assets.js'
8 |
9 | import Camera from './Camera.js'
10 | import Renderer from './Renderer.js'
11 | import Preloader from './Preloader.js'
12 | import Theme from './Theme.js'
13 |
14 | import World from './World/World.js'
15 | import Controls from './Controls.js'
16 |
17 | export default class Experience {
18 | static instance
19 | constructor(canvas) {
20 | if(Experience.instance) {
21 | return Experience.instance
22 | }
23 | Experience.instance = this
24 | this.canvas = canvas
25 | this.scene = new THREE.Scene()
26 | this.debug = new Debug()
27 | this.sizes = new Sizes()
28 | this.time = new Time()
29 | this.camera = new Camera()
30 | this.renderer = new Renderer()
31 | this.resources = new Resources(assets)
32 | this.theme = new Theme()
33 | this.world = new World()
34 | this.preloader = new Preloader()
35 |
36 | this.preloader.on('enableControls', () => {
37 | this.controls = new Controls()
38 | })
39 |
40 | this.sizes.on('resize', () => {
41 | this.resize()
42 | })
43 |
44 | this.time.on('update', () => {
45 | this.update()
46 | })
47 | }
48 |
49 | resize() {
50 | this.camera.resize()
51 | this.world.resize()
52 | this.renderer.resize()
53 | }
54 |
55 | update() {
56 | this.preloader.update()
57 | this.camera.update()
58 | this.world.update()
59 | this.renderer.update()
60 |
61 | if(this.controls) {
62 | this.controls.update()
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Experience/Preloader.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import GSAP from 'gsap'
3 | import { EventEmitter } from 'events'
4 | import convert from './Utils/convertDivsToSpans'
5 |
6 | import Experience from "./Experience"
7 |
8 | export default class Preloader extends EventEmitter {
9 | constructor() {
10 | super()
11 | this.experience = new Experience()
12 | this.scene = this.experience.scene
13 | this.resources = this.experience.resources
14 | this.sizes = this.experience.sizes
15 | this.camera = this.experience.camera
16 | this.world = this.experience.world
17 | this.device = this.sizes.device
18 |
19 | this.sizes.on('switchdevice', (device) => {
20 | this.device = device
21 | })
22 |
23 | this.world.on('worldready', () => {
24 | this.setAssets()
25 | this.playIntro()
26 | })
27 | }
28 |
29 | setAssets() {
30 | convert(document.querySelector('.intro-text'))
31 | convert(document.querySelector('.hero-main-title'))
32 | convert(document.querySelector('.hero-main-description'))
33 | convert(document.querySelector('.hero-second-subheading'))
34 | convert(document.querySelector('.second-sub'))
35 | this.group = this.experience.world.bike.group
36 | this.actualBike = this.experience.world.bike.actualBike
37 | this.bikeChildren = this.experience.world.bike.bikeChildren
38 | }
39 |
40 | firstIntro() {
41 | return new Promise((resolve) => {
42 | this.timeline = new GSAP.timeline()
43 | this.timeline.set('.animatethis', { y: 0, yPercent: 100 })
44 | this.timeline.to('.preloader', {
45 | opacity: 0,
46 | delay: 1,
47 | onComplete: () => {
48 | document.querySelector('.preloader').classList.add('hidden')
49 | }
50 | })
51 |
52 | if (this.device === 'desktop') {
53 | this.timeline.to(this.actualBike.scale, {
54 | x: 0.5,
55 | y: 0.5,
56 | z: 0.5,
57 | ease: 'back.out(1.5)',
58 | duration: 0.7
59 | })
60 | } else {
61 | this.timeline.to(this.actualBike.scale, {
62 | x: 0.175,
63 | y: 0.175,
64 | z: 0.175,
65 | ease: 'back.out(2.5)',
66 | duration: 0.7
67 | }, 'same')
68 | .to(this.camera.perspectiveCamera.position, {
69 | y: 0.1
70 | }, 'same')
71 | }
72 |
73 | this.timeline.to('.intro-text .animatethis', {
74 | yPercent: 0,
75 | stagger: 0.04,
76 | ease: 'back.out(1.5)',
77 | onComplete: resolve
78 | })
79 | .to('.arrow-svg-wrapper', {
80 | opacity: 1
81 | }, 'fadein')
82 | .to('.navbar', {
83 | opacity: 1,
84 | onComplete: resolve
85 | }, 'fadein')
86 | })
87 | }
88 |
89 | secondIntro() {
90 | return new Promise((resolve) => {
91 | this.secondTimeline = new GSAP.timeline()
92 |
93 | this.secondTimeline.to('.intro-text .animatethis', {
94 | yPercent: 100,
95 | stagger: 0.04,
96 | ease: 'back.in(1.5)'
97 | }, 'fadeout')
98 | .to('.arrow-svg-wrapper', {
99 | opacity: 0
100 | }, 'fadeout')
101 |
102 | if (this.device === 'desktop') {
103 | this.secondTimeline.to(this.actualBike.scale, {
104 | x: 0.65,
105 | y: 0.65,
106 | z: 0.65,
107 | stagger: 2,
108 | ease: 'power1.out'
109 | }, 'introtext')
110 | } else {
111 | this.secondTimeline.to(this.actualBike.scale, {
112 | x: 0.50,
113 | y: 0.50,
114 | z: 0.50,
115 | stagger: 1,
116 | ease: 'power1.out'
117 | }, 'introtext')
118 | .to(this.actualBike.rotation, {
119 | y: -Math.PI * 0.5
120 | }, 'introtext')
121 | .to(this.camera.perspectiveCamera.position, {
122 | y: 0.4
123 | }, 'introtext')
124 | }
125 |
126 | this.secondTimeline.to(this.bikeChildren.boxface1.rotation, {
127 | x: 0,
128 | y: 0,
129 | z: -Math.PI,
130 | duration: 2
131 | }, 'introtext')
132 | .to(this.bikeChildren.boxface2.rotation, {
133 | x: -Math.PI,
134 | y: 0,
135 | z: 0,
136 | duration: 2
137 | }, 'introtext')
138 | .to(this.bikeChildren.boxface3.rotation, {
139 | x: Math.PI,
140 | y: 0,
141 | z: 0,
142 | duration: 2
143 | }, 'introtext')
144 | .to(this.bikeChildren.boxface4.rotation, {
145 | x: 0,
146 | y: 0,
147 | z: Math.PI,
148 | duration: 2
149 | }, 'introtext')
150 | .to('.hero-main-title .animatethis', {
151 | yPercent: 0,
152 | stagger: 0.02,
153 | ease: 'back.out(1.5)'
154 | }, 'introtext')
155 | .to('.hero-main-description .animatethis', {
156 | yPercent: 0,
157 | stagger: 0.02,
158 | ease: 'back.out(1.5)'
159 | }, 'introtext')
160 | .to('.first-sub .animatethis', {
161 | yPercent: 0,
162 | stagger: 0.02,
163 | ease: 'back.out(1.5)'
164 | }, 'introtext')
165 | .to('.second-sub .animatethis', {
166 | yPercent: 0,
167 | stagger: 0.02,
168 | ease: 'back.out(1.5)'
169 | }, 'introtext')
170 | .to('.arrow-svg-wrapper', {
171 | opacity: 1,
172 | onComplete: resolve
173 | })
174 | })
175 | }
176 |
177 | onScroll(e) {
178 | if(e.deltaY > 0) {
179 | this.removeEventListeners()
180 | this.playSecondIntro()
181 | }
182 | }
183 |
184 | onTouch(e) {
185 | this.initialY = e.touches[0].clientY
186 | }
187 |
188 | onTouchMove(e) {
189 | let currentY = e.touches[0].clientY
190 | let difference = this.initialY - currentY
191 | if(difference > 0) {
192 | console.log('swipped up')
193 | this.removeEventListeners()
194 | this.playSecondIntro()
195 | }
196 | this.initialY = null
197 | }
198 |
199 | removeEventListeners() {
200 | window.removeEventListener('wheel', this.scrollOnceEvent)
201 | window.removeEventListener('touchstart', this.touchStart)
202 | window.removeEventListener('touchmove', this.touchMove)
203 | }
204 |
205 | async playIntro() {
206 | await this.firstIntro()
207 | this.moveFlag = true
208 |
209 | // Mouse
210 | this.scrollOnceEvent = this.onScroll.bind(this)
211 |
212 | // Touch
213 | this.touchStart = this.onTouch.bind(this)
214 | this.touchMove = this.onTouchMove.bind(this)
215 |
216 | window.addEventListener('wheel', this.scrollOnceEvent)
217 | window.addEventListener('touchstart', this.touchStart)
218 | window.addEventListener('touchmove', this.touchMove)
219 | }
220 |
221 | async playSecondIntro() {
222 | this.moveFlag = false
223 | this.scaleFlag = true
224 |
225 | await this.secondIntro()
226 | this.scaleFlag = false
227 | this.emit('enableControls')
228 | }
229 |
230 | move() {
231 | if(this.device === 'desktop') {
232 | this.group.position.set(0, 0, 0) // same values as provided to gsap
233 | } else {
234 | this.group.position.set(0, 0, 0) // same values as provided to gsap
235 | }
236 | }
237 |
238 | scale() {
239 | if(this.device === 'desktop') {
240 | this.group.scale.set(1, 1, 1) // same values as provided to gsap
241 | } else {
242 | this.group.scale.set(1, 1, 1) // same values as provided to gsap
243 | }
244 | }
245 |
246 | update() {
247 | if(this.moveFlag) {
248 | this.move()
249 | }
250 |
251 | if(this.scaleFlag) {
252 | this.scale()
253 | }
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/Experience/Renderer.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import Experience from "./Experience.js"
3 |
4 | export default class Renderer {
5 | constructor() {
6 | this.experience = new Experience()
7 | this.sizes = this.experience.sizes
8 | this.scene = this.experience.scene
9 | this.canvas = this.experience.canvas
10 | this.camera = this.experience.camera
11 |
12 | this.setRenderer()
13 | }
14 |
15 | setRenderer() {
16 | this.renderer = new THREE.WebGLRenderer({
17 | canvas: this.canvas,
18 | antialias: true,
19 | })
20 |
21 | this.renderer.physicallyCorrectLights = true
22 | this.renderer.outputEncoding = THREE.sRGBEncoding
23 | this.renderer.toneMapping = THREE.ACESFilmicToneMapping
24 | this.renderer.toneMappingExposure = 1.75
25 | this.renderer.shadowMap.enabled = true
26 | this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
27 | this.renderer.setSize(this.sizes.width, this.sizes.height)
28 | this.renderer.setPixelRatio(this.sizes.pixelRatio)
29 | }
30 |
31 | resize() {
32 | this.renderer.setSize(this.sizes.width, this.sizes.height)
33 | this.renderer.setPixelRatio(this.sizes.pixelRatio)
34 | }
35 |
36 | update() {
37 | this.renderer.setViewport(0, 0, this.sizes.width, this.sizes.height)
38 | this.renderer.render(this.scene, this.camera.perspectiveCamera)
39 |
40 | // // Second Screen
41 | // this.renderer.setScissorTest(true)
42 | // this.renderer.setViewport(
43 | // this.sizes.width - this.sizes.width / 2,
44 | // this.sizes.height - this.sizes.height / 2,
45 | // this.sizes.width / 2,
46 | // this.sizes.height / 2
47 | // )
48 |
49 | // this.renderer.setScissor(
50 | // this.sizes.width - this.sizes.width / 2,
51 | // this.sizes.height - this.sizes.height / 2,
52 | // this.sizes.width / 2,
53 | // this.sizes.height / 2
54 | // )
55 |
56 | // this.renderer.render(this.scene, this.camera.orthographicCamera)
57 |
58 | // this.renderer.setScissorTest(false)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Experience/Theme.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events'
2 |
3 | export default class Theme extends EventEmitter {
4 | constructor() {
5 | super()
6 |
7 | this.theme = 'dark'
8 |
9 | this.toggleButton = document.querySelector('.toggle-button')
10 | this.toggleCircle = document.querySelector('.toggle-circle')
11 |
12 | this.setEventListeners()
13 | }
14 |
15 | setEventListeners() {
16 | this.toggleButton.addEventListener('click', () => {
17 | this.toggleCircle.classList.toggle('slide')
18 | this.theme = this.theme === 'dark' ? 'light' : 'dark'
19 | document.body.classList.toggle('dark-theme')
20 | document.body.classList.toggle('light-theme')
21 |
22 | this.emit('switch', this.theme)
23 | })
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Experience/Utils/Debug.js:
--------------------------------------------------------------------------------
1 | import * as dat from 'lil-gui'
2 |
3 | export default class Debug
4 | {
5 | constructor() {
6 | this.active = window.location.hash === '#debug'
7 |
8 | if(this.active) {
9 | this.ui = new dat.GUI({ container: document.querySelector('.debug') })
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Experience/Utils/Resources.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"
3 | import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"
4 | import { EventEmitter } from 'events'
5 | import Experience from "../Experience.js"
6 |
7 | export default class Resources extends EventEmitter {
8 | constructor(assets) {
9 | super()
10 | this.experience = new Experience()
11 | this.renderer = this.experience.renderer
12 |
13 | this.assets = assets
14 |
15 | this.items = {}
16 | this.queue = this.assets.length
17 | this.loaded = 0
18 |
19 | this.setLoaders()
20 | this.startLoading()
21 | }
22 |
23 | setLoaders() {
24 | this.loaders = {}
25 | this.loaders.gltfLoader = new GLTFLoader()
26 | this.loaders.dracoLoader = new DRACOLoader()
27 | this.loaders.dracoLoader.setDecoderPath("/draco/")
28 | this.loaders.gltfLoader.setDRACOLoader(this.loaders.dracoLoader)
29 | this.loaders.cubeTextureLoader = new THREE.CubeTextureLoader()
30 | }
31 |
32 | startLoading() {
33 | for(const asset of this.assets) {
34 | if (asset.type === 'glbModel') {
35 | this.loaders.gltfLoader.load(asset.path, (file) => {
36 | this.singleAssetLoaded(asset, file)
37 | })
38 | } else if (asset.type === 'cubeTexture') {
39 | this.loaders.cubeTextureLoader.load(asset.path, (file => {
40 | this.singleAssetLoaded(asset, file)
41 | }))
42 | // } else if (asset.type === 'videoTexture') {
43 | // this.video = {}
44 | // this.videoTexture = {}
45 |
46 | // this.video[asset.name] = document.createElement("video")
47 | // this.video[asset.name].src = asset.path
48 | // this.video[asset.name].playInline = true
49 | // this.video[asset.name].muted = true
50 | // this.video[asset.name].autoplay = true
51 | // this.video[asset.name].loop = true
52 | // this.video[asset.name].play()
53 |
54 | // this.videoTexture[asset.name] = new THREE.VideoTexture(
55 | // this.video[asset.name]
56 | // )
57 | // this.videoTexture[asset.name].flipY = true
58 | // this.videoTexture[asset.name].minFilter = THREE.NearestFilter
59 | // this.videoTexture[asset.name].mageFilter = THREE.NearestFilter
60 | // this.videoTexture[asset.name].generateMipmaps = false
61 | // this.videoTexture[asset.name].encoding = THREE.sRGBEncoding
62 |
63 | // this.singleAssetLoaded(asset, this.videoTexture[asset.name])
64 | }
65 | }
66 | }
67 |
68 | singleAssetLoaded(asset, file) {
69 | this.items[asset.name] = file
70 | this.loaded++
71 |
72 | if (this.loaded === this.queue) {
73 | this.emit('ready')
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Experience/Utils/Sizes.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events'
2 |
3 | export default class Sizes extends EventEmitter {
4 | constructor() {
5 | super()
6 | this.width = window.innerWidth
7 | this.height = window.innerHeight
8 | this.aspect = this.width / this.height
9 | this.pixelRatio = Math.min(window.devicePixelRatio, 2)
10 | this.frustum = 5
11 | if(this.width < 968) {
12 | this.device = 'mobile'
13 | } else {
14 | this.device = 'desktop'
15 | }
16 |
17 | window.addEventListener('resize', () => {
18 | this.width = window.innerWidth
19 | this.height = window.innerHeight
20 | this.aspect = this.width / this.height
21 | this.pixelRatio = Math.min(window.devicePixelRatio, 2)
22 | this.emit('resize')
23 |
24 | if(this.width < 968 && this.device !== 'mobile') {
25 | this.device = 'mobile'
26 | this.emit('switchdevice', this.device)
27 | } else if (this.width >= 968 && this.device !== 'desktop') {
28 | this.device = 'desktop'
29 | this.emit('switchdevice', this.device)
30 | }
31 | })
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Experience/Utils/Time.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events'
2 |
3 | export default class Time extends EventEmitter {
4 | constructor() {
5 | super()
6 | this.start = Date.now()
7 | this.current = this.start
8 | this.elapsed = 0
9 | this.delta = 16
10 |
11 | this.update()
12 | }
13 |
14 | update() {
15 | const currentTime = Date.now()
16 | this.delta = currentTime - this.current
17 | this.current = currentTime
18 | this.elapsed = this.current - this.start
19 |
20 | this.emit('update')
21 | window.requestAnimationFrame(() => this.update())
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Experience/Utils/assets.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | name: 'bike',
4 | type: 'glbModel',
5 | path: '/models/bike-model.glb'
6 | },
7 | {
8 | name: 'environmentMapTexture',
9 | type: 'cubeTexture',
10 | path: [
11 | 'textures/environmentMap/px.png',
12 | 'textures/environmentMap/nx.png',
13 | 'textures/environmentMap/py.png',
14 | 'textures/environmentMap/ny.png',
15 | 'textures/environmentMap/pz.png',
16 | 'textures/environmentMap/nz.png'
17 | ]
18 | }
19 | // {
20 | // name: 'screen',
21 | // type: 'videoTexture',
22 | // path: '/textures/kda.mp4'
23 | // }
24 | ]
25 |
--------------------------------------------------------------------------------
/Experience/Utils/convertDivsToSpans.js:
--------------------------------------------------------------------------------
1 | export default function (element) {
2 | element.style.overflow = 'hidden'
3 | element.innerHTML = element.innerText.split('').map((char) => {
4 | if(char === ' ') {
5 | return `${char}`
6 | }
7 | return `${char}`
8 | })
9 | .join('')
10 |
11 | return element
12 | }
13 |
--------------------------------------------------------------------------------
/Experience/World/Bike.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import { DirectionalLightHelper } from 'three'
3 | import GSAP from 'gsap'
4 | import Experience from '../Experience.js'
5 |
6 | export default class Bike {
7 | constructor() {
8 | this.experience = new Experience()
9 | this.scene = this.experience.scene
10 | this.resources = this.experience.resources
11 | this.debug = this.experience.debug
12 | this.bike = this.resources.items.bike
13 | this.actualBike = this.bike.scene
14 | this.bikeChildren = {}
15 |
16 | this.lerp = {
17 | current: 0,
18 | target: 0,
19 | ease: 0.1
20 | }
21 |
22 | // Debug
23 | if(this.debug.active) {
24 | this.debugFolder = this.debug.ui.addFolder('bike')
25 | this.obj = {
26 | colorObj: {r:0 , g: 0, b: 0}
27 | }
28 | }
29 |
30 | this.setBikeModel()
31 | this.onMouseMove()
32 | this.setBikeGroup()
33 | }
34 |
35 | setBikeModel() {
36 | this.actualBike.scale.set(0, 0, 0)
37 |
38 | this.actualBike.traverse((child) => {
39 | if(child instanceof THREE.Mesh) {
40 | // Shadows
41 | child.castShadow = true
42 | child.receiveShadow = true
43 |
44 | // Material
45 | this.bikeMaterial = new THREE.MeshStandardMaterial({
46 | color: 0xd7d8d9,
47 | envMapIntensity: 0.1
48 | })
49 | child.material = this.bikeMaterial
50 | }
51 |
52 | if(child.name === 'BoxFace1') {
53 | child.material.side = THREE.BackSide
54 | child.material.color.set(0x111111)
55 | }
56 | if(child.name === 'BoxFace2') {
57 | child.material.side = THREE.BackSide
58 | child.material.color.set(0x111111)
59 | }
60 | if(child.name === 'BoxFace3') {
61 | child.material.side = THREE.BackSide
62 | child.material.color.set(0x111111)
63 | }
64 | if(child.name === 'BoxFace4') {
65 | child.material.side = THREE.BackSide
66 | child.material.color.set(0x111111)
67 | }
68 |
69 | if(child.name === 'BrakeF') {
70 | child.material.color.set(0x050505)
71 | }
72 |
73 | if(child.name === 'BrakeB') {
74 | child.material.color.set(0x050505)
75 | }
76 |
77 | if(child.name === 'BrakePadsF') {
78 | child.material.color.set(0x050505)
79 | }
80 |
81 | if(child.name === 'BrakePadsB') {
82 | child.material.color.set(0x050505)
83 | }
84 |
85 | if(child.name === 'BrakeCableF') {
86 | child.material.color.set(0xff8c00)
87 | }
88 |
89 | if(child.name === 'BrakeCableB') {
90 | child.material.color.set(0xff8c00)
91 | }
92 |
93 | if(child.name === 'BrakeDetailF') {
94 | child.material.color.set(0xff8c00)
95 | }
96 |
97 | if(child.name === 'BrakeDetailB') {
98 | child.material.color.set(0xff8c00)
99 | }
100 |
101 | if(child.name === 'Frame') {
102 | child.material.metalness = 0.9
103 | child.material.roughness = 0
104 | }
105 |
106 | if(child.name === 'Chain1') {
107 | child.material.color.set(0x050505)
108 | child.material.metalness = 0.9
109 | child.material.roughness = 0
110 | }
111 |
112 | if(child.name === 'Chain2') {
113 | child.material.color.set(0x050505)
114 | child.material.metalness = 0.9
115 | child.material.roughness = 0
116 | }
117 |
118 | if(child.name === 'ChainringsCover') {
119 | child.material.color.set(0x050505)
120 | child.material.metalness = 0.9
121 | child.material.roughness = 0
122 | }
123 |
124 | if(child.name === 'CrankArm') {
125 | child.material.metalness = 0.9
126 | child.material.roughness = 0
127 | }
128 |
129 | if(child.name === 'Cassette') {
130 | child.material.metalness = 0.9
131 | child.material.roughness = 0
132 | }
133 |
134 | if(child.name === 'PedalL') {
135 | child.material.color.set(0x050505)
136 | }
137 |
138 | if(child.name === 'PedalR') {
139 | child.material.color.set(0x050505)
140 | }
141 |
142 | if(child.name === 'PedalGripL') {
143 | child.material.color.set(0x050505)
144 | }
145 |
146 | if(child.name === 'PedalGripR') {
147 | child.material.color.set(0x050505)
148 | }
149 |
150 | if(child.name === 'CockpitStem') {
151 | child.material.color.set(0x050505)
152 | child.material.metalness = 0.5
153 | child.material.roughness = 0
154 | }
155 |
156 | if(child.name === 'CockpitHandlebar') {
157 | child.material.color.set(0x050505)
158 | child.material.roughness = 1
159 | }
160 |
161 | if(child.name === 'TireF') {
162 | child.material.color.set(0x050505)
163 | child.material.roughness = 1
164 | }
165 |
166 | if(child.name === 'TireB') {
167 | child.material.color.set(0x050505)
168 | child.material.roughness = 1
169 | }
170 |
171 | if(child.name === 'RimF') {
172 | child.material.metalness = 0.5
173 | child.material.roughness = 0
174 | }
175 |
176 | if(child.name === 'RimB') {
177 | child.material.metalness = 0.5
178 | child.material.roughness = 0
179 | }
180 |
181 | if(child.name === 'RimInnerF') {
182 | child.material.color.set(0xff8c00)
183 | child.material.roughness = 0
184 | }
185 |
186 | if(child.name === 'RimInnerB') {
187 | child.material.color.set(0xff8c00)
188 | child.material.roughness = 0
189 | }
190 |
191 | if(child.name === 'SpokesF') {
192 | child.material.metalness = 0.9
193 | child.material.roughness = 0
194 | }
195 |
196 | if(child.name === 'SpokesB') {
197 | child.material.metalness = 0.9
198 | child.material.roughness = 0
199 | }
200 |
201 | if(child.name === 'FasteningF') {
202 | child.material.color.set(0x050505)
203 | child.material.metalness = 0.5
204 | child.material.roughness = 0
205 | }
206 |
207 | if(child.name === 'FasteningB') {
208 | child.material.color.set(0x050505)
209 | child.material.metalness = 0.5
210 | child.material.roughness = 0
211 | }
212 |
213 | if(child.name === 'HubF') {
214 | child.material.color.set(0x050505)
215 | child.material.metalness = 0.5
216 | child.material.roughness = 0
217 | }
218 |
219 | if(child.name === 'HubB') {
220 | child.material.color.set(0x050505)
221 | child.material.metalness = 0.5
222 | child.material.roughness = 0
223 | }
224 |
225 | if(child.name === 'Seat') {
226 | child.material.color.set(0x050505)
227 | child.material.roughness = 1
228 | }
229 |
230 | this.bikeChildren[child.name.toLowerCase()] = child
231 | })
232 | }
233 |
234 | switchTheme(theme) {
235 | if(theme === 'dark') {
236 | this.toDarkTimeline = new GSAP.timeline()
237 |
238 | this.actualBike.traverse((child) => {
239 | if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial) {
240 | this.toDarkTimeline.to(child.material, {
241 | envMapIntensity: 0.1
242 | }, 'same')
243 | }
244 |
245 | if(child.name === 'BoxFace1') {
246 | child.material.color.set(0x111111)
247 | } else if (child.name === 'BoxFace2') {
248 | child.material.color.set(0x111111)
249 | } else if (child.name === 'BoxFace3') {
250 | child.material.color.set(0x111111)
251 | } else if (child.name === 'BoxFace4') {
252 | child.material.color.set(0x111111)
253 | }
254 | })
255 | } else {
256 | this.toLightTimeline = new GSAP.timeline()
257 |
258 | this.actualBike.traverse((child) => {
259 | if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial) {
260 | this.toLightTimeline.to(child.material, {
261 | envMapIntensity: 1
262 | }, 'same')
263 | }
264 |
265 | if(child.name === 'BoxFace1') {
266 | child.material.color.set(0xd7d8d9)
267 | } else if (child.name === 'BoxFace2') {
268 | child.material.color.set(0xd7d8d9)
269 | } else if (child.name === 'BoxFace3') {
270 | child.material.color.set(0xd7d8d9)
271 | } else if (child.name === 'BoxFace4') {
272 | child.material.color.set(0xd7d8d9)
273 | }
274 | })
275 | }
276 | }
277 |
278 | onMouseMove() {
279 | window.addEventListener('mousemove', (e) => {
280 | this.rotation = ((e.clientX - window.innerWidth / 2) * 2) / window.innerWidth // makes the position of the cursor from -1 to 1
281 | this.lerp.target = this.rotation * 0.3
282 | })
283 | }
284 |
285 | setBikeGroup() {
286 | // New group so we can rotate the bike with GSAP without intefering with our mouse rotation lerping
287 | // Like a spinning plateform that can spin independetly from others
288 | this.group = new THREE.Group()
289 | this.group.add(this.actualBike)
290 | this.scene.add(this.group)
291 | }
292 |
293 | resize() {}
294 |
295 | update() {
296 | this.lerp.current = GSAP.utils.interpolate(
297 | this.lerp.current,
298 | this.lerp.target,
299 | this.lerp.ease
300 | )
301 |
302 | this.group.rotation.y = this.lerp.current
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/Experience/World/Environment.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import { DirectionalLightHelper } from 'three'
3 | import GSAP from 'gsap'
4 | import Experience from '../Experience.js'
5 |
6 | export default class Environment {
7 | constructor() {
8 | this.experience = new Experience()
9 | this.scene = this.experience.scene
10 | this.resources = this.experience.resources
11 | this.debug = this.experience.debug
12 |
13 | // Debug
14 | if(this.debug.active) {
15 | this.debugFolder = this.debug.ui.addFolder('environment')
16 | this.obj = {
17 | colorObj: {r:0 , g: 0, b: 0}
18 | }
19 | }
20 |
21 | // Setup
22 | this.setBackground()
23 | this.setLights()
24 | this.setEnvironmentMap()
25 | }
26 |
27 | setBackground() {
28 | this.bgColor = 0x222222
29 | this.scene.background = new THREE.Color(this.bgColor)
30 | this.scene.fog = new THREE.Fog(this.bgColor, 5, 20)
31 | }
32 |
33 | setLights() {
34 | // Sun Light
35 | this.sunLight = new THREE.DirectionalLight("#222222", 0.2)
36 | this.sunLight.castShadow = true
37 | this.sunLight.shadow.camera.far = 20
38 | this.sunLight.shadow.mapSize.set(1024, 1024)
39 | this.sunLight.shadow.normalBias = 0.05
40 | this.sunLight.position.set(3, 7, 3)
41 | this.scene.add(this.sunLight)
42 |
43 | // // Sun Light Helper
44 | // this.sunLightHelper = new THREE.DirectionalLightHelper(this.sunLight, 5)
45 | // this.scene.add(this.sunLightHelper)
46 |
47 | // Ambient Light
48 | this.ambientLight = new THREE.AmbientLight('#222222', 0.2)
49 | this.scene.add(this.ambientLight)
50 |
51 | // Directional Light
52 | const color = 0xffffff
53 | const intensity = 1
54 |
55 | const targetObject = new THREE.Object3D();
56 | targetObject.position.set(0, 0.5, 0)
57 | this.scene.add(targetObject);
58 |
59 | this.directionalLight = new THREE.DirectionalLight( color, intensity )
60 | this.directionalLight.position.set( 0, 1.5, 3 )
61 | this.directionalLight.target = targetObject;
62 | this.scene.add( this.directionalLight )
63 |
64 | this.directionalLight2 = new THREE.DirectionalLight( color, intensity )
65 | this.directionalLight2.position.set( -2, 2, 3 )
66 | this.directionalLight2.target = targetObject;
67 | this.scene.add( this.directionalLight2 )
68 |
69 | this.directionalLight3 = new THREE.DirectionalLight( color, intensity )
70 | this.directionalLight3.position.set( 2, 2, 3 )
71 | this.directionalLight3.target = targetObject;
72 | this.directionalLight3.castShadow = true
73 | this.directionalLight3.shadow.camera.far = 20
74 | this.directionalLight3.shadow.mapSize.set(1024, 1024)
75 | this.directionalLight3.shadow.normalBias = 0.05
76 | this.scene.add( this.directionalLight3 )
77 |
78 | // // Directional Light Helpers
79 | // this.directionalLightHelper = new DirectionalLightHelper( this.directionalLight )
80 | // this.directionalLight.add( this.directionalLightHelper )
81 | // this.directionalLightHelper2 = new DirectionalLightHelper( this.directionalLight2 )
82 | // this.directionalLight2.add( this.directionalLightHelper2 )
83 | // this.directionalLightHelper3 = new DirectionalLightHelper( this.directionalLight3 )
84 | // this.directionalLight3.add( this.directionalLightHelper3 )
85 |
86 | // Debug
87 | if(this.debug.active) {
88 | // All Lights
89 | this.debugFolder
90 | .addColor(this.obj, 'colorObj')
91 | .name('lightsColor')
92 | .onChange(() => {
93 | this.sunLight.color.copy(this.obj.colorObj)
94 | this.ambientLight.color.copy(this.obj.colorObj)
95 | this.directionalLight.color.copy(this.obj.colorObj)
96 | this.directionalLight2.color.copy(this.obj.colorObj)
97 | this.directionalLight3.color.copy(this.obj.colorObj)
98 | })
99 |
100 | // Sun Light
101 | this.debugFolder
102 | .add(this.sunLight, 'intensity')
103 | .name('sunLightIntensity')
104 | .min(0)
105 | .max(10)
106 | .step(0.001)
107 |
108 | this.debugFolder
109 | .add(this.sunLight.position, 'x')
110 | .name('sunLightX')
111 | .min(- 5)
112 | .max(5)
113 | .step(0.001)
114 |
115 | this.debugFolder
116 | .add(this.sunLight.position, 'y')
117 | .name('sunLightY')
118 | .min(- 5)
119 | .max(5)
120 | .step(0.001)
121 |
122 | this.debugFolder
123 | .add(this.sunLight.position, 'z')
124 | .name('sunLightZ')
125 | .min(- 5)
126 | .max(5)
127 | .step(0.001)
128 |
129 | this.debugFolder
130 | .add(this.sunLight.rotation, 'x')
131 | .name('sunRotationX')
132 | .min(- 5)
133 | .max(5)
134 | .step(0.001)
135 |
136 | this.debugFolder
137 | .add(this.sunLight.rotation, 'y')
138 | .name('sunRotationY')
139 | .min(- 5)
140 | .max(5)
141 | .step(0.001)
142 |
143 | this.debugFolder
144 | .add(this.sunLight.rotation, 'z')
145 | .name('sunRotationZ')
146 | .min(- 5)
147 | .max(5)
148 | .step(0.001)
149 |
150 | // Ambient Light
151 | this.debugFolder
152 | .add(this.ambientLight, 'intensity')
153 | .name('ambLightIntensity')
154 | .min(0)
155 | .max(10)
156 | .step(0.001)
157 |
158 | // Directional Lights
159 | // 1
160 | this.debugFolder
161 | .add(this.directionalLight, 'intensity')
162 | .name('directionalLightIntensity')
163 | .min(0)
164 | .max(10)
165 | .step(0.001)
166 |
167 | this.debugFolder
168 | .add(this.directionalLight.position, 'x')
169 | .name('directionalLightX')
170 | .min(- 10)
171 | .max(10)
172 | .step(0.001)
173 |
174 | this.debugFolder
175 | .add(this.directionalLight.position, 'y')
176 | .name('directionalLightY')
177 | .min(- 10)
178 | .max(10)
179 | .step(0.001)
180 |
181 | this.debugFolder
182 | .add(this.directionalLight.position, 'z')
183 | .name('directionalLightZ')
184 | .min(- 10)
185 | .max(10)
186 | .step(0.001)
187 |
188 | this.debugFolder
189 | .add(this.directionalLight.rotation, 'x')
190 | .name('directionalLightRotX')
191 | .min(- 10)
192 | .max(10)
193 | .step(0.001)
194 |
195 | this.debugFolder
196 | .add(this.directionalLight.rotation, 'y')
197 | .name('directionalLightRotY')
198 | .min(- 10)
199 | .max(10)
200 | .step(0.001)
201 |
202 | this.debugFolder
203 | .add(this.directionalLight.rotation, 'z')
204 | .name('directionalLightRotZ')
205 | .min(- 10)
206 | .max(10)
207 | .step(0.001)
208 |
209 | // 2
210 | this.debugFolder
211 | .add(this.directionalLight2, 'intensity')
212 | .name('directionalLight2Intensity')
213 | .min(0)
214 | .max(10)
215 | .step(0.001)
216 |
217 | this.debugFolder
218 | .add(this.directionalLight2.position, 'x')
219 | .name('directionalLight2X')
220 | .min(- 10)
221 | .max(10)
222 | .step(0.001)
223 |
224 | this.debugFolder
225 | .add(this.directionalLight2.position, 'y')
226 | .name('directionalLight2Y')
227 | .min(- 10)
228 | .max(10)
229 | .step(0.001)
230 |
231 | this.debugFolder
232 | .add(this.directionalLight2.position, 'z')
233 | .name('directionalLight2Z')
234 | .min(- 10)
235 | .max(10)
236 | .step(0.001)
237 |
238 | this.debugFolder
239 | .add(this.directionalLight2.rotation, 'x')
240 | .name('directionalLight2RotX')
241 | .min(- 10)
242 | .max(10)
243 | .step(0.001)
244 |
245 | this.debugFolder
246 | .add(this.directionalLight2.rotation, 'y')
247 | .name('directionalLight2RotY')
248 | .min(- 10)
249 | .max(10)
250 | .step(0.001)
251 |
252 | this.debugFolder
253 | .add(this.directionalLight2.rotation, 'z')
254 | .name('directionalLight2RotZ')
255 | .min(- 10)
256 | .max(10)
257 | .step(0.001)
258 |
259 | // 3
260 | this.debugFolder
261 | .add(this.directionalLight3, 'intensity')
262 | .name('directionalLight3Intensity')
263 | .min(0)
264 | .max(10)
265 | .step(0.001)
266 |
267 | this.debugFolder
268 | .add(this.directionalLight3.position, 'x')
269 | .name('directionalLight3X')
270 | .min(- 10)
271 | .max(10)
272 | .step(0.001)
273 |
274 | this.debugFolder
275 | .add(this.directionalLight3.position, 'y')
276 | .name('directionalLight3Y')
277 | .min(- 10)
278 | .max(10)
279 | .step(0.001)
280 |
281 | this.debugFolder
282 | .add(this.directionalLight3.position, 'z')
283 | .name('directionalLight3Z')
284 | .min(- 10)
285 | .max(10)
286 | .step(0.001)
287 |
288 | this.debugFolder
289 | .add(this.directionalLight3.rotation, 'x')
290 | .name('directionalLight3RotX')
291 | .min(- 10)
292 | .max(10)
293 | .step(0.001)
294 |
295 | this.debugFolder
296 | .add(this.directionalLight3.rotation, 'y')
297 | .name('directionalLight3RotY')
298 | .min(- 10)
299 | .max(10)
300 | .step(0.001)
301 |
302 | this.debugFolder
303 | .add(this.directionalLight3.rotation, 'z')
304 | .name('directionalLight3RotZ')
305 | .min(- 10)
306 | .max(10)
307 | .step(0.001)
308 | }
309 | }
310 |
311 | setEnvironmentMap() {
312 | this.environmentMap = {}
313 | this.environmentMap.intensity = 1
314 | this.environmentMap.texture = this.resources.items.environmentMapTexture
315 | this.environmentMap.texture.encoding = THREE.sRGBEncoding
316 |
317 | this.scene.environment = this.environmentMap.texture
318 |
319 | this.environmentMap.updateMaterials = () => {
320 | this.scene.traverse((child) => {
321 | if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial)
322 | {
323 | child.material.envMap = this.environmentMap.texture
324 | child.material.envMapIntensity = this.environmentMap.intensity
325 | child.material.needsUpdate = true
326 | }
327 | })
328 | }
329 | this.environmentMap.updateMaterials()
330 | }
331 |
332 | switchTheme(theme) {
333 | if(theme === 'dark') {
334 | this.toDarkTimeline = new GSAP.timeline()
335 | .to(this.scene.background, {
336 | r: 34 / 255,
337 | g: 34 / 255,
338 | b: 34 / 255
339 | }, 'same')
340 | .to(this.scene.fog.color, {
341 | r: 34 / 255,
342 | g: 34 / 255,
343 | b: 34 / 255
344 | }, 'same')
345 | .to(this.sunLight.color, {
346 | r: 34 / 255,
347 | g: 34 / 255,
348 | b: 34 / 255
349 | }, 'same')
350 | .to(this.ambientLight.color, {
351 | r: 34 / 255,
352 | g: 34 / 255,
353 | b: 34 / 255
354 | }, 'same')
355 | .to(this.sunLight, {
356 | intensity: 0.2
357 | }, 'same')
358 | .to(this.ambientLight, {
359 | intensity: 0.2
360 | }, 'same')
361 | .to(this.directionalLight, {
362 | intensity: 1
363 | }, 'same')
364 | .to(this.directionalLight2, {
365 | intensity: 1
366 | }, 'same')
367 | .to(this.directionalLight3, {
368 | intensity: 1
369 | }, 'same')
370 | } else {
371 | this.toLightTimeline = new GSAP.timeline()
372 | .to(this.scene.background, {
373 | r: 215 / 255,
374 | g: 216 / 255,
375 | b: 217 / 255
376 | }, 'same')
377 | .to(this.scene.fog.color, {
378 | r: 215 / 255,
379 | g: 216 / 255,
380 | b: 217 / 255
381 | }, 'same')
382 | .to(this.sunLight.color, {
383 | r: 215 / 255,
384 | g: 216 / 255,
385 | b: 217 / 255
386 | }, 'same')
387 | .to(this.ambientLight.color, {
388 | r: 215 / 255,
389 | g: 216 / 255,
390 | b: 217 / 255
391 | }, 'same')
392 | .to(this.sunLight, {
393 | intensity: 3
394 | }, 'same')
395 | .to(this.ambientLight, {
396 | intensity: 1
397 | }, 'same')
398 | .to(this.directionalLight, {
399 | intensity: 0
400 | }, 'same')
401 | .to(this.directionalLight2, {
402 | intensity: 0
403 | }, 'same')
404 | .to(this.directionalLight3, {
405 | intensity: 0
406 | }, 'same')
407 | }
408 | }
409 |
410 | resize() {}
411 |
412 | update() {}
413 | }
414 |
--------------------------------------------------------------------------------
/Experience/World/Floor.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import GSAP from 'gsap'
3 | import Experience from '../Experience.js'
4 |
5 | export default class Floor {
6 | constructor() {
7 | this.experience = new Experience()
8 | this.scene = this.experience.scene
9 |
10 | this.setFloor()
11 | }
12 |
13 | setFloor() {
14 | this.geometry = new THREE.PlaneGeometry(100, 100)
15 | this.material = new THREE.MeshStandardMaterial({
16 | color: 0x020202,
17 | side: THREE.BackSide,
18 | })
19 | this.plane = new THREE.Mesh(this.geometry, this.material)
20 | this.plane.rotation.x = Math.PI / 2
21 | this.plane.receiveShadow= true
22 |
23 | this.scene.add(this.plane)
24 | }
25 |
26 | switchTheme(theme) {
27 | if(theme === 'dark') {
28 | GSAP.to(this.plane.material.color, {
29 | r: 2 / 255,
30 | g: 2 / 255,
31 | b: 2 / 255
32 | })
33 | } else {
34 | GSAP.to(this.plane.material.color, {
35 | r: 215 / 255,
36 | g: 216 / 255,
37 | b: 217 / 255
38 | })
39 | }
40 | }
41 |
42 | resize() {}
43 |
44 | update() {}
45 | }
46 |
--------------------------------------------------------------------------------
/Experience/World/World.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import Experience from "../Experience.js"
3 |
4 | import { EventEmitter } from 'events'
5 |
6 | import Environment from './Environment.js'
7 | import Bike from './Bike.js'
8 | import Floor from './Floor.js'
9 | // import Controls from '../Controls.js'
10 |
11 |
12 | export default class World extends EventEmitter {
13 | constructor() {
14 | super()
15 | this.experience = new Experience()
16 | this.sizes = this.experience.sizes
17 | this.scene = this.experience.scene
18 | this.canvas = this.experience.canvas
19 | this.camera = this.experience.camera
20 | this.resources = this.experience.resources
21 | this.theme = this.experience.theme
22 |
23 | this.resources.on('ready', () => {
24 | this.environment = new Environment()
25 | this.floor = new Floor()
26 | this.bike = new Bike()
27 | // this.controls = new Controls()
28 | this.emit('worldready')
29 | })
30 |
31 | this.theme.on('switch', (theme) => {
32 | theme = this.theme.theme
33 | this.switchTheme(theme)
34 | })
35 | }
36 |
37 | switchTheme(theme) {
38 | if(this.environment) {
39 | this.environment.switchTheme(theme)
40 | }
41 | if(this.bike) {
42 | this.bike.switchTheme(theme)
43 | }
44 | if(this.floor) {
45 | this.floor.switchTheme(theme)
46 | }
47 | }
48 |
49 | resize() {
50 |
51 | }
52 |
53 | update() {
54 | if(this.bike) {
55 | this.bike.update()
56 | }
57 |
58 | if(this.controls) {
59 | this.controls.update()
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bike Demo Three.js
2 |
3 | This fictive project was created to showcase my current skills using Three.js and GSAP. It was inspired by the [tutorial](https://www.youtube.com/watch?v=rxTb9ys834w&t=9266s) created by Andrew Woan based on Bokoko33's [portfolio](https://bokoko33.me/).
4 |
5 | **[> View the live demo](https://bike-demo-threejs.vercel.app/)**
6 |
7 |
8 | ## Overview
9 |
10 |
11 |
12 |
13 |
14 | ### Built with
15 |
16 | - HTML, CSS, Javascript
17 | - Three.js
18 | - GSAP
19 | - ASScroll
20 | - Vite
21 |
22 | ## Authors
23 |
24 | - [Kiril Bernard Tucker](https://github.com/Kirilbt)
25 |
26 | Special thanks to Andrew Woan, Bruno Simon and everyone on the Three.Js Journey's discord who helped me.
27 |
--------------------------------------------------------------------------------
/favicon.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Pure Speed | Built to Win
63 |Limited Edition
67 |• 100
68 |This cockpit features thicker walls in key positions for extra strength and stability where it matters most. You can adjust the fit quickly, easily, and precisely, thanks to height and width adjustment without using spacers or cutting the steerer tube.
92 |Now with reinforced carbon walls for added stiffness and improved durability, this is the latest generation of aerodynamically optimised, Vitesse-developed WorldTour-level cockpits. With complete cable and wire integration, get super-clean optics and reduced drag at the same time. And with single-tool adjustable height and width, big spacer stacks are a thing of the past.
94 |Bar tape with EVA foam and elastomer gel mix for optimal comfort
96 |When only the very highest levels of performance will do, then you need a power meter. This model is seamlessly integrated in the crank and connects wirelessly with all common GPS units and SRAM’s AXS system, so you can measure your training and racing efforts in real time with pinpoint accuracy.
119 |The PD-V8000 pedals from Vitesse combine the two most desired qualities for road cyclists: stiffness and light weight. With a carbon-composite body, an extra-wide platform and a reduced stack height, you get the power transfer you’ve always dreamed about. Perfect for racers!
121 |SRAM FORCE D1 12-speed with signature flat-top design for quieter operation, increased strength and durability. Hard chrome plated inner link plates and rollers for reduced wear.
123 |Saddle with modern short-nose design and unisex ergonomics.
146 |This lightweight Vitesse-developed carbon seatpost now features a new sleeve bearing that creates a seal and reduces seatpost and seat tube friction and prevents dirt from entering. It’s lightweight and also gives you extra aero gains, thanks to its aerodynamically optimised tube profile. The new, lower mounted clamping mechanism and unique double-chamber design increase comfort, and we combine it with a Selle Italia saddle, for incredible performance.
148 |This fictive project was created to showcase my current skills using Three.js and GSAP.
170 | Last update: 23 Aug 2022