diff --git a/src/blueprints/experiences/scene-component.blueprint.ts b/src/blueprints/experiences/scene-component.blueprint.ts index e6dd66c..9239dac 100644 --- a/src/blueprints/experiences/scene-component.blueprint.ts +++ b/src/blueprints/experiences/scene-component.blueprint.ts @@ -1,5 +1,14 @@ -import { Group, Mesh, Object3D, type Object3DEventMap, Material } from "three"; +import { + Group, + Mesh, + Object3D, + type Object3DEventMap, + Material, + CatmullRomCurve3, + Vector3, +} from "three"; import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader"; +import type { gsap } from "gsap"; // BLUEPRINTS import { ExperienceBasedBlueprint } from "./experience-based.blueprint"; @@ -45,10 +54,14 @@ export abstract class SceneComponentBlueprint extends ExperienceBasedBlueprint { protected _modelScene?: Group; protected _availableMaterials: Materials = {}; - public abstract readonly navigationLimits?: { + public readonly navigationLimits?: { spherical: Exclude["limits"]; target: Exclude["limits"]; - }; + } = undefined; + public cameraPath = new CatmullRomCurve3(); + public center = new Vector3(); + + public timeline?: gsap.core.Timeline; constructor(_: SceneBlueprintProps) { super(); @@ -118,10 +131,7 @@ export abstract class SceneComponentBlueprint extends ExperienceBasedBlueprint { if (typeof callback === "function") callback(); - this._availableMaterials = { - ...this._world?.commonMaterials, - ...this._getAvailableMaterials(), - }; + this._availableMaterials = this._getAvailableMaterials(); this._initModelMaterials(); this.emit(events.CONSTRUCTED); } @@ -132,9 +142,13 @@ export abstract class SceneComponentBlueprint extends ExperienceBasedBlueprint { this.emit(events.DESTRUCTED); } - public intro(): void {} + public intro(): gsap.core.Timeline | undefined { + return this.timeline; + } - public outro(): void {} + public outro(): gsap.core.Timeline | undefined { + return this.timeline; + } public update(): void {} } diff --git a/src/common/experiences/navigation.model.ts b/src/common/experiences/navigation.model.ts index 736733a..e372878 100644 --- a/src/common/experiences/navigation.model.ts +++ b/src/common/experiences/navigation.model.ts @@ -1,10 +1,16 @@ import type { Spherical, Vector3 } from "three"; export interface NavigationView { - enabled?: boolean; - controls?: boolean; - center?: Vector3; - spherical?: { + /** Enable navigation update and controls interactions. */ + enabled: boolean; + /** Enable controls interactions. */ + controls: boolean; + /** Enable navigation limits. */ + limits: boolean; + /** Define the center of the scene. used to correctly set the limits. */ + center: Vector3; + /** Spherical space for navigation */ + spherical: { smoothed: Spherical; smoothing: number; limits: { @@ -17,7 +23,8 @@ export interface NavigationView { }; value: Spherical; }; - target?: { + /** Camera target */ + target: { value: Vector3; smoothed: Vector3; smoothing: number; @@ -28,13 +35,13 @@ export interface NavigationView { enabled: boolean; }; }; - drag?: { + drag: { delta: { x: number; y: number }; previous: { x: number; y: number }; sensitivity: number; alternative: boolean; }; - zoom?: { sensitivity: number; delta: number }; + zoom: { sensitivity: number; delta: number }; down?: (x: number, y: number) => unknown; move?: (x: number, y: number) => unknown; up?: () => unknown; diff --git a/src/config/common.config.ts b/src/config/common.config.ts index 26a9820..ac1d616 100644 --- a/src/config/common.config.ts +++ b/src/config/common.config.ts @@ -5,7 +5,7 @@ export abstract class CommonConfig { "M0,0 C0.001,0.001 0.002,0.003 0.003,0.004 0.142,0.482 0.284,0.75 0.338,0.836 0.388,0.924 0.504,1 1,1"; static readonly DEBUG = (() => { try { - return useRuntimeConfig().public.env !== "development"; + return useRuntimeConfig().public.env === "development"; } catch (_) { return false; } diff --git a/src/experiences/home/camera-aninmation.ts b/src/experiences/home/camera-aninmation.ts index 33a1dc3..fcde909 100644 --- a/src/experiences/home/camera-aninmation.ts +++ b/src/experiences/home/camera-aninmation.ts @@ -15,19 +15,17 @@ export const defaultCameraPath = new CatmullRomCurve3([ new Vector3(12, 3.7, 12), new Vector3(0, 5.5, 21), ]); -export const defaultCameraTarget = new Vector3(0, 2, 0); export class CameraAnimation extends ExperienceBasedBlueprint { protected readonly _experience = new HomeExperience(); private readonly _appSizes = this._experience.app.sizes; - private readonly _appCamera = this._experience.app.camera; private readonly _camera = this._experience.camera; + private readonly _world = this._experience.world; private readonly _navigation = this._experience.navigation; public enabled = false; public cameraPath = defaultCameraPath; - public cameraTarget = defaultCameraTarget; public progress = { current: 0, target: 0, @@ -117,13 +115,14 @@ export class CameraAnimation extends ExperienceBasedBlueprint { if (this.enabled) return; if (this._navigation?.view) this._navigation.view.controls = false; - const toPosition = this.cameraPath.getPointAt( - this.progress.current, - this.positionInCurve - ); + this.cameraPath.getPointAt(this.progress.current, this.positionInCurve); this._navigation - ?.updateCameraPosition(toPosition, this.cameraTarget) + ?.updateCameraPosition( + this.positionInCurve, + this._navigation.view.center, + Config.GSAP_ANIMATION_DURATION * 0.8 + ) .then(() => { this.enabled = true; }); @@ -137,17 +136,25 @@ export class CameraAnimation extends ExperienceBasedBlueprint { this._navigation ?.updateCameraPosition( - this.positionInCurve.setY(2), - this.cameraTarget, + this.positionInCurve.setY(this._camera?.lookAtPosition.y ?? 0), + this._navigation.view.center, Config.GSAP_ANIMATION_DURATION * 0.5 ) .then(() => { - if (this._navigation?.view) this._navigation.view.controls = true; + if (this._navigation?.view) { + this._navigation.view.controls = true; + } }); } public update(): void { - if (!this.enabled || this.timeline.isActive()) return; + if ( + !this.enabled || + this.timeline.isActive() || + this._navigation?.timeline.isActive() || + this._world?.manager?.timeline.isActive() + ) + return; this.progress.current = gsap.utils.interpolate( this.progress.current, diff --git a/src/experiences/home/camera.ts b/src/experiences/home/camera.ts index f13f245..3944e28 100644 --- a/src/experiences/home/camera.ts +++ b/src/experiences/home/camera.ts @@ -38,6 +38,7 @@ export class Camera extends ExperienceBasedBlueprint { }; public readonly initialCameraFov = 45; + public readonly initialCameraPosition = new Vector3(0, 10, 20); public readonly cameras = [ (() => this._appCameraInstance instanceof PerspectiveCamera @@ -70,6 +71,13 @@ export class Camera extends ExperienceBasedBlueprint { return this.cameras[this.currentCameraIndex]; } + public get instance(): PerspectiveCamera { + if (this._appCamera.instance instanceof PerspectiveCamera) + return this._appCamera.instance; + + return new PerspectiveCamera(); + } + public construct() { if (!Config.DEBUG && this._appDebug?.cameraHelper) { this._experience.app.scene.remove(this._appDebug?.cameraHelper); @@ -81,6 +89,7 @@ export class Camera extends ExperienceBasedBlueprint { this.correctAspect(); this._appCamera.miniCamera?.position.set(10, 8, 30); + this._appCameraInstance.position.copy(this.initialCameraPosition); this._prevCameraProps = { fov: this._appCameraInstance.fov, aspect: this._appCameraInstance.aspect, diff --git a/src/experiences/home/navigation.ts b/src/experiences/home/navigation.ts index 441548d..c62333f 100644 --- a/src/experiences/home/navigation.ts +++ b/src/experiences/home/navigation.ts @@ -16,6 +16,7 @@ import { ANIMATION_ENDED, ANIMATION_STARTED } from "~/static/event.static"; // CONFIG import { Config } from "~/config"; +import { errors } from "~/static"; /** * @original-author {@link @brunosimon} / https://github.com/brunonsimon @@ -27,30 +28,28 @@ export class Navigation extends ExperienceBasedBlueprint { private readonly _targetElement = this._experience.app.renderer.instance.domElement; - private readonly _appCamera = this._experience.app.camera; private readonly _appSizes = this._experience.app.sizes; private readonly _ui = this._experience.ui; private readonly _camera = this._experience.camera; private readonly _time = this._experience.app.time; private readonly _config = { - pixelRatio: 0, - width: 0, - height: 0, smallestSide: 0, largestSide: 0, }; private readonly _timeline = gsap.timeline({ onStart: () => { - if (this._view.spherical?.limits) - this._view.spherical.limits.enabled = false; - if (this._view.target?.limits) this._view.target.limits.enabled = false; + this._view.spherical.limits.enabled = false; + this._view.target.limits.enabled = false; + this._view.controls = false; + this.emit(ANIMATION_STARTED); }, onComplete: () => { setTimeout(() => { - if (this._view.spherical?.limits) - this._view.spherical.limits.enabled = true; - if (this._view.target?.limits) this._view.target.limits.enabled = true; + this._view.spherical.limits.enabled = true; + this._view.target.limits.enabled = true; + this._view.controls = true; + this._timeline.clear(); this.emit(ANIMATION_ENDED); }, 100); @@ -61,348 +60,323 @@ export class Navigation extends ExperienceBasedBlueprint { target: Exclude["limits"]; } = { spherical: { - radius: { min: 5, max: 20 }, - phi: { min: 0.01, max: Math.PI * 0.5 }, - theta: { min: 0, max: Math.PI * 0.5 }, - enabled: true, - enabledPhi: true, - enabledTheta: true, + radius: { min: 0, max: 0 }, + phi: { min: 0, max: 0 }, + theta: { min: 0, max: 0 }, + enabled: false, + enabledPhi: false, + enabledTheta: false, }, target: { - x: { min: -3, max: 3 }, - y: { min: 2, max: 6 }, - z: { min: -2.5, max: 4 }, - enabled: true, + x: { min: 0, max: 0 }, + y: { min: 0, max: 0 }, + z: { min: 0, max: 0 }, + enabled: false, }, } as const; - private _view: NavigationView = {}; - - private _setView() { - this._view.enabled = true; - this._view.controls = true; - - this._view.center = new Vector3(); - - this._view.spherical = { - value: new Spherical(20, Math.PI * 0.5, 0), - smoothed: new Spherical(20, Math.PI * 0.5, 0), + private readonly _view: NavigationView = { + enabled: true, + controls: true, + limits: false, + center: new Vector3(), + spherical: { + value: new Spherical(), + smoothed: new Spherical(), smoothing: 0.005, limits: this._viewLimits.spherical, - }; - - this._view.target = { + }, + target: { value: new Vector3(0, 2, 0), smoothed: new Vector3(0, 2, 0), smoothing: 0.005, limits: this._viewLimits.target, - }; - - this._view.drag = { + }, + drag: { delta: { x: 0, y: 0 }, previous: { x: 0, y: 0 }, sensitivity: 1, alternative: false, - }; - - this._view.zoom = { sensitivity: 0.01, delta: 0 }; - - this._view.down = (_x, _y) => { - if (!this._view.drag?.previous) return; - - this._view.drag.previous.x = _x; - this._view.drag.previous.y = _y; - }; - this._view.move = (_x, _y) => { - if ( - !this.view.controls || - !this._view.enabled || - !this._view?.drag?.delta || - !this._view.drag.previous - ) - return; - - this._view.drag.delta.x += _x - this._view.drag.previous.x; - this._view.drag.delta.y += _y - this._view.drag.previous.y; - - this._view.drag.previous.x = _x; - this._view.drag.previous.y = _y; - }; - this._view.up = () => {}; - this._view.zooming = (_delta) => { - if (typeof this._view.zoom?.delta !== "number") return; - - this._view.zoom.delta += _delta; - }; - - /** - * Mouse events - */ - this._view.onMouseDown = (_event) => { - _event.preventDefault(); - - if ( - !this.view.controls || - !this._view.enabled || - !this._view.drag || - !this._view.down || - !this._view.onMouseUp || - !this._view.onMouseMove - ) - return; - - this._view.drag.alternative = - _event.button === 2 || - _event.button === 1 || - _event.ctrlKey || - _event.shiftKey; - - this._view?.down(_event.clientX, _event.clientY); - - this._ui?.targetElementParent?.addEventListener<"mouseup">( - "mouseup", - this._view.onMouseUp - ); - this._ui?.targetElementParent?.addEventListener( - "mousemove", - this._view.onMouseMove - ); - }; + }, + zoom: { sensitivity: 0.01, delta: 0 }, + }; - this._view.onMouseMove = (_event) => { - _event.preventDefault(); - if (!this._view.move) return; + public get timeline() { + return this._timeline; + } - this._view.move(_event.clientX, _event.clientY); - }; + public get view() { + return this._view; + } - this._view.onMouseUp = (_event) => { - _event.preventDefault(); + private _setConfig() { + const boundingClient = this._targetElement.getBoundingClientRect(); + const width = boundingClient.width; + const height = Number(boundingClient.height || window?.innerHeight); - if (!this._view?.up || !this._view.onMouseUp || !this._view.onMouseMove) - return; + this._config.smallestSide = Math.min(width, height); + this._config.largestSide = Math.max(width, height); + } - this._view.up(); + public construct() { + if (!this._camera) + throw new Error("Camera class not initialized", { + cause: errors.WRONG_PARAM, + }); - this._ui?.targetElementParent?.removeEventListener( - "mouseup", - this._view.onMouseUp + this._setConfig(); + + // Init view + ~(() => { + this._view.spherical.value = new Spherical().setFromVector3( + this._camera?.instance.position ?? new Vector3() ); - this._ui?.targetElementParent?.removeEventListener( - "mousemove", - this._view.onMouseMove + this._view.spherical.smoothed = new Spherical().setFromVector3( + this._camera?.instance.position ?? new Vector3() ); - }; + })(); - this._ui?.targetElementParent?.addEventListener( - "mousedown", - this._view.onMouseDown - ); + // Init mouse events + ~(() => { + this._view.down = (_x, _y) => { + if (!this._view.drag?.previous) return; - this._view.onTouchStart = (_event) => { - _event.preventDefault(); + this._view.drag.previous.x = _x; + this._view.drag.previous.y = _y; + }; + this._view.move = (_x, _y) => { + if (!this.view.controls || !this._view.enabled) return; - if ( - !this._view.drag || - !this._view.down || - !this._view.onTouchEnd || - !this._view.onTouchMove - ) - return; + this._view.drag.delta.x += _x - this._view.drag.previous.x; + this._view.drag.delta.y += _y - this._view.drag.previous.y; - this._view.drag.alternative = _event.touches.length > 1; + this._view.drag.previous.x = _x; + this._view.drag.previous.y = _y; + }; + this._view.up = () => {}; + this._view.zooming = (_delta) => { + if (typeof this._view.zoom?.delta !== "number") return; - this._view.down(_event.touches[0].clientX, _event.touches[0].clientY); + this._view.zoom.delta += _delta; + }; - this._ui?.targetElementParent?.addEventListener( - "touchend", - this._view.onTouchEnd - ); - this._ui?.targetElementParent?.addEventListener( - "touchmove", - this._view.onTouchMove - ); - }; + this._view.onMouseDown = (_event) => { + _event.preventDefault(); - this._view.onTouchMove = (_event) => { - _event.preventDefault(); + if ( + !this.view.controls || + !this._view.enabled || + !this._view.drag || + !this._view.down || + !this._view.onMouseUp || + !this._view.onMouseMove + ) + return; - if (!this._view.move) return; + this._view.drag.alternative = + _event.button === 2 || + _event.button === 1 || + _event.ctrlKey || + _event.shiftKey; - this._view.move(_event.touches[0].clientX, _event.touches[0].clientY); - }; + this._view?.down(_event.clientX, _event.clientY); - this._view.onTouchEnd = (_event) => { - _event.preventDefault(); + this._ui?.targetElementParent?.addEventListener<"mouseup">( + "mouseup", + this._view.onMouseUp + ); + this._ui?.targetElementParent?.addEventListener( + "mousemove", + this._view.onMouseMove + ); + }; + this._view.onMouseMove = (_event) => { + _event.preventDefault(); + if (!this._view.move) return; - if (!this._view.up || !this._view.onTouchEnd || !this._view.onTouchMove) - return; + this._view.move(_event.clientX, _event.clientY); + }; + this._view.onMouseUp = (_event) => { + _event.preventDefault(); - this._view.up(); + if (!this._view?.up || !this._view.onMouseUp || !this._view.onMouseMove) + return; - this._ui?.targetElementParent?.removeEventListener( - "touchend", - this._view.onTouchEnd - ); - this._ui?.targetElementParent?.removeEventListener( - "touchmove", - this._view.onTouchMove - ); - }; - - this._ui?.targetElementParent?.addEventListener( - "touchstart", - this._view.onTouchStart - ); - - this._view.onContextMenu = (_event) => { - _event.preventDefault(); - }; - - this._ui?.targetElementParent?.addEventListener( - "contextmenu", - this._view.onContextMenu - ); - - this._view.onWheel = (_event) => { - _event.preventDefault(); - - if ( - !this.view.controls || - !this._view.enabled || - !this._view.zooming || - !this._view.onWheel - ) - return; - - const normalized = normalizeWheel(_event); - this._view.zooming(normalized.pixelY); - }; - - this._ui?.targetElementParent?.addEventListener( - "mousewheel", - this._view.onWheel, - { - passive: false, - } - ); + this._view.up(); - this._ui?.targetElementParent?.addEventListener( - "wheel", - this._view.onWheel, - { passive: false } - ); - } + this._ui?.targetElementParent?.removeEventListener( + "mouseup", + this._view.onMouseUp + ); + this._ui?.targetElementParent?.removeEventListener( + "mousemove", + this._view.onMouseMove + ); + }; + this._view.onTouchStart = (_event) => { + _event.preventDefault(); - private _setConfig() { - this._config.pixelRatio = this._experience.app.sizes.pixelRatio; + if ( + !this._view.down || + !this._view.onTouchEnd || + !this._view.onTouchMove + ) + return; - const boundingClient = this._targetElement.getBoundingClientRect(); - this._config.width = boundingClient.width; - this._config.height = Number(boundingClient.height || window?.innerHeight); - this._config.smallestSide = Math.min( - this._config.width, - this._config.height - ); - this._config.largestSide = Math.max( - this._config.width, - this._config.height - ); - } + this._view.drag.alternative = _event.touches.length > 1; + this._view.down(_event.touches[0].clientX, _event.touches[0].clientY); - public get timeline() { - return this._timeline; - } + this._ui?.targetElementParent?.addEventListener( + "touchend", + this._view.onTouchEnd + ); + this._ui?.targetElementParent?.addEventListener( + "touchmove", + this._view.onTouchMove + ); + }; + this._view.onTouchMove = (_event) => { + _event.preventDefault(); - public get view() { - return this._view; - } + if (!this._view.move) return; - public construct() { - this._setConfig(); - this._setView(); + this._view.move(_event.touches[0].clientX, _event.touches[0].clientY); + }; + this._view.onTouchEnd = (_event) => { + _event.preventDefault(); + + if (!this._view.up || !this._view.onTouchEnd || !this._view.onTouchMove) + return; + + this._view.up(); + + this._ui?.targetElementParent?.removeEventListener( + "touchend", + this._view.onTouchEnd + ); + this._ui?.targetElementParent?.removeEventListener( + "touchmove", + this._view.onTouchMove + ); + }; + this._view.onContextMenu = (_event) => { + _event.preventDefault(); + }; + this._view.onWheel = (_event) => { + _event.preventDefault(); + + if ( + !this.view.controls || + !this._view.enabled || + !this._view.zooming || + !this._view.onWheel + ) + return; + + const normalized = normalizeWheel(_event); + this._view.zooming(normalized.pixelY); + }; + + this._ui?.targetElementParent?.addEventListener( + "mousedown", + this._view.onMouseDown + ); + this._ui?.targetElementParent?.addEventListener( + "touchstart", + this._view.onTouchStart + ); + this._ui?.targetElementParent?.addEventListener( + "contextmenu", + this._view.onContextMenu + ); + this._ui?.targetElementParent?.addEventListener( + "mousewheel", + this._view.onWheel, + { + passive: false, + } + ); + this._ui?.targetElementParent?.addEventListener( + "wheel", + this._view.onWheel, + { passive: false } + ); + })(); this._appSizes.on("resize", () => this._setConfig()); } public destruct() { - if (!this.view) return; - this._view.onMouseDown && this._ui?.targetElementParent?.removeEventListener( "mousedown", this._view.onMouseDown ); - this._view.onMouseUp && this._ui?.targetElementParent?.removeEventListener( "mouseup", this._view.onMouseUp ); - this._view.onMouseMove && this._ui?.targetElementParent?.removeEventListener( "mousemove", this._view.onMouseMove ); - this._view.onTouchEnd && this._ui?.targetElementParent?.removeEventListener( "touchend", this._view.onTouchEnd ); - this._view.onTouchMove && this._ui?.targetElementParent?.removeEventListener( "touchmove", this._view.onTouchMove ); - this._view.onTouchStart && this._ui?.targetElementParent?.removeEventListener( "touchstart", this._view.onTouchStart ); - this._view.onContextMenu && this._ui?.targetElementParent?.removeEventListener( "contextmenu", this._view.onContextMenu ); - this._view.onWheel && this._ui?.targetElementParent?.removeEventListener( "mousewheel", this._view.onWheel ); - this._view.onWheel && this._ui?.targetElementParent?.removeEventListener( "wheel", this._view.onWheel ); + + this._appSizes.off("resize", () => this._setConfig()); } - public disableFreeAzimuthRotation(limits?: { min: number; max: number }) { - if (this._view.spherical?.limits) { - this._view.spherical.limits.enabledTheta = true; - if (limits) this._view.spherical.limits.theta = limits; - } + /** + * Disable horizontal free rotation. + * + * @param limits Set the min & max limits (Optional) + */ + public disableAzimuthRotation(limits?: { min: number; max: number }) { + this._view.spherical.limits.enabledTheta = true; + if (limits) this._view.spherical.limits.theta = limits; } - public disableFreePolarRotation(limits?: { min: number; max: number }) { - if (this._view.spherical?.limits) { - this._view.spherical.limits.enabledPhi = true; - if (limits) this._view.spherical.limits.phi = limits; - } + public disablePolarRotation(limits?: { min: number; max: number }) { + this._view.spherical.limits.enabledPhi = true; + if (limits) this._view.spherical.limits.phi = limits; } - public enableFreeAzimuthRotation() { + public enableAzimuthRotation() { if (this._view.spherical?.limits) this._view.spherical.limits.enabledTheta = false; } - public enableFreePolarRotation() { + public enablePolarRotation() { if (this._view.spherical?.limits) this._view.spherical.limits.enabledPhi = false; } @@ -417,8 +391,8 @@ export class Navigation extends ExperienceBasedBlueprint { */ public setTargetPosition(v3 = new Vector3()) { const V3 = v3.clone(); - this._view.target?.value?.copy(V3); - this._view.target?.smoothed?.copy(V3); + this._view.target.value = V3; + this._view.target.smoothed = V3; return this._view.target; } @@ -428,12 +402,6 @@ export class Navigation extends ExperienceBasedBlueprint { * @param v3 The {@link Vector3} position where the the camera will look at. */ public setPositionInSphere(v3 = new Vector3()) { - if ( - !this._view.spherical?.smoothed || - !this._view.spherical.value || - !this._view.target?.smoothed - ) - return; const SAFE_V3 = v3 .clone() .add( @@ -457,60 +425,69 @@ export class Navigation extends ExperienceBasedBlueprint { * * @param toPosition The new camera position. * @param target Where the camera will look at. + * @param duration Animation duration. */ public updateCameraPosition( toPosition = new Vector3(), lookAt = new Vector3(), + // TODO: Change it to TimelineVars instead. duration = Config.GSAP_ANIMATION_DURATION ) { - if (!this._appCamera.instance) return this._timeline; + if (!this._camera) return this._timeline; const targetA = this._view.target?.value?.clone(); const targetB = lookAt.clone(); - const currentCamPosition = this._appCamera.instance?.position.clone(); + const currentCamPosition = this._camera.instance.position.clone(); if (!targetA || !targetB || !currentCamPosition) return this._timeline; + if (this._timeline.isActive()) { + console.log("One"); + this._timeline.progress(1); + } - return this._timeline.to(currentCamPosition, { - x: toPosition.x, - y: toPosition.y, - z: toPosition.z, - duration, - ease: Config.GSAP_ANIMATION_EASE, - onStart: () => { - gsap.to(targetA, { - x: targetB.x, - y: targetB.y, - z: targetB.z, - duration: duration * 0.55, + return this._timeline + .to(targetA, { + x: targetB.x, + y: targetB.y, + z: targetB.z, + duration: duration * 0.55, + ease: Config.GSAP_ANIMATION_EASE, + onUpdate: () => { + this.setTargetPosition(targetA); + }, + onComplete: () => { + this.setTargetPosition(targetA); + }, + }) + .add( + gsap.to(currentCamPosition, { + x: toPosition.x, + y: toPosition.y, + z: toPosition.z, + duration, ease: Config.GSAP_ANIMATION_EASE, onUpdate: () => { - this?.setTargetPosition(targetA); + this.setPositionInSphere(currentCamPosition); }, - }); - }, - onUpdate: () => { - if ( - !this._view.spherical?.value || - !this._view.spherical.smoothed || - !this._view.spherical.smoothing - ) - return; - this.setPositionInSphere(currentCamPosition); - }, - }); + onComplete: () => { + this.setPositionInSphere(currentCamPosition); + this.setTargetPosition(targetB); + }, + }), + "<" + ); } /** * Set navigation limits. use default config limits if not parameter was passed. * - * @param _ Limits `spherical` and `target` + * @param limits Limits `spherical` and `target` (Optional) */ - public setLimits(_?: { + public setLimits(limits?: { spherical?: Exclude["limits"]; target?: Exclude["limits"]; }) { - if (!_) { + if (!limits) { if (this._view.spherical) this._view.spherical.limits = this._viewLimits.spherical; if (this._view.target) this._view.target.limits = this._viewLimits.target; @@ -518,22 +495,13 @@ export class Navigation extends ExperienceBasedBlueprint { return; } - if (_.spherical) - if (this._view.spherical) this._view.spherical.limits = _.spherical; - if (_.target && this._view.target) this._view.target.limits = _.target; + if (limits.spherical) + if (this._view.spherical) this._view.spherical.limits = limits.spherical; + if (limits.target) this._view.target.limits = limits.target; } public update() { - if ( - !this._view.enabled || - !this._view.spherical || - !this._view.zoom || - !this._view.drag || - !this._view.target || - !this._view.center || - !this._appCamera.instance - ) - return; + if (!this._view.enabled || !this._camera?.instance) return; // Zoom this._view.spherical.value.radius += @@ -544,8 +512,8 @@ export class Navigation extends ExperienceBasedBlueprint { const up = new Vector3(0, 1, 0); const right = new Vector3(-1, 0, 0); - up.applyQuaternion(this._appCamera.instance.quaternion); - right.applyQuaternion(this._appCamera.instance.quaternion); + up.applyQuaternion(this._camera.instance.quaternion); + right.applyQuaternion(this._camera.instance.quaternion); up.multiplyScalar(Number(this._view.drag.delta?.y) * 0.01); right.multiplyScalar(Number(this._view.drag.delta?.x) * 0.01); @@ -561,7 +529,7 @@ export class Navigation extends ExperienceBasedBlueprint { this._config.smallestSide; } - if (!this._timeline.isActive()) { + if (!this._timeline.isActive() && this._view.limits) { // Apply limits if (this._view.spherical.limits.enabled) { this._view.spherical.value.radius = Math.min( diff --git a/src/experiences/home/renderer.ts b/src/experiences/home/renderer.ts index bc92000..3561d54 100644 --- a/src/experiences/home/renderer.ts +++ b/src/experiences/home/renderer.ts @@ -17,6 +17,9 @@ import { HomeExperience } from "."; // INTERFACES import { ExperienceBasedBlueprint } from "~/blueprints/experiences/experience-based.blueprint"; +// CONFIG +import { Config } from "~/config"; + export interface PortalAssets { mesh: THREE.Mesh; meshWebGLTexture: THREE.WebGLRenderTarget; @@ -114,7 +117,8 @@ export class Renderer extends ExperienceBasedBlueprint { this._experience.app.sizes.pixelRatio ); this._appRenderer.instance.localClippingEnabled = true; - this._appRenderer.instance.domElement.style.pointerEvents = "none"; + if (!Config.DEBUG) + this._appRenderer.instance.domElement.style.pointerEvents = "none"; })(); ~(() => { diff --git a/src/experiences/home/world/index.ts b/src/experiences/home/world/index.ts index af8d3ae..6c5bf2b 100644 --- a/src/experiences/home/world/index.ts +++ b/src/experiences/home/world/index.ts @@ -43,16 +43,6 @@ export class World extends ExperienceBasedBlueprint { private _availablePageScenes: { [sceneKey: string]: SceneComponentBlueprint; } = {}; - private _mainSceneConfig: SceneConfig = { - center: new Vector3(), - position: new Vector3(), - cameraPath: new CatmullRomCurve3([]), - }; - private _projectedSceneConfig: SceneConfig = { - center: new Vector3(), - position: new Vector3(), - cameraPath: new CatmullRomCurve3([]), - }; private _projectedSceneContainer?: Group; public readonly mainSceneKey = pages.HOME_PAGE; @@ -108,14 +98,6 @@ export class World extends ExperienceBasedBlueprint { return this._projectedSceneContainer; } - public get mainSceneConfig() { - return this._mainSceneConfig; - } - - public get projectedSceneConfig() { - return this._projectedSceneConfig; - } - public destruct() { if (this.group) { this.group.traverse((child) => { @@ -146,6 +128,15 @@ export class World extends ExperienceBasedBlueprint { } } + private _correctCameraPath(paths: CatmullRomCurve3, center: Vector3) { + const points = paths.points; + for (const point of points) { + point.add(center); + } + + return new CatmullRomCurve3(points); + } + public async construct() { if (!(this._appCamera.instance instanceof PerspectiveCamera)) throw new Error(undefined, { cause: errors.CAMERA_UNAVAILABLE }); @@ -160,65 +151,49 @@ export class World extends ExperienceBasedBlueprint { this.scene2?.construct(); this.scene3?.construct(); - if (this.sceneContainer?.modelScene instanceof Group) { - const BOUNDING_BOX = new Box3().setFromObject( - this.sceneContainer.modelScene - ); - // const WIDTH = BOUNDING_BOX.max.x - BOUNDING_BOX.min.x; - const HEIGHT = BOUNDING_BOX.max.y - BOUNDING_BOX.min.y; - - this._mainSceneConfig.position.set(0, 0, 0); - this._mainSceneConfig.center.set( - this._mainSceneConfig.position.x, - this._mainSceneConfig.position.y + 2.5, - this._mainSceneConfig.position.z - ); - this._mainSceneConfig.cameraPath.points = [ - new Vector3(0, 5.5, 21), - new Vector3(12, 10, 12), - new Vector3(21, 5.5, 0), - new Vector3(12, 3.7, 12), - new Vector3(0, 5.5, 21), - ]; - - this._projectedSceneConfig.position.set(0, HEIGHT * -2, 0); - this._projectedSceneConfig.center.set( - this._projectedSceneConfig.position.x, - this._projectedSceneConfig.position.y + 1.5, - this._projectedSceneConfig.position.z - ); - this._projectedSceneConfig.cameraPath.points = [ - new Vector3(0, 5.5, 21).add(this._projectedSceneConfig.position), - new Vector3(12, 10, 12).add(this._projectedSceneConfig.position), - new Vector3(21, 5.5, 0).add(this._projectedSceneConfig.position), - new Vector3(12, 3.7, 12).add(this._projectedSceneConfig.position), - new Vector3(0, 5.5, 21).add(this._projectedSceneConfig.position), - ]; - - this._projectedSceneContainer = this.sceneContainer.modelScene.clone(); - this._projectedSceneContainer.position.copy( - this._projectedSceneConfig.position - ); + if (!(this.sceneContainer?.modelScene instanceof Group)) return; - this.group?.add( - this.sceneContainer.modelScene, - this._projectedSceneContainer - ); - } + const BOUNDING_BOX = new Box3().setFromObject( + this.sceneContainer.modelScene + ); + // const WIDTH = BOUNDING_BOX.max.x - BOUNDING_BOX.min.x; + const HEIGHT = BOUNDING_BOX.max.y - BOUNDING_BOX.min.y; + const PROJECTED_SCENE_POSITION = new Vector3(0, HEIGHT * -2, 0); + const PROJECTED_SCENE_CENTER = PROJECTED_SCENE_POSITION.clone(); + PROJECTED_SCENE_CENTER.y += 1.5; + + this._projectedSceneContainer = this.sceneContainer.modelScene.clone(); + this._projectedSceneContainer.position.copy(PROJECTED_SCENE_POSITION); + + this.group?.add( + this.sceneContainer.modelScene, + this._projectedSceneContainer + ); if (this.scene1?.modelScene) { + this.scene1.center = this.scene1.center.setY(2.5); this._availablePageScenes[pages.HOME_PAGE] = this.scene1; this.group?.add(this.scene1.modelScene); } if (this.scene2?.modelScene) { - this.scene2.modelScene.position.copy(this.projectedSceneConfig.position); + this.scene2.modelScene.position.copy(PROJECTED_SCENE_POSITION); + this.scene2.center = PROJECTED_SCENE_CENTER; + this.scene2.cameraPath = this._correctCameraPath( + this.scene2.cameraPath, + PROJECTED_SCENE_CENTER + ); this._availablePageScenes[pages.SKILL_PAGE] = this.scene2; this.group?.add(this.scene2.modelScene); } if (this.scene3?.modelScene) { - this.scene3.modelScene.position.copy(this.projectedSceneConfig.position); + this.scene3.modelScene.position.copy(PROJECTED_SCENE_POSITION); + this.scene3.center = PROJECTED_SCENE_CENTER; + this.scene3.cameraPath = this._correctCameraPath( + this.scene3.cameraPath, + PROJECTED_SCENE_CENTER + ); this._availablePageScenes[pages.CONTACT_PAGE] = this.scene3; this.group?.add(this.scene3.modelScene); } diff --git a/src/experiences/home/world/manager.ts b/src/experiences/home/world/manager.ts index b8061ff..c953c61 100644 --- a/src/experiences/home/world/manager.ts +++ b/src/experiences/home/world/manager.ts @@ -1,4 +1,4 @@ -import { Material, Mesh, Raycaster, Vector3 } from "three"; +import { Vector3 } from "three"; import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass"; import gsap, { Power0 } from "gsap"; @@ -8,14 +8,8 @@ import { HomeExperience } from ".."; // BLUEPRINTS import { ExperienceBasedBlueprint } from "~/blueprints/experiences/experience-based.blueprint"; -// CONFIG -import { Config } from "~/config"; - // STATIC -import { errors, events } from "~/static"; - -// ERROR -import { ErrorFactory } from "~/errors"; +import { errors, events, pages } from "~/static"; // UTILS import { lerpPosition } from "~/utils/three-utils"; @@ -23,41 +17,27 @@ import { lerpPosition } from "~/utils/three-utils"; // SHADERS import camTransitionFrag from "./shaders/glass-effect/fragment.glsl"; import camTransitionVert from "./shaders/glass-effect/vertex.glsl"; +import { WRONG_PARAM } from "~/static/error.static"; export class WorldManager extends ExperienceBasedBlueprint { protected readonly _experience = new HomeExperience(); - private readonly _appCamera = this._experience.app.camera; - private readonly _appCameraInstance = this._appCamera.instance; private readonly _appResources = this._experience.app.resources; private readonly _router = this._experience.router; private readonly _camera = this._experience.camera; + private readonly _cameraAnimation = this._experience.cameraAnimation; private readonly _navigation = this._experience.navigation; private readonly _composer = this._experience.composer; - private readonly _renderer = this._experience.renderer; - private readonly _timeline = gsap.timeline(); + private readonly _transitionEffectDefault = { + duration: 0.3, + ease: Power0.easeIn, + }; private _world: typeof this._experience.world; - private _config: { - prevSceneKey?: string; - cameraTransitionPass: ShaderPass; - glassEffectDefault: { duration: number; ease: gsap.EaseFunction }; - } = { - cameraTransitionPass: new ShaderPass({ - uniforms: { - tDiffuse: { value: null }, - uStrength: { value: 0 }, - uDisplacementMap: { - value: this._appResources.items.rocksAlphaMap, - }, - }, - vertexShader: camTransitionVert, - fragmentShader: camTransitionFrag, - }), - glassEffectDefault: { duration: 0.3, ease: Power0.easeIn }, - }; + private _prevSceneKey?: string; + private _prevProjectedSceneKey?: string; - public rayCaster = new Raycaster(); + public readonly timeline = gsap.timeline(); private get _supportedPageKeys() { if (!this._world?.availablePageScenes) return []; @@ -65,288 +45,210 @@ export class WorldManager extends ExperienceBasedBlueprint { return Object.keys(this._world.availablePageScenes); } - public get timeline() { - return this._timeline; - } - - /** Initialize the the default scenes states. */ - private async _initScenes() { - if (this._supportedPageKeys.length < 2) - throw new ErrorFactory( - new Error("Unable to display the projected scene ", { - cause: errors.WRONG_PARAM, - }) - ); - - if ( - !this._appCameraInstance || - !this._camera?.cameras || - this._camera?.cameras.length < 2 - ) - throw new ErrorFactory( - new Error("No enough camera found", { - cause: errors.WRONG_PARAM, - }) - ); - - if (!this._world) - throw new ErrorFactory( - new Error("World not initialized", { - cause: errors.WRONG_PARAM, - }) - ); - - const CURRENT_CAMERA = this._camera.cameras[0]; - const SECONDARY_CAMERA = this._camera.cameras[1]; - - let projectedScene = - this._world.availablePageScenes[this._supportedPageKeys[1]]; - - this._world.mainSceneConfig.cameraPath.getPointAt( - 0, - CURRENT_CAMERA.position - ); - this._world.mainSceneConfig.cameraPath.getPointAt( - 0, - this._appCameraInstance.position - ); - - this._camera.setCameraLookAt( - (this._world?.scene1?.modelScene?.position ?? new Vector3()) - .clone() - .setY(2) - ); + /** Launch a screen composer effect. */ + private _triggerTransitionEffect() { + if (!this._composer) return this.timeline; + if (this.timeline.isActive()) this.timeline.progress(1); - if ( - typeof this._router?.currentRouteKey === "string" && - this._router.currentRouteKey !== this._world?.mainSceneKey && - this._router.currentRouteKey !== this._supportedPageKeys[1] - ) - projectedScene = - this._world?.availablePageScenes[this._router.currentRouteKey]; - - projectedScene.modelScene?.children.forEach((child) => { - if (child instanceof Mesh) child.material.alphaTest = 0; + let shaderPass: ShaderPass | undefined = new ShaderPass({ + uniforms: { + tDiffuse: { value: null }, + uStrength: { value: 0 }, + uDisplacementMap: { + value: this._appResources.items.rocksAlphaMap, + }, + }, + vertexShader: camTransitionVert, + fragmentShader: camTransitionFrag, }); - if ( - this._world.scene1?.modelScene && - this._world.scene1.pcScreen && - this._world.scene1.pcScreenWebglTexture && - SECONDARY_CAMERA - ) { - await this._world.scene1.togglePcOpening(); - - this._renderer?.addPortalAssets(`${this._world.scene1}_pc_screen`, { - mesh: this._world.scene1.pcScreen, - meshCamera: SECONDARY_CAMERA, - meshWebGLTexture: this._world.scene1.pcScreenWebglTexture, - }); - - this._world.projectedSceneConfig.cameraPath.getPoint( - 0, - SECONDARY_CAMERA.position - ); - SECONDARY_CAMERA.lookAt(this._world.projectedSceneConfig.center); - SECONDARY_CAMERA.userData.lookAt = - this._world.projectedSceneConfig.center; - } + shaderPass.clear = true; + shaderPass.uniforms.uStrength.value = 0; - this._setScene(); - } - - /** Launch a screen glass effect. */ - private _triggerGlassTransitionEffect() { - if ( - !this._config.cameraTransitionPass.uniforms.uStrength || - !this._composer - ) - return this._timeline; - if (this._timeline.isActive()) this._timeline.progress(1); - - this._config.cameraTransitionPass.clear = true; - this._config.cameraTransitionPass.uniforms.uStrength.value = 0; - - this._composer.addPass( - "cameraTransitionPass", - this._config.cameraTransitionPass - ); + this._composer.addPass(`${WorldManager.name}_shaderPass`, shaderPass); - return this._timeline - .to(this._config.cameraTransitionPass.material.uniforms.uStrength, { - ...this._config.glassEffectDefault, + return this.timeline + .to(shaderPass.material.uniforms.uStrength, { + ...this._transitionEffectDefault, value: 0.175, }) - .to(this._config.cameraTransitionPass.material.uniforms.uStrength, { - ...this._config.glassEffectDefault, + .to(shaderPass.material.uniforms.uStrength, { + ...this._transitionEffectDefault, value: 0, ease: Power0.easeOut, }) - .add( - () => - this._config.cameraTransitionPass && - this._composer?.removePass("cameraTransitionPass"), - ">" - ); + .add(() => { + this._composer?.removePass(`${WorldManager.name}_shaderPass`); + shaderPass?.dispose(); + shaderPass = undefined; + }, ">"); } - /** - * Transition between projected scenes. - * - * @param nextSceneKey The incoming scene key. - */ - private _changeProjectedScene(nextSceneKey?: string) { - const CURRENT_SCENE = this._world?.availablePageScenes[nextSceneKey ?? ""]; - - if ( - CURRENT_SCENE?.modelScene && - nextSceneKey !== this._world?.mainSceneKey && - nextSceneKey !== this._config.prevSceneKey - ) { - const PARAMS = { alphaTest: 0 }; - CURRENT_SCENE.modelScene.renderOrder = 1; - - this._timeline.to(PARAMS, { - alphaTest: 1, - duration: Config.GSAP_ANIMATION_DURATION, - onStart: () => { - if (!this._navigation?.timeline.isActive()) - this._navigation?.setLimits(CURRENT_SCENE.navigationLimits); - }, - onUpdate: () => { - this._supportedPageKeys.slice(1).forEach((supportedPageKey) => { - const SCENE = this._world?.availablePageScenes[supportedPageKey]; - if ( - supportedPageKey === this._world?.mainSceneKey || - !SCENE?.modelScene - ) - return; - - SCENE?.modelScene?.traverse((child) => { - if ( - !(child instanceof Mesh) || - !(child.material instanceof Material) || - (nextSceneKey === supportedPageKey && - child.material.alphaTest === 0) || - (nextSceneKey !== supportedPageKey && - child.material.alphaTest === 1) - ) - return; - - child.material.alphaTest = - nextSceneKey === supportedPageKey - ? 1 - PARAMS.alphaTest - : PARAMS.alphaTest; - }); - }); - }, - onComplete: () => { - CURRENT_SCENE.intro(); - }, - }); - } - } - - /** Set the current scene depending to the current `Navigation` state */ + /** Set the current scene depending to the current `Router` state */ private _setScene() { if ( - typeof this._experience.router?.currentRouteKey !== "string" || - this._supportedPageKeys.indexOf( - this._experience.router.currentRouteKey - ) === -1 + typeof this._router?.currentRouteKey !== "string" || + this._supportedPageKeys.indexOf(this._router.currentRouteKey) === -1 ) - throw new ErrorFactory( - new Error("Page not supported", { cause: errors.WRONG_PARAM }) - ); + throw new Error("Page not supported", { cause: errors.WRONG_PARAM }); if (!this._world) - throw new ErrorFactory( - new Error("World not initialized", { - cause: errors.WRONG_PARAM, - }) - ); + throw new Error("World not initialized", { + cause: errors.WRONG_PARAM, + }); - if ( - !this._appCameraInstance || - !this._camera?.cameras || - this._camera?.cameras.length < 2 - ) - throw new ErrorFactory( - new Error("No enough camera found", { - cause: errors.WRONG_PARAM, - }) + if (!this._camera?.cameras || this._camera?.cameras.length < 2) + throw new Error("No enough camera found", { + cause: errors.WRONG_PARAM, + }); + + if (!this._cameraAnimation) + throw new Error("No cameraAnimation class found", { cause: WRONG_PARAM }); + + const PREV_SCENE = this?._prevSceneKey + ? this._world.availablePageScenes[this._prevSceneKey] + : undefined; + const CURRENT_SCENE = + this._world.availablePageScenes[this._router.currentRouteKey]; + const SCENE1_PC_SCREEN_POSITION = + this._world.scene1?.pcScreen?.localToWorld(new Vector3()) ?? + new Vector3(); + const IS_SWITCHING_MAIN = !!( + this._prevSceneKey && + this._router?.currentRouteKey === this._world.mainSceneKey + ); + const IS_SWITCHING_PROJECTED = + this._router.currentRouteKey !== this._world?.mainSceneKey && + this._camera?.currentCameraIndex === 0; + + const updateCameraToCurrentScene = () => + this._navigation?.updateCameraPosition( + CURRENT_SCENE.cameraPath?.getPoint(0), + CURRENT_SCENE.center, + 0.84 ); if (this._camera?.timeline.isActive()) this._camera.timeline.progress(1); if (this._navigation?.timeline.isActive()) this._navigation.timeline.progress(1); - if (this._timeline.isActive()) this._timeline.progress(1); + if (this.timeline.isActive()) this.timeline.progress(1); + if (PREV_SCENE?.timeline?.isActive()) PREV_SCENE.timeline.progress(1); + if (CURRENT_SCENE.timeline?.isActive()) CURRENT_SCENE.timeline.progress(1); - const CURRENT_SCENE = - this._world.availablePageScenes[this._experience.router.currentRouteKey]; - const SCREEN_POSITION = - this._world.scene1?.pcScreen?.localToWorld(new Vector3()) ?? - new Vector3(); + if (this?._prevSceneKey !== this._world.mainSceneKey && !IS_SWITCHING_MAIN) + PREV_SCENE?.outro(); - if ( - this._config.prevSceneKey && - (this._experience.router?.currentRouteKey === this._world.mainSceneKey || - CURRENT_SCENE === undefined) - ) { - this._triggerGlassTransitionEffect().add(() => { - if (!this._world?.mainSceneConfig) return; + if (this._prevProjectedSceneKey !== this._router?.currentRouteKey) { + CURRENT_SCENE.intro(); + + if (this._prevProjectedSceneKey && IS_SWITCHING_PROJECTED) { + this._world.availablePageScenes[this._prevProjectedSceneKey]?.outro(); + this._prevProjectedSceneKey = undefined; + } + } + + this._navigation?.setViewCenter(CURRENT_SCENE.center); + this._cameraAnimation.cameraPath = CURRENT_SCENE.cameraPath; + this._cameraAnimation.progress = { + ...this._cameraAnimation.progress, + current: 0, + target: 0, + }; + if (IS_SWITCHING_MAIN) { + this._prevProjectedSceneKey = this._prevSceneKey; + + this._triggerTransitionEffect().add(() => { this._camera?.switchCamera(0); this._navigation?.setLimits(CURRENT_SCENE.navigationLimits); - this._navigation?.setViewCenter(new Vector3()); - this._navigation?.setTargetPosition(SCREEN_POSITION); + this._navigation?.setTargetPosition(SCENE1_PC_SCREEN_POSITION); this._navigation?.updateCameraPosition( - this._world.mainSceneConfig.cameraPath.getPoint(0), - this._world.mainSceneConfig.center + CURRENT_SCENE.cameraPath?.getPoint(0), + CURRENT_SCENE.center ); - }, `-=${this._config.glassEffectDefault.duration}`); + }, `-=${this._transitionEffectDefault.duration}`); } - if ( - this._experience.router.currentRouteKey !== this._world?.mainSceneKey && - this._camera?.currentCameraIndex !== 1 - ) { + if (IS_SWITCHING_PROJECTED) this._navigation ?.updateCameraPosition( lerpPosition( - new Vector3(0, SCREEN_POSITION.y, 0), - SCREEN_POSITION, + new Vector3(0, SCENE1_PC_SCREEN_POSITION.y, 0), + SCENE1_PC_SCREEN_POSITION, 0.84 ), - SCREEN_POSITION + SCENE1_PC_SCREEN_POSITION ) .add(() => { - this._triggerGlassTransitionEffect().add(() => { - if (!this._world?.projectedSceneConfig.center) return; - - this._camera?.switchCamera(1); - this._navigation?.setLimits(CURRENT_SCENE.navigationLimits); - this._navigation?.setViewCenter( - this._world?.projectedSceneConfig.center - ); - this._navigation?.setTargetPosition( - this._camera?.currentCamera.userData.lookAt - ); - this._navigation?.setPositionInSphere( - this._camera?.currentCamera.position - ); - }, `-=${this._config.glassEffectDefault.duration}`); + this._triggerTransitionEffect() + .add(() => { + this._camera?.switchCamera(1); + this._navigation?.setLimits(CURRENT_SCENE.navigationLimits); + this._navigation?.setTargetPosition( + this._camera?.currentCamera.userData.lookAt + ); + this._navigation?.setPositionInSphere( + this._camera?.currentCamera.position + ); + }, `-=${this._transitionEffectDefault.duration}`) + .add(() => { + PREV_SCENE?.outro(); + updateCameraToCurrentScene(); + }); }, "<87%"); - } - this._changeProjectedScene(this._experience.router.currentRouteKey); - this._config.prevSceneKey = this._experience.router?.currentRouteKey; + if (!IS_SWITCHING_MAIN && !IS_SWITCHING_PROJECTED) + updateCameraToCurrentScene(); + + if (this._prevProjectedSceneKey === this._router?.currentRouteKey) + this._prevProjectedSceneKey = undefined; + this._prevSceneKey = this._router?.currentRouteKey; } - public construct() { + public async construct() { this._world = this._experience.world; - this._initScenes(); - this._experience.router?.on(events.CHANGED, () => this._setScene()); + + if (this._supportedPageKeys.length < 2) + throw new Error("Unable to display the projected scene.", { + cause: errors.WRONG_PARAM, + }); + + if (!this._camera?.cameras || this._camera?.cameras.length < 2) + throw new Error("No enough camera found.", { + cause: errors.WRONG_PARAM, + }); + + if (!this._world) + throw new Error("World not initialized.", { + cause: errors.WRONG_PARAM, + }); + + // INTRO + this._prevProjectedSceneKey = pages.SKILL_PAGE; + this._world.availablePageScenes[this._prevProjectedSceneKey].intro(); + + if ( + this._world.scene1?.pcScreen && + this._world.scene1.pcScreenWebglTexture && + this._world.scene1.pcScreenProjectedCamera + ) { + this._world.availablePageScenes[ + this._prevProjectedSceneKey ?? "" + ]?.cameraPath?.getPoint( + 0, + this._world.scene1.pcScreenProjectedCamera.position + ); + this._world.scene1.pcScreenProjectedCamera.lookAt( + this._world.availablePageScenes[this._prevProjectedSceneKey].center + ); + this._world.scene1.pcScreenProjectedCamera.userData.lookAt = + this._world.availablePageScenes[this._prevProjectedSceneKey].center; + } + + await this._world.availablePageScenes[this._world.mainSceneKey].intro(); + + this._setScene(); + this._router?.on(events.CHANGED, () => this._setScene()); this.emit(events.CONSTRUCTED, this); } diff --git a/src/experiences/home/world/model.ts b/src/experiences/home/world/model.ts index b78fc8b..e371720 100644 --- a/src/experiences/home/world/model.ts +++ b/src/experiences/home/world/model.ts @@ -1,7 +1,6 @@ -import { CatmullRomCurve3, Vector3 } from "three"; +import { Vector3 } from "three"; export interface SceneConfig { position: Vector3; center: Vector3; - cameraPath: CatmullRomCurve3; } diff --git a/src/experiences/home/world/scene-1.component.ts b/src/experiences/home/world/scene-1.component.ts index 91c5bd2..1ba46bf 100644 --- a/src/experiences/home/world/scene-1.component.ts +++ b/src/experiences/home/world/scene-1.component.ts @@ -1,5 +1,4 @@ import { - AnimationMixer, BackSide, Color, Material, @@ -12,6 +11,9 @@ import { type Object3DEventMap, VideoTexture, LinearSRGBColorSpace, + CatmullRomCurve3, + PerspectiveCamera, + Vector3, } from "three"; import gsap from "gsap"; @@ -31,9 +33,6 @@ import { Config } from "~/config"; // STATICS import { DESTRUCTED } from "~/static/event.static"; -// ERROR -import { ErrorFactory } from "~/errors"; - // MODELS import type { Materials, @@ -48,7 +47,7 @@ import phone_screen_recording from "~/assets/videos/phone_screen_recording.webm" export class Scene1Component extends SceneComponentBlueprint { private _renderer = this._experience.renderer; private _appTime = this._experience.app.time; - private _mixer?: AnimationMixer; + private _camera = this._experience.camera; private _initialPcTopArticulation?: Object3D; public readonly timeline = gsap.timeline(); @@ -76,59 +75,69 @@ export class Scene1Component extends SceneComponentBlueprint { enabled: true, }, }; + public readonly cameraPath = new CatmullRomCurve3([ + new Vector3(0, 5.5, 21), + new Vector3(12, 10, 12), + new Vector3(21, 5.5, 0), + new Vector3(12, 3.7, 12), + new Vector3(0, 5.5, 21), + ]); - public pcScreenWebglTexture = new WebGLRenderTarget(1024, 1024); public pcTopArticulation?: Object3D; - public treeOutside?: Object3D; + public pcScreenProjectedCamera = + this._camera?.cameras[1] ?? new PerspectiveCamera(); + public pcScreenWebglTexture = new WebGLRenderTarget(1024, 1024); public pcScreen?: Mesh; public phoneScreen?: Mesh; - public coffeeSteam?: Mesh; public phoneScreenVideo?: HTMLVideoElement; + public treeOutside?: Object3D; + public coffeeSteam?: Mesh; public monitorAScreenVideo?: HTMLVideoElement; public monitorBScreenVideo?: HTMLVideoElement; constructor() { - try { - super({ - modelName: "scene_1", - childrenMaterials: { - scene_1_room: "room", - chair_top: "room", - pc_top: "room", - scene_1_woods: "wood", - coffee_steam: "coffee_steam", - window_glasses: "glass", - scene_1_floor: "scene_container", - monitor_a_screen: "monitor_a", - monitor_b_screen: "monitor_b", - scene_1_phone_screen: "phone_screen", - pc_top_screen: "pc_screen", - tree: "tree", - ...(() => { - const _RESULTS: ModelChildrenMaterials = {}; - const _KEYS = ["top", "front", "back", "left", "right"]; - for (let i = 0; i < _KEYS.length; i++) { - _KEYS[i]; - _RESULTS[`tree_cube_${_KEYS[i]}`] = "tree"; - _RESULTS[`tree_cube_${_KEYS[i]}_clone`] = "tree_outside"; - } - - return _RESULTS; - })(), - }, - onTraverseModelScene: (child: Object3D) => { - this._setPcTopBone(child); - this._setPcScreen(child); - this._setCoffeeSteam(child); - this._setPhoneScreen(child); - this._setTreeOutSide(child); - }, - }); + super({ + modelName: "scene_1", + childrenMaterials: { + scene_1_room: "room", + chair_top: "room", + pc_top: "room", + scene_1_woods: "wood", + coffee_steam: "coffee_steam", + window_glasses: "glass", + scene_1_floor: "scene_container", + monitor_a_screen: "monitor_a", + monitor_b_screen: "monitor_b", + scene_1_phone_screen: "phone_screen", + pc_top_screen: "pc_screen", + tree: "tree", + ...(() => { + const _RESULTS: ModelChildrenMaterials = {}; + const _KEYS = ["top", "front", "back", "left", "right"]; + for (let i = 0; i < _KEYS.length; i++) { + _KEYS[i]; + _RESULTS[`tree_cube_${_KEYS[i]}`] = "tree"; + _RESULTS[`tree_cube_${_KEYS[i]}_clone`] = "tree_outside"; + } + + return _RESULTS; + })(), + }, + onTraverseModelScene: (child: Object3D) => { + this._setPcTopBone(child); + this._setPcScreen(child); + this._setCoffeeSteam(child); + this._setPhoneScreen(child); + this._setTreeOutSide(child); + }, + }); + } - this; - } catch (_err) { - throw new ErrorFactory(_err); - } + public get isPcOpen() { + return ( + this.pcTopArticulation?.rotation.z !== + this._initialPcTopArticulation?.rotation.z + ); } private _setPcTopBone(item: Object3D) { @@ -240,6 +249,14 @@ export class Scene1Component extends SceneComponentBlueprint { ); MONITOR_B_VIDEO_TEXTURE.colorSpace = LinearSRGBColorSpace; + // MATERIALS + if (this._world?.commonMaterials.scene_container) + AVAILABLE_MATERIALS.scene_container = + this._world?.commonMaterials.scene_container; + + if (this._world?.commonMaterials.scene_container) + AVAILABLE_MATERIALS.glass = this._world?.commonMaterials.glass; + AVAILABLE_MATERIALS.pc_screen = new MeshBasicMaterial({ map: this.pcScreenWebglTexture?.texture, }); @@ -346,74 +363,60 @@ export class Scene1Component extends SceneComponentBlueprint { this.modelScene?.clear(); this.modelScene?.removeFromParent(); this._renderer?.removePortalAssets(`${Scene1Component.name}_screen_pc`); - this._mixer?.stopAllAction(); - this._mixer = undefined; this.emit(DESTRUCTED); }, }); } - // public intro(): void { - // const WorldManager = this._world?.manager; - - // if ( - // !(WorldManager && this._appCamera.instance instanceof PerspectiveCamera) - // ) - // return; - - // const { x, y, z } = this.cameraPath.getPointAt(0); - - // gsap.to(this._appCamera.instance.position, { - // ...this._world?.manager?.getGsapDefaultProps(), - // duration: Config.GSAP_ANIMATION_DURATION, - // ease: Config.GSAP_ANIMATION_EASE, - // x, - // y, - // z, - // delay: Config.GSAP_ANIMATION_DURATION * 0.8, - // onUpdate: () => { - // // this._camera?.setCameraLookAt(WorldManager.initialLookAtPosition); - // }, - // onComplete: () => { - // setTimeout(() => { - // if (this._world?.manager) { - // WorldManager?.getGsapDefaultProps().onComplete(); - - // this._world.manager.autoCameraAnimation = true; - // } - // }, 1000); - // }, - // }); - // } + public intro() { + if (!(this?.modelScene && this.pcScreen && this.pcScreenWebglTexture)) + return; + + this._renderer?.addPortalAssets(`${Scene1Component.name}_pc_screen`, { + mesh: this.pcScreen, + meshCamera: this.pcScreenProjectedCamera, + meshWebGLTexture: this.pcScreenWebglTexture, + }); + + if (!this.isPcOpen) return this.togglePcOpening(1); + + return this.timeline; + } + + public outro() { + this._renderer?.removePortalAssets(`${Scene1Component.name}_pc_screen`); + + return this.timeline; + } /** * Toggle the state of the pc between open and close * - * @param state Force the state of the pc (0 = "close" & 1 = "open") + * @param forceState Force the state of the pc (0 = "close" & 1 = "open") * @returns */ - public togglePcOpening(state?: 0 | 1) { - if (!this._model || !this.modelScene || !this.pcTopArticulation) return; - const isOpen = - typeof state === "number" - ? state === 1 - : this.pcTopArticulation.rotation.z !== - this._initialPcTopArticulation?.rotation.z; + public togglePcOpening(force?: 0 | 1) { + if (!this._model || !this.modelScene || !this.pcTopArticulation) + return this.timeline; + + const isOpen = typeof force === "number" ? force !== 1 : this.isPcOpen; const _NEXT_VALUE = isOpen ? this._initialPcTopArticulation?.rotation.z ?? 0 - : this.pcTopArticulation.rotation.z + 2.1; - - return this.timeline.to(this.pcTopArticulation.rotation, { - z: _NEXT_VALUE, - duration: Config.GSAP_ANIMATION_DURATION, - onUpdate: () => {}, - onComplete: () => { - if (this.pcTopArticulation?.rotation) - this.pcTopArticulation.rotation.z = Number(_NEXT_VALUE); - }, - }); + : (this._initialPcTopArticulation?.rotation.z ?? 0) + 2.1; + + return this.pcTopArticulation.rotation.z === _NEXT_VALUE + ? this.timeline + : this.timeline.to(this.pcTopArticulation.rotation, { + z: _NEXT_VALUE, + duration: Config.GSAP_ANIMATION_DURATION, + onUpdate: () => {}, + onComplete: () => { + if (this.pcTopArticulation?.rotation) + this.pcTopArticulation.rotation.z = Number(_NEXT_VALUE); + }, + }); } public update(): void { diff --git a/src/experiences/home/world/scene-2.component.ts b/src/experiences/home/world/scene-2.component.ts index 6e8f873..2c22ac3 100644 --- a/src/experiences/home/world/scene-2.component.ts +++ b/src/experiences/home/world/scene-2.component.ts @@ -1,10 +1,12 @@ -import { MeshBasicMaterial } from "three"; +import { CatmullRomCurve3, Material, Mesh, MeshBasicMaterial, Vector3 } from "three"; +import { gsap } from "gsap"; // BLUEPRINTS import { SceneComponentBlueprint } from "~/blueprints/experiences/scene-component.blueprint"; // MODELS import type { Materials } from "~/common/experiences/experience-world.model"; +import { Config } from "~/config"; export class Scene2Component extends SceneComponentBlueprint { public readonly navigationLimits = { @@ -23,6 +25,14 @@ export class Scene2Component extends SceneComponentBlueprint { enabled: true, }, }; + public cameraPath = new CatmullRomCurve3([ + new Vector3(0, 5.5, 21), + new Vector3(12, 10, 12), + new Vector3(21, 5.5, 0), + new Vector3(12, 3.7, 12), + new Vector3(0, 5.5, 21), + ]); + public timeline = gsap.timeline(); constructor() { try { @@ -54,4 +64,47 @@ export class Scene2Component extends SceneComponentBlueprint { return availableMaterials; } + + public intro() { + if (!this.modelScene) return this.timeline; + this.modelScene.renderOrder = 1; + + const _PARAMS = { alphaTest: 0 }; + const _MAT_KEYS = Object.keys(this._availableMaterials); + + return this.timeline?.to(_PARAMS, { + alphaTest: 1, + duration: Config.GSAP_ANIMATION_DURATION, + onStart: () => { + // if (!this._navigation?.timeline.isActive()) + // this._navigation?.setLimits(this.navigationLimits); + }, + onUpdate: () => { + for (const key of _MAT_KEYS) + this._availableMaterials[key].alphaTest = 1 - _PARAMS.alphaTest; + }, + }); + } + + public outro() { + if (!this.modelScene) return this.timeline; + + this.modelScene.renderOrder = 2; + + const _PARAMS = { alphaTest: 0 }; + const _MAT_KEYS = Object.keys(this._availableMaterials); + + return this.timeline?.to(_PARAMS, { + alphaTest: 1, + duration: Config.GSAP_ANIMATION_DURATION, + onStart: () => { + // if (!this._navigation?.timeline.isActive()) + // this._navigation?.setLimits(this.navigationLimits); + }, + onUpdate: () => { + for (const key of _MAT_KEYS) + this._availableMaterials[key].alphaTest = _PARAMS.alphaTest; + }, + }); + } } diff --git a/src/experiences/home/world/scene-3.component.ts b/src/experiences/home/world/scene-3.component.ts index 2146f49..823e765 100644 --- a/src/experiences/home/world/scene-3.component.ts +++ b/src/experiences/home/world/scene-3.component.ts @@ -6,8 +6,10 @@ import { Mesh, ShaderMaterial, DoubleSide, + CatmullRomCurve3, + Vector3, } from "three"; -import gsap from "gsap"; +import { gsap } from "gsap"; import { HtmlMixerPlane } from "threex.htmlmixer-continued/lib/html-mixer"; // CONFIG @@ -63,6 +65,13 @@ export class Scene3Component extends SceneComponentBlueprint { enabled: true, }, }; + public cameraPath = new CatmullRomCurve3([ + new Vector3(0, 5.5, 21), + new Vector3(12, 10, 12), + new Vector3(21, 5.5, 0), + new Vector3(12, 3.7, 12), + new Vector3(0, 5.5, 21), + ]); public pcTopArticulation?: Object3D; public pcScreen?: Mesh; @@ -81,6 +90,7 @@ export class Scene3Component extends SceneComponentBlueprint { pc_top_2: "scene_3", "eye-glass_glass": "glass", scene_3_floor: "scene_container", + pc_top_screen_2: "glass", phone_screen_2: "phone_screen", watch_screen: "watch_screen", gamepad_led: "gamepad_led", @@ -95,6 +105,13 @@ export class Scene3Component extends SceneComponentBlueprint { }); } + public get isPcOpen() { + return ( + this.pcTopArticulation?.rotation.z !== + this._initialPcTopArticulation?.rotation.z + ); + } + private _setPcTopArticulation(item: Object3D) { if (!(item instanceof Object3D) || item.name !== "pc_top_articulation_2") return; @@ -133,6 +150,7 @@ export class Scene3Component extends SceneComponentBlueprint { if (!AVAILABLE_TEXTURE) return AVAILABLE_MATERIALS; + // MATERIALS if (this._world?.commonMaterials.scene_container) { AVAILABLE_MATERIALS.scene_container = this._world?.commonMaterials.scene_container.clone(); @@ -140,8 +158,13 @@ export class Scene3Component extends SceneComponentBlueprint { AVAILABLE_MATERIALS.scene_container.depthWrite = false; } - if (this._world?.commonMaterials.glass) + if (this._world?.commonMaterials.glass) { AVAILABLE_MATERIALS.glass = this._world?.commonMaterials.glass.clone(); + if (AVAILABLE_MATERIALS.glass instanceof MeshBasicMaterial) { + AVAILABLE_MATERIALS.glass.alphaTest = 1; + AVAILABLE_MATERIALS.glass.alphaMap = AVAILABLE_TEXTURE.cloudAlphaMap; + } + } AVAILABLE_MATERIALS.scene_3 = new MeshBasicMaterial({ alphaMap: AVAILABLE_TEXTURE.cloudAlphaMap, @@ -189,41 +212,35 @@ export class Scene3Component extends SceneComponentBlueprint { /** * Toggle the state of the pc between open and close * - * @param state Force the state of the pc (0 = "close" & 1 = "open") + * @param forceState Force the state of the pc (0 = "close" & 1 = "open") * @returns */ - public togglePcOpening(state?: 0 | 1) { - if (!this._model || !this.modelScene || !this.pcTopArticulation) return; - const isOpen = - typeof state === "number" - ? state === 1 - : this.pcTopArticulation.rotation.z !== - this._initialPcTopArticulation?.rotation.z; + public togglePcOpening(force?: 0 | 1) { + if (!this._model || !this.modelScene || !this.pcTopArticulation) + return this.timeline; + + const isOpen = typeof force === "number" ? force !== 1 : this.isPcOpen; const _NEXT_VALUE = isOpen ? this._initialPcTopArticulation?.rotation.z ?? 0 - : this.pcTopArticulation.rotation.z + 1.7; - - return this.timeline.to(this.pcTopArticulation.rotation, { - z: _NEXT_VALUE, - duration: Config.GSAP_ANIMATION_DURATION, - onUpdate: () => {}, - onComplete: () => { - if (this.pcTopArticulation?.rotation) - this.pcTopArticulation.rotation.z = Number(_NEXT_VALUE); - }, - }); + : (this._initialPcTopArticulation?.rotation.z ?? 0) + 1.7; + + return this.pcTopArticulation.rotation.z === _NEXT_VALUE + ? this.timeline + : this.timeline.to(this.pcTopArticulation.rotation, { + z: _NEXT_VALUE, + duration: Config.GSAP_ANIMATION_DURATION, + onUpdate: () => {}, + onComplete: () => { + if (this.pcTopArticulation?.rotation) + this.pcTopArticulation.rotation.z = Number(_NEXT_VALUE); + }, + }); } public construct(): void { super.construct(); - ~(() => { - if (!this._renderer) return; - - this._renderer.enableCssRender = true; - })(); - ~(() => { if ( !this._renderer?.mixerContext || @@ -254,12 +271,83 @@ export class Scene3Component extends SceneComponentBlueprint { this.pcTopArticulation.add(this._pcScreenMixerPlane.object3d); })(); + + ~(() => { + const _MAT_KEYS = Object.keys(this._availableMaterials).slice(3); + + if (this._pcScreenMixerPlane) + this._pcScreenMixerPlane.object3d.visible = false; + for (const key of _MAT_KEYS) + this._availableMaterials[key].visible = false; + })(); } - public intro(): void { - this.togglePcOpening(); + public intro() { + if (!this.modelScene) return this.timeline; + + this.modelScene.renderOrder = 1; + + const _PARAMS = { alphaTest: 0 }; + const _MAT_KEYS = Object.keys(this._availableMaterials); + const _ALPHA_MAT_KEYS = _MAT_KEYS.slice(0, 3); + const _OTHER_MAT_KEYS = _MAT_KEYS.slice(3); + + return this.togglePcOpening(1) + ?.add( + gsap.to(_PARAMS, { + alphaTest: 1, + duration: Config.GSAP_ANIMATION_DURATION, + onStart: () => { + // if (!this._navigation?.timeline.isActive()) + // this._navigation?.setLimits(this.navigationLimits); + }, + onUpdate: () => { + for (const key of _ALPHA_MAT_KEYS) + this._availableMaterials[key].alphaTest = 1 - _PARAMS.alphaTest; + }, + }), + "<" + ) + .add(() => { + if (this._renderer) this._renderer.enableCssRender = true; + if (this._pcScreenMixerPlane) + this._pcScreenMixerPlane.object3d.visible = true; + for (const key of _OTHER_MAT_KEYS) + this._availableMaterials[key].visible = true; + }, "<40%"); } + public outro() { + if (!this.modelScene) return this.timeline; + + this.modelScene.renderOrder = 2; + + const _PARAMS = { alphaTest: 0 }; + const _MAT_KEYS = Object.keys(this._availableMaterials); + const _ALPHA_MAT_KEYS = _MAT_KEYS.slice(0, 3); + const _OTHER_MAT_KEYS = _MAT_KEYS.slice(3); + + return this.togglePcOpening(0)?.add( + gsap.to(_PARAMS, { + alphaTest: 1, + duration: Config.GSAP_ANIMATION_DURATION, + onStart: () => { + if (this._renderer) this._renderer.enableCssRender = false; + if (this._pcScreenMixerPlane) + this._pcScreenMixerPlane.object3d.visible = false; + for (const key of _OTHER_MAT_KEYS) + this._availableMaterials[key].visible = false; + // if (!this._navigation?.timeline.isActive()) + // this._navigation?.setLimits(this.navigationLimits); + }, + onUpdate: () => { + for (const key of _ALPHA_MAT_KEYS) + this._availableMaterials[key].alphaTest = _PARAMS.alphaTest; + }, + }), + "<" + ); + } public update() { this._uTime = this._appTime.elapsed * 0.001; this._uTimestamps = this._currentDayTimestamp * 0.001 + this._uTime; diff --git a/src/experiences/home/world/scene-container.component.ts b/src/experiences/home/world/scene-container.component.ts index 68d8aac..f995853 100644 --- a/src/experiences/home/world/scene-container.component.ts +++ b/src/experiences/home/world/scene-container.component.ts @@ -1,9 +1,10 @@ // EXPERIENCES import { SceneComponentBlueprint } from "~/blueprints/experiences/scene-component.blueprint"; -export class SceneContainerComponent extends SceneComponentBlueprint { - public readonly navigationLimits = undefined; +// COMMONS +import type { Materials } from "~/common/experiences/experience-world.model"; +export class SceneContainerComponent extends SceneComponentBlueprint { constructor() { try { super({ @@ -16,6 +17,13 @@ export class SceneContainerComponent extends SceneComponentBlueprint { } protected _getAvailableMaterials() { - return {}; + const AVAILABLE_MATERIALS: Materials = {}; + + // MATERIALS + if (this._world?.commonMaterials.scene_container) + AVAILABLE_MATERIALS.scene_container = + this._world?.commonMaterials.scene_container; + + return AVAILABLE_MATERIALS; } }