From 1d6a3e15699992b89c6e152275ea425723e2a8cb Mon Sep 17 00:00:00 2001 From: pjanik Date: Wed, 7 Aug 2024 21:24:53 +0200 Subject: [PATCH] feat: setup Earth Close-up view and implement animation between it and Orbit view [PT-188059349] [PT-188059181] --- src/grasp-seasons/3d-models/earth.ts | 9 +- src/grasp-seasons/3d-views/base-view.ts | 2 +- src/grasp-seasons/3d-views/orbit-view.ts | 102 +++++++++++++++++++++- src/grasp-seasons/components/seasons.scss | 15 +++- src/grasp-seasons/components/seasons.tsx | 23 ++++- src/grasp-seasons/translation/en-us.ts | 5 ++ src/grasp-seasons/translation/es-es.ts | 5 ++ src/grasp-seasons/types.ts | 8 +- 8 files changed, 154 insertions(+), 15 deletions(-) diff --git a/src/grasp-seasons/3d-models/earth.ts b/src/grasp-seasons/3d-models/earth.ts index 3ab43be..21e783d 100644 --- a/src/grasp-seasons/3d-models/earth.ts +++ b/src/grasp-seasons/3d-models/earth.ts @@ -3,7 +3,6 @@ import * as data from "../utils/solar-system-data"; import * as c from "./constants"; import earthLargeImg from "../assets/earth-2k.jpg";//'../assets/earth-grid-2k.jpg'; import earthLargeGridImg from "../assets/earth-grid-2k.jpg"; -import earthSimpleImg from "../assets/earth-equator-0.5k.jpg";//'../assets/earth-0.5k.jpg'; import earthBumpImg from "../assets/earth-bump-2k.jpg"; import { IModelParams } from "../types"; // import earthSpecularImg from '../assets/earth-specular-2k.png'; @@ -24,11 +23,9 @@ export default class Earth { const geometry = new THREE.SphereGeometry(RADIUS, 64, 64); this._material = new THREE.MeshPhongMaterial(COLORS); const textureLoader = new THREE.TextureLoader(); - this._material.map = textureLoader.load(simple ? earthSimpleImg : earthLargeImg); - if (!simple) { - this._material.bumpMap = textureLoader.load(earthBumpImg); - this._material.bumpScale = 100000 * c.SF; - } + this._material.map = textureLoader.load(earthLargeImg); + this._material.bumpMap = textureLoader.load(earthBumpImg); + this._material.bumpScale = 100000 * c.SF; this._earthObject = new THREE.Mesh(geometry, this._material); this._orbitRotationObject = new THREE.Object3D(); this._orbitRotationObject.add(this._earthObject); diff --git a/src/grasp-seasons/3d-views/base-view.ts b/src/grasp-seasons/3d-views/base-view.ts index 72881bd..2fd5722 100644 --- a/src/grasp-seasons/3d-views/base-view.ts +++ b/src/grasp-seasons/3d-views/base-view.ts @@ -137,7 +137,7 @@ export default class BaseView { } } - registerInteractionHandler(handler: BaseInteraction) { + registerInteractionHandler(handler: BaseInteraction | null) { this._interactionHandler = handler; } diff --git a/src/grasp-seasons/3d-views/orbit-view.ts b/src/grasp-seasons/3d-views/orbit-view.ts index cd5d140..ac074ff 100644 --- a/src/grasp-seasons/3d-views/orbit-view.ts +++ b/src/grasp-seasons/3d-views/orbit-view.ts @@ -13,14 +13,39 @@ const DEF_PROPERTIES = { sunEarthLine: true }; +const CAMERA_TWEEN_LENGTH = 1000; + +// Cubic Bezier function +function cubicBezier(t: number, p0: number, p1: number, p2: number, p3: number): number { + const oneMinusT = 1 - t; + return Math.pow(oneMinusT, 3) * p0 + + 3 * Math.pow(oneMinusT, 2) * t * p1 + + 3 * oneMinusT * Math.pow(t, 2) * p2 + + Math.pow(t, 3) * p3; +} + +// Ease-in-out function +function easeInOut(t: number): number { + const p0 = 0, p1 = 0.25, p2 = 0.75, p3 = 1; + return cubicBezier(t, p0, p1, p2, p3); +} + export default class OrbitView extends BaseView { cameraSymbol!: THREE.Object3D; latLine!: LatitudeLine; latLongMarker!: LatLongMarker; monthLabels!: THREE.Object3D[]; + earthDraggingInteraction: EarthDraggingInteraction = new EarthDraggingInteraction(this); + + startingCameraPos?: THREE.Vector3; + desiredCameraPos?: THREE.Vector3; + startingCameraLookAt?: THREE.Vector3; + desiredCameraLookAt?: THREE.Vector3; + cameraSwitchTimestamp?: number; + constructor(parentEl: HTMLElement, props = DEF_PROPERTIES) { super(parentEl, props, "orbit-view"); - this.registerInteractionHandler(new EarthDraggingInteraction(this)); + this.registerInteractionHandler(this.earthDraggingInteraction); } setViewAxis(vec3: THREE.Vector3) { @@ -55,10 +80,50 @@ export default class OrbitView extends BaseView { }; } + getCloseUpCameraPosition() { + const cameraOffset = new THREE.Vector3(0, 0, 40000000 / data.SCALE_FACTOR); + return this.earth.posObject.position.clone().add(cameraOffset); + } + + setupEarthCloseUpView() { + this.registerInteractionHandler(null); // disable dragging interaction in close-up view + this.sunEarthLine.rootObject.visible = false; + this.monthLabels.forEach((label) => label.visible = false); + } + + setupOrbitView() { + this.registerInteractionHandler(this.earthDraggingInteraction); + this.sunEarthLine.rootObject.visible = true; + this.monthLabels.forEach((label) => label.visible = true); + } + + render(timestamp: number) { + super.render(timestamp); + + if (this.desiredCameraPos && this.startingCameraPos && this.desiredCameraLookAt && this.startingCameraLookAt && + this.cameraSwitchTimestamp !== undefined + ) { + const progress = Math.max(0, Math.min(1, (Date.now() - this.cameraSwitchTimestamp) / CAMERA_TWEEN_LENGTH)); + const progressEased = easeInOut(progress); + + this.camera.position.lerpVectors(this.startingCameraPos, this.desiredCameraPos, progressEased); + this.controls.target.lerpVectors(this.startingCameraLookAt, this.desiredCameraLookAt, progressEased); + if (progress === 1) { + this.startingCameraPos = undefined; + this.desiredCameraPos = undefined; + this.startingCameraLookAt = undefined; + this.desiredCameraLookAt = undefined; + this.cameraSwitchTimestamp = undefined; + } + } + } + + getInitialCameraPosition() { + return new THREE.Vector3(0, 360000000 / data.SCALE_FACTOR, 0); + } + _setInitialCamPos() { - this.camera.position.x = 0; - this.camera.position.y = 360000000 / data.SCALE_FACTOR; - this.camera.position.z = 0; + this.camera.position.copy(this.getInitialCameraPosition()); } toggleCameraModel(show: boolean) { @@ -91,6 +156,35 @@ export default class OrbitView extends BaseView { this.latLongMarker.setLatLong(this.props.lat, this.props.long); } + _updateEarthCloseUpView() { + this.startingCameraPos = this.camera.position.clone(); + this.startingCameraLookAt = this.controls.target.clone(); + this.cameraSwitchTimestamp = Date.now(); + + if (this.props.earthCloseUpView) { + this.desiredCameraPos = this.getCloseUpCameraPosition(); + this.desiredCameraLookAt = this.earth.posObject.position.clone(); + this.setupEarthCloseUpView(); + } else { + this.desiredCameraPos = this.getInitialCameraPosition(); + this.desiredCameraLookAt = new THREE.Vector3(0, 0, 0); + this.setupOrbitView(); + } + } + + _updateDay(): void { + super._updateDay(); + if (this.props.earthCloseUpView) { + if (this.desiredCameraPos && this.desiredCameraLookAt) { + this.desiredCameraPos.copy(this.getCloseUpCameraPosition()); + this.desiredCameraLookAt.copy(this.earth.posObject.position); + } else { + this.camera.position.copy(this.getCloseUpCameraPosition()); + this.controls.target.copy(this.earth.posObject.position); + } + } + } + _initScene() { super._initScene(); this.latLine = new LatitudeLine(false, true); diff --git a/src/grasp-seasons/components/seasons.scss b/src/grasp-seasons/components/seasons.scss index f07a52b..5693cb5 100644 --- a/src/grasp-seasons/components/seasons.scss +++ b/src/grasp-seasons/components/seasons.scss @@ -35,6 +35,7 @@ body { width: 378px; height: 374px; position: relative; + color: #fff; .playback-controls { position: absolute; @@ -42,12 +43,24 @@ body { left: 10px; display: flex; flex-direction: row; - color: #fff; label { font-weight: normal; } } + + .view-type-dropdown { + position: absolute; + top: 10px; + display: flex; + justify-content: center; + width: 100%; + + select { + margin-left: 7px; + width: 150px; + } + } } .ground-view-label { diff --git a/src/grasp-seasons/components/seasons.tsx b/src/grasp-seasons/components/seasons.tsx index 7fedd12..569bf05 100644 --- a/src/grasp-seasons/components/seasons.tsx +++ b/src/grasp-seasons/components/seasons.tsx @@ -27,8 +27,14 @@ const DEFAULT_SIM_STATE: ISimState = { sunrayDistMarker: false, dailyRotation: false, earthGridlines: false, + lang: "en_us", + // -- Day Length Plugin extra state --- + // It could be ported back to GRASP Seasons too to handle camera model cleaner way. showCamera: false, - lang: "en_us" + // A new type of view where the camera is locked on Earth. It is different from GRASP Seasons Earth View because the + // camera follows Earth's position but does not rotate. As the year passes, we'll see different parts of Earth, + // including its night side. This is useful for keeping the Earth's axis constant. + earthCloseUpView: false, }; function capitalize(string: string) { @@ -83,7 +89,7 @@ const Seasons: React.FC = ({ lang = "en_us", initialState = {}, log = (a // Derived state const simLang = simState.lang; - const playStopLabel = mainAnimationStarted ? t("~STOP", simLang) : t("~PLAY", simLang); + const playStopLabel = mainAnimationStarted ? t("~STOP", simLang) : t("~ORBIT_BUTTON", simLang); // Log helpers const logCheckboxChange = (event: ChangeEvent) => { @@ -160,6 +166,11 @@ const Seasons: React.FC = ({ lang = "en_us", initialState = {}, log = (a }); }; + const handleViewChange = (event: ChangeEvent) => { + const earthCloseUpView = event.target.value === "true"; + setSimState(prevState => ({ ...prevState, earthCloseUpView })); + } + const solarIntensityValue = getSolarNoonIntensity(simState.day, simState.lat).toFixed(2); return ( @@ -169,6 +180,14 @@ const Seasons: React.FC = ({ lang = "en_us", initialState = {}, log = (a +
+ +