diff --git a/src/grasp-seasons/3d-models/common-models.ts b/src/grasp-seasons/3d-models/common-models.ts index d1b528c..94f62cc 100644 --- a/src/grasp-seasons/3d-models/common-models.ts +++ b/src/grasp-seasons/3d-models/common-models.ts @@ -6,6 +6,8 @@ import * as data from "../utils/solar-system-data"; import * as c from "./constants"; import { IModelParams } from "../types"; +import SunPNG from "../assets/orbital-sun@3x.png"; + function addEdges(mesh: THREE.Mesh) { const geometry = new THREE.EdgesGeometry(mesh.geometry); const material = new THREE.LineBasicMaterial({ color: 0x000000 }); @@ -56,7 +58,7 @@ export default { return light; }, - sun (params: IModelParams) { + sun2 (params: IModelParams) { const radius = params.type === "orbit-view" ? c.SIMPLE_SUN_RADIUS : c.SUN_RADIUS; const geometry = new THREE.SphereGeometry(radius, 32, 32); const material = new THREE.MeshPhongMaterial({ emissive: c.SUN_COLOR, color: 0x000000 }); @@ -64,6 +66,15 @@ export default { return mesh; }, + sun (params: IModelParams) { + const texture = new THREE.TextureLoader().load(SunPNG); + const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false }); + const sprite = new THREE.Sprite(material); + sprite.renderOrder = 1; + sprite.scale.set(100000000 * c.SF, 100000000 * c.SF, 1); + return sprite; + }, + earth (params: IModelParams) { const simple = params.type === "orbit-view"; const RADIUS = simple ? c.SIMPLE_EARTH_RADIUS : c.EARTH_RADIUS; @@ -96,13 +107,13 @@ export default { // Load font in a sync way, using webpack raw-loader. Based on async THREE JS loader: // https://github.com/mrdoob/three.js/blob/ddab1fda4fd1e21babf65aa454fc0fe15bfabc33/src/loaders/FontLoader.js#L20 const font = new Font(museo500FontDef as any); - const SIZE = 16000000; + const SIZE = 13000000; const HEIGHT = 1000000; - const SIZE_SMALL = SIZE / 2; - const HEIGHT_SMALL = HEIGHT / 2; + const SIZE_SMALL = SIZE; + const HEIGHT_SMALL = HEIGHT; const COLOR = 0xffff00; - const COLOR_SMALL = 0x999966; + const COLOR_SMALL = 0xffffff; const geometry = new TextGeometry(txt, { size: small ? SIZE_SMALL * c.SF : SIZE * c.SF, @@ -122,12 +133,11 @@ export default { }, grid (params: IModelParams) { - const simple = params.type === "orbit-view"; - const RAY_COUNT = 24; + const RAY_COUNT = 12; const DAY_COUNT = 365; const STEP = DAY_COUNT / RAY_COUNT; const geometry = new THREE.BufferGeometry(); - const material = new THREE.LineBasicMaterial({ color: 0xffff00, transparent: true, opacity: simple ? 0.4 : 0.6 }); + const material = new THREE.LineBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.7 }); const vertices = new Float32Array(2 * RAY_COUNT * 3); for (let i = 0; i < RAY_COUNT; ++i) { vertices[i * 6] = 0; diff --git a/src/grasp-seasons/3d-models/constants.ts b/src/grasp-seasons/3d-models/constants.ts index 3dcaeed..0c8da56 100644 --- a/src/grasp-seasons/3d-models/constants.ts +++ b/src/grasp-seasons/3d-models/constants.ts @@ -5,13 +5,13 @@ import * as data from "../utils/solar-system-data"; export const SF = 1 / data.SCALE_FACTOR; export const EARTH_RADIUS = 7000000 * SF; -export const SIMPLE_EARTH_RADIUS = 10000000 * SF; +export const SIMPLE_EARTH_RADIUS = 13000000 * SF; export const SUN_RADIUS = 4000000 * SF; -export const SIMPLE_SUN_RADIUS = 15000000 * SF; +export const SIMPLE_SUN_RADIUS = 25000000 * SF; export const LATLNG_MARKER_RADIUS = 300000 * SF; export const LAT_LINE_THICKNESS = 0.01; -export const SUN_COLOR = 0xCB671F; +export const SUN_COLOR = 0xdcdca3; export const HIGHLIGHT_COLOR = 0xff0000; export const HIGHLIGHT_EMISSIVE = 0xbb3333; diff --git a/src/grasp-seasons/3d-models/earth.ts b/src/grasp-seasons/3d-models/earth.ts index 3ab43be..e2aec1c 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'; @@ -17,6 +16,7 @@ export default class Earth { _orbitRotationObject: THREE.Object3D; _posObject: THREE.Object3D; _tiltObject: THREE.Object3D; + constructor(params: IModelParams) { const simple = params.type === "orbit-view"; const RADIUS = simple ? c.SIMPLE_EARTH_RADIUS : c.EARTH_RADIUS; @@ -24,11 +24,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-models/sun-earth-line.ts b/src/grasp-seasons/3d-models/sun-earth-line.ts index a246382..2ddcc12 100644 --- a/src/grasp-seasons/3d-models/sun-earth-line.ts +++ b/src/grasp-seasons/3d-models/sun-earth-line.ts @@ -83,9 +83,9 @@ export default class { } _initArrow(simple: boolean) { - const HEIGHT = simple ? 25000000 * c.SF : 2500000 * c.SF; + const HEIGHT = simple ? 115000000 * c.SF : 2500000 * c.SF; const RADIUS = simple ? 1500000 * c.SF : 100000 * c.SF; - const HEAD_RADIUS = RADIUS * (simple ? 2.5 : 2); + const HEAD_RADIUS = RADIUS * (simple ? 7 : 2); const HEAD_HEIGHT = HEIGHT * 0.2; const geometry = new THREE.CylinderGeometry(RADIUS, RADIUS, HEIGHT, 32); const material = new THREE.MeshPhongMaterial({ color: 0xff0000, emissive: c.SUN_COLOR }); diff --git a/src/grasp-seasons/3d-views/base-view.ts b/src/grasp-seasons/3d-views/base-view.ts index 72881bd..c3e2bbe 100644 --- a/src/grasp-seasons/3d-views/base-view.ts +++ b/src/grasp-seasons/3d-views/base-view.ts @@ -32,6 +32,7 @@ export default class BaseView { scene: THREE.Scene; sunEarthLine!: SunEarthLine; type: ModelType; + constructor(parentEl: HTMLElement, props: Partial = DEF_PROPERTIES, modelType: ModelType = "unknown") { const width = parentEl.clientWidth; const height = parentEl.clientHeight; @@ -137,7 +138,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..dc600e2 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, 440000000 / 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); @@ -106,12 +200,12 @@ export default class OrbitView extends BaseView { const months = this.months; const segments = months.length; const arc = 2 * Math.PI / segments; - const labelRadius = data.EARTH_ORBITAL_RADIUS * 1.15; + const labelRadius = data.EARTH_ORBITAL_RADIUS * 1.22; const monthLabels: THREE.Object3D[] = []; for (let i = 0; i < months.length; i++) { - const monthLbl = models.label(months[i], months[i].length === 3); + const monthLbl = models.label(months[i], i % 3 !== 0); const angle = i * arc; monthLbl.position.x = labelRadius * Math.sin(angle); monthLbl.position.z = labelRadius * Math.cos(angle); diff --git a/src/grasp-seasons/assets/orbital-sun@3x.png b/src/grasp-seasons/assets/orbital-sun@3x.png new file mode 100644 index 0000000..b81843b Binary files /dev/null and b/src/grasp-seasons/assets/orbital-sun@3x.png differ 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 +
+ +