diff --git a/src/components/App.tsx b/src/components/App.tsx index 1f73ef2..bdea506 100755 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -11,7 +11,7 @@ import { Header } from "./header"; import "../assets/scss/App.scss"; export const App: React.FC = () => { - const [activeTab, setActiveTab] = useState<"location" | "simulation">("location"); + const [activeTab, setActiveTab] = useState<"location" | "simulation">("simulation"); const [latitude, setLatitude] = useState(""); const [longitude, setLongitude] = useState(""); const [dayOfYear, setDayOfYear] = useState("280"); diff --git a/src/grasp-seasons/3d-models/common-models.ts b/src/grasp-seasons/3d-models/common-models.ts index 94f62cc..861dd1a 100644 --- a/src/grasp-seasons/3d-models/common-models.ts +++ b/src/grasp-seasons/3d-models/common-models.ts @@ -68,7 +68,7 @@ export default { sun (params: IModelParams) { const texture = new THREE.TextureLoader().load(SunPNG); - const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false }); + const material = new THREE.SpriteMaterial({ map: texture, transparent: true }); const sprite = new THREE.Sprite(material); sprite.renderOrder = 1; sprite.scale.set(100000000 * c.SF, 100000000 * c.SF, 1); diff --git a/src/grasp-seasons/3d-views/base-view.ts b/src/grasp-seasons/3d-views/base-view.ts index c3e2bbe..2d1ad8a 100644 --- a/src/grasp-seasons/3d-views/base-view.ts +++ b/src/grasp-seasons/3d-views/base-view.ts @@ -57,6 +57,10 @@ export default class BaseView { this.controls.enablePan = false; this.controls.enableZoom = false; this.controls.rotateSpeed = 0.5; + // Very important: 0 degrees would place the camera in a position where it is impossible to determine + // the desired direction of the camera tilt action. + this.controls.minPolarAngle = THREE.MathUtils.degToRad(1); + this.controls.maxPolarAngle = THREE.MathUtils.degToRad(179); this.dispatch = new EventEmitter(); diff --git a/src/grasp-seasons/3d-views/orbit-view.ts b/src/grasp-seasons/3d-views/orbit-view.ts index 4e072be..05d557a 100644 --- a/src/grasp-seasons/3d-views/orbit-view.ts +++ b/src/grasp-seasons/3d-views/orbit-view.ts @@ -14,7 +14,7 @@ const DEF_PROPERTIES = { sunEarthLine: true }; -const CAMERA_TWEEN_LENGTH = 1000; +const CAMERA_TWEEN_LENGTH = 1500; // Cubic Bezier function function cubicBezier(t: number, p0: number, p1: number, p2: number, p3: number): number { @@ -48,6 +48,10 @@ export default class OrbitView extends BaseView { constructor(parentEl: HTMLElement, props = DEF_PROPERTIES) { super(parentEl, props, "orbit-view"); this.registerInteractionHandler(this.earthDraggingInteraction); + + this.controls.addEventListener("change", () => { + this.dispatch.emit("props.change", { cameraTiltAngle: this.getCameraTiltAngle() }); + }); } setViewAxis(vec3: THREE.Vector3) { @@ -55,13 +59,41 @@ export default class OrbitView extends BaseView { this.cameraSymbol.rotateX(Math.PI * 0.5); } - getCameraAngle() { - const refVec = this.camera.position.clone().setY(0); - let angle = this.camera.position.angleTo(refVec) * 180 / Math.PI; + getCameraTiltAngle() { + const targetPos = this.controls.target.clone(); + const refVec = this.camera.position.clone().sub(targetPos).setY(0); + let angle = this.camera.position.clone().sub(targetPos).angleTo(refVec); if (this.camera.position.y < 0) angle *= -1; - return angle; + const angleInDeg = angle * 180 / Math.PI; + return angleInDeg; } + setCameraTiltAngle(angleInDeg: number) { + const angleInRad = angleInDeg * Math.PI / 180; + + const targetPos = this.controls.target.clone(); + const cameraToTarget = this.camera.position.clone().sub(targetPos); + const cameraToTargetLen = cameraToTarget.length(); + + // Calculate reference vector with zero y-component + const refVec = cameraToTarget.clone().setY(0); + + // Calculate the axis of rotation (cross product of the Y-axis and refVec) + const axisOfRotation = new THREE.Vector3(0, 1, 0).cross(refVec).normalize(); + + // Rotate the reference vector to achieve the desired tilt angle + const newPos = refVec.applyAxisAngle(axisOfRotation, -angleInRad); + + // Set the length of the new position vector to the original distance from target + newPos.setLength(cameraToTargetLen); + + // Translate the new position back to the world coordinates + newPos.add(targetPos); + + // Update the camera's position + this.camera.position.copy(newPos); +} + getEarthPosition() { const vector = this.earth.posObject.position.clone(); @@ -83,7 +115,7 @@ export default class OrbitView extends BaseView { } getCloseUpCameraPosition() { - const cameraOffset = new THREE.Vector3(0, 0, 40000000 / data.SCALE_FACTOR); + const cameraOffset = new THREE.Vector3(0, 1000000 / data.SCALE_FACTOR, 40000000 / data.SCALE_FACTOR); return this.earth.posObject.position.clone().add(cameraOffset); } @@ -110,6 +142,7 @@ export default class OrbitView extends BaseView { 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; @@ -121,7 +154,7 @@ export default class OrbitView extends BaseView { } getInitialCameraPosition() { - return new THREE.Vector3(0, 440000000 / data.SCALE_FACTOR, 0); + return new THREE.Vector3(0, 440000000 / data.SCALE_FACTOR, 10); } _setInitialCamPos() { @@ -174,15 +207,25 @@ export default class OrbitView extends BaseView { } } + _updateCameraTiltAngle() { + if (this.desiredCameraPos || this.desiredCameraLookAt) { + // Do not try to update camera tilt angle while transitioning camera position + return; + } + this.setCameraTiltAngle(this.props.cameraTiltAngle ?? 0); + } + _updateDay(): void { + const oldEarthPosition = this.earth.posObject.position.clone(); super._updateDay(); if (this.props.earthCloseUpView) { + const positionDiff = this.earth.posObject.position.clone().sub(oldEarthPosition); if (this.desiredCameraPos && this.desiredCameraLookAt) { - this.desiredCameraPos.copy(this.getCloseUpCameraPosition()); - this.desiredCameraLookAt.copy(this.earth.posObject.position); + this.desiredCameraPos.add(positionDiff); + this.desiredCameraLookAt.add(positionDiff); } else { - this.camera.position.copy(this.getCloseUpCameraPosition()); - this.controls.target.copy(this.earth.posObject.position); + this.camera.position.add(positionDiff); + this.controls.target.add(positionDiff); } } } diff --git a/src/grasp-seasons/components/orbit-view-comp.tsx b/src/grasp-seasons/components/orbit-view-comp.tsx index 92dd941..1986c79 100644 --- a/src/grasp-seasons/components/orbit-view-comp.tsx +++ b/src/grasp-seasons/components/orbit-view-comp.tsx @@ -39,11 +39,11 @@ export default class OrbitViewComp extends CanvasView { _setupLogging() { this.externalView.on("camera.changeStart", () => { - this._startAngle = this.externalView.getCameraAngle(); + this._startAngle = this.externalView.getCameraTiltAngle(); }); this.externalView.on("camera.changeEnd", () => { this.props.log?.("OrbitViewAngleChanged", { - value: this.externalView.getCameraAngle(), + value: this.externalView.getCameraTiltAngle(), prevValue: this._startAngle }); }); diff --git a/src/grasp-seasons/components/seasons.scss b/src/grasp-seasons/components/seasons.scss index 5693cb5..c2f14b3 100644 --- a/src/grasp-seasons/components/seasons.scss +++ b/src/grasp-seasons/components/seasons.scss @@ -49,6 +49,24 @@ body { } } + .tilt-slider { + position: absolute; + bottom: 10px; + right: 10px; + width: 36px; + text-align: center; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + + .slider-container { + height: 90px; + margin-top: 20px; + margin-bottom: 20px; + } + } + .view-type-dropdown { position: absolute; top: 10px; diff --git a/src/grasp-seasons/components/seasons.tsx b/src/grasp-seasons/components/seasons.tsx index e36e195..b972994 100644 --- a/src/grasp-seasons/components/seasons.tsx +++ b/src/grasp-seasons/components/seasons.tsx @@ -21,8 +21,8 @@ const DEFAULT_SIM_STATE: ISimState = { earthTilt: true, earthRotation: 1.539, sunEarthLine: true, - lat: 40.11, - long: -88.2, + lat: 0, + long: 0, sunrayColor: "#D8D8AC", groundColor: "#4C7F19", // 'auto' will make color different for each season sunrayDistMarker: false, @@ -36,6 +36,7 @@ const DEFAULT_SIM_STATE: ISimState = { // 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, + cameraTiltAngle: 89 }; function capitalize(string: string) { @@ -194,6 +195,10 @@ const Seasons: React.FC = ({ lang = "en_us", initialState = {}, log = (a setLocationSearch(""); }; + const handleTiltSliderChange = (event: any, ui: any) => { + setSimState(prevState => ({ ...prevState, cameraTiltAngle: ui.value })); + }; + const handleMyLocationChange = (lat: number, long: number, name: string) => { const rot = -long * Math.PI / 180; setSimState(prevState => ({ ...prevState, lat, long, earthRotation: rot })); @@ -242,6 +247,23 @@ const Seasons: React.FC = ({ lang = "en_us", initialState = {}, log = (a { t("~DAILY_ROTATION", simLang) } +
+ +
+ +
+
diff --git a/src/grasp-seasons/components/slider/slider.scss b/src/grasp-seasons/components/slider/slider.scss index cde09a9..2546485 100644 --- a/src/grasp-seasons/components/slider/slider.scss +++ b/src/grasp-seasons/components/slider/slider.scss @@ -31,4 +31,16 @@ .ui-slider-tick-label { margin-top: 15px; } + + &.ui-slider-vertical { + width: 8px; + height: 100%; + margin-top: 0; + margin-bottom: 0; + + .ui-slider-handle { + margin-left: -10px; + margin-bottom: -15px; + } + } } diff --git a/src/grasp-seasons/components/slider/slider.tsx b/src/grasp-seasons/components/slider/slider.tsx index ae08965..49fdd1f 100644 --- a/src/grasp-seasons/components/slider/slider.tsx +++ b/src/grasp-seasons/components/slider/slider.tsx @@ -15,6 +15,7 @@ interface IProps { log: ((action: string, data: any) => void) | null; start?: (event: any, ui: any) => void; stop?: (event: any, ui: any) => void; + orientation?: "horizontal" | "vertical"; } interface IOptions extends IProps { _slideStart: number; diff --git a/src/grasp-seasons/types.ts b/src/grasp-seasons/types.ts index 7c434d8..d601ee0 100644 --- a/src/grasp-seasons/types.ts +++ b/src/grasp-seasons/types.ts @@ -20,6 +20,7 @@ export interface ISimState { // 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; + cameraTiltAngle: number; } export type ModelType = "earth-view" | "orbit-view" | "unknown";