Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup Earth Close-up view and implement animation between it and Orbit view #13

Merged
merged 1 commit into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/grasp-seasons/3d-models/earth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/grasp-seasons/3d-views/base-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default class BaseView {
}
}

registerInteractionHandler(handler: BaseInteraction) {
registerInteractionHandler(handler: BaseInteraction | null) {
this._interactionHandler = handler;
}

Expand Down
102 changes: 98 additions & 4 deletions src/grasp-seasons/3d-views/orbit-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 14 additions & 1 deletion src/grasp-seasons/components/seasons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,32 @@ body {
width: 378px;
height: 374px;
position: relative;
color: #fff;

.playback-controls {
position: absolute;
bottom: 10px;
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 {
Expand Down
23 changes: 21 additions & 2 deletions src/grasp-seasons/components/seasons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -83,7 +89,7 @@ const Seasons: React.FC<IProps> = ({ 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<HTMLInputElement>) => {
Expand Down Expand Up @@ -160,6 +166,11 @@ const Seasons: React.FC<IProps> = ({ lang = "en_us", initialState = {}, log = (a
});
};

const handleViewChange = (event: ChangeEvent<HTMLSelectElement>) => {
const earthCloseUpView = event.target.value === "true";
setSimState(prevState => ({ ...prevState, earthCloseUpView }));
}

const solarIntensityValue = getSolarNoonIntensity(simState.day, simState.lat).toFixed(2);

return (
Expand All @@ -169,6 +180,14 @@ const Seasons: React.FC<IProps> = ({ lang = "en_us", initialState = {}, log = (a
<OrbitViewComp
ref={orbitViewRef} simulation={simState} onSimStateChange={handleSimStateChange} log={log} showCamera={false}
/>
<div className="view-type-dropdown">
<label>{ t("~VIEW", simLang) }
<select value={simState.earthCloseUpView.toString()} onChange={handleViewChange}>
<option value="false">{ t("~EARTH_ORBIT", simLang) }</option>
<option value="true">{ t("~EARTH_CLOSE_UP", simLang) }</option>
</select>
</label>
</div>
<div className="playback-controls">
<button
className="btn btn-default animation-btn"
Expand Down
5 changes: 5 additions & 0 deletions src/grasp-seasons/translation/en-us.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,9 @@ export default {
"~SOLAR_INTENSITY": "Solar Intensity",
"~SOLAR_INTENSITY_UNIT": "Watts/m²",
"~MY_LOCATIONS": "My Locations",
"~VIEW": "View",
"~EARTH_ORBIT": "Earth Orbit",
"~EARTH_CLOSE_UP": "Earth Close-up",
"~TILT_VIEW": "Tilt View",
"~ORBIT_BUTTON": "Orbit",
};
5 changes: 5 additions & 0 deletions src/grasp-seasons/translation/es-es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,9 @@ export default {
"~SOLAR_INTENSITY": "Intensidad solar",
"~SOLAR_INTENSITY_UNIT": "Vatios/m²",
"~MY_LOCATIONS": "Mis localizaciones",
"~VIEW": "Vista",
"~EARTH_ORBIT": "Órbita de la Tierra",
"~EARTH_CLOSE_UP": "Acercamiento a la Tierra",
"~TILT_VIEW": "Vista inclinada",
"~ORBIT_BUTTON": "Órbita",
};
8 changes: 7 additions & 1 deletion src/grasp-seasons/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ export interface ISimState {
sunrayDistMarker: boolean;
dailyRotation: boolean;
earthGridlines: boolean;
showCamera: boolean;
lang: Language;
// -- Day Length Plugin extra state ---
// It could be ported back to GRASP Seasons too to handle camera model cleaner way.
showCamera: boolean;
// 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: boolean;
}

export type ModelType = "earth-view" | "orbit-view" | "unknown";
Expand Down
Loading