176 lines
6.9 KiB
JavaScript
176 lines
6.9 KiB
JavaScript
// Three.js scene setup: renderer, cameras, controls, lights, atmosphere, water, stars.
|
|
|
|
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
export const canvas = document.getElementById('canvas');
|
|
export const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
renderer.setSize(innerWidth, innerHeight);
|
|
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
|
|
export const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x030308);
|
|
|
|
export const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 200);
|
|
camera.position.set(0, 0.4, 2.8);
|
|
|
|
export const ctrl = new OrbitControls(camera, canvas);
|
|
ctrl.enableDamping = true; ctrl.dampingFactor = 0.06;
|
|
ctrl.enablePan = false;
|
|
ctrl.minDistance = 1.4; ctrl.maxDistance = 8;
|
|
ctrl.enableZoom = false; // disable built-in zoom; custom handler below
|
|
|
|
// Smooth zoom: wheel sets a target distance, each frame lerps toward it
|
|
let _zoomTarget = camera.position.distanceTo(ctrl.target);
|
|
const ZOOM_STEP = 0.92; // multiplier per tick (lower = faster zoom)
|
|
const ZOOM_SMOOTH = 0.12; // lerp speed per frame (higher = snappier)
|
|
|
|
canvas.addEventListener('wheel', (e) => {
|
|
if (!ctrl.enabled) return;
|
|
e.preventDefault();
|
|
const dir = Math.sign(e.deltaY);
|
|
_zoomTarget *= dir > 0 ? 1 / ZOOM_STEP : ZOOM_STEP;
|
|
_zoomTarget = THREE.MathUtils.clamp(_zoomTarget, ctrl.minDistance, ctrl.maxDistance);
|
|
}, { passive: false });
|
|
|
|
// Pinch-to-zoom for globe (touch)
|
|
let _pinchDist = 0;
|
|
canvas.addEventListener('touchstart', (e) => {
|
|
if (!ctrl.enabled || e.touches.length !== 2) { _pinchDist = 0; return; }
|
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
_pinchDist = Math.sqrt(dx * dx + dy * dy);
|
|
}, { passive: true });
|
|
|
|
canvas.addEventListener('touchmove', (e) => {
|
|
if (!ctrl.enabled || e.touches.length !== 2 || _pinchDist === 0) return;
|
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
const ratio = _pinchDist / dist;
|
|
_zoomTarget *= ratio;
|
|
_zoomTarget = THREE.MathUtils.clamp(_zoomTarget, ctrl.minDistance, ctrl.maxDistance);
|
|
_pinchDist = dist;
|
|
}, { passive: true });
|
|
|
|
canvas.addEventListener('touchend', () => { _pinchDist = 0; }, { passive: true });
|
|
|
|
export function tickZoom() {
|
|
const v = new THREE.Vector3().subVectors(camera.position, ctrl.target);
|
|
const cur = v.length();
|
|
const next = THREE.MathUtils.lerp(cur, _zoomTarget, ZOOM_SMOOTH);
|
|
if (Math.abs(next - cur) < 0.0001) return;
|
|
v.setLength(next);
|
|
camera.position.copy(ctrl.target).add(v);
|
|
}
|
|
|
|
scene.add(new THREE.AmbientLight(0xaabbcc, 3.5));
|
|
export const sun = new THREE.DirectionalLight(0xfff8ee, 1.5);
|
|
sun.position.set(5, 3, 4);
|
|
scene.add(sun);
|
|
|
|
// Stars
|
|
export let starsMesh;
|
|
{ const g=new THREE.BufferGeometry(),p=[];
|
|
for(let i=0;i<3000;i++){const th=Math.random()*Math.PI*2,ph=Math.acos(2*Math.random()-1),r=40+Math.random()*30;
|
|
p.push(r*Math.sin(ph)*Math.cos(th),r*Math.sin(ph)*Math.sin(th),r*Math.cos(ph));}
|
|
g.setAttribute('position',new THREE.Float32BufferAttribute(p,3));
|
|
starsMesh = new THREE.Points(g,new THREE.PointsMaterial({color:0xffffff,size:0.08}));
|
|
scene.add(starsMesh); }
|
|
|
|
// Atmosphere
|
|
const atmosMat = new THREE.ShaderMaterial({
|
|
uniforms:{c:{value:new THREE.Color(0.35,0.6,1.0)}},
|
|
vertexShader:`varying vec3 vN,vP;void main(){vN=normalize(normalMatrix*normal);vP=(modelViewMatrix*vec4(position,1)).xyz;gl_Position=projectionMatrix*vec4(vP,1);}`,
|
|
fragmentShader:`uniform vec3 c;varying vec3 vN,vP;void main(){float r=1.0-max(0.0,dot(normalize(-vP),vN));gl_FragColor=vec4(c,pow(r,3.5)*0.55);}`,
|
|
transparent:true,side:THREE.FrontSide,depthWrite:false
|
|
});
|
|
export const atmosMesh = new THREE.Mesh(new THREE.SphereGeometry(1.12,64,64), atmosMat);
|
|
scene.add(atmosMesh);
|
|
|
|
// Water sphere
|
|
const waterMat = new THREE.MeshPhongMaterial({
|
|
color:0x0c3a6e, transparent:true, opacity:0.55,
|
|
shininess:120, specular:0x4488bb, depthWrite:false
|
|
});
|
|
export const waterMesh = new THREE.Mesh(new THREE.SphereGeometry(1.0,80,80), waterMat);
|
|
scene.add(waterMesh);
|
|
|
|
// Equirectangular map camera & controls
|
|
export const mapCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 100);
|
|
mapCamera.position.set(0, 0, 5);
|
|
mapCamera.lookAt(0, 0, 0);
|
|
|
|
export function updateMapCameraFrustum() {
|
|
const aspect = innerWidth / innerHeight;
|
|
const mapAspect = 2;
|
|
let halfW, halfH;
|
|
if (aspect > mapAspect) {
|
|
halfH = 1.15;
|
|
halfW = halfH * aspect;
|
|
} else {
|
|
halfW = 2.3;
|
|
halfH = halfW / aspect;
|
|
}
|
|
mapCamera.left = -halfW; mapCamera.right = halfW;
|
|
mapCamera.top = halfH; mapCamera.bottom = -halfH;
|
|
mapCamera.updateProjectionMatrix();
|
|
}
|
|
updateMapCameraFrustum();
|
|
|
|
export const mapCtrl = new OrbitControls(mapCamera, canvas);
|
|
mapCtrl.enableRotate = false;
|
|
mapCtrl.enableDamping = true;
|
|
mapCtrl.dampingFactor = 0.09;
|
|
mapCtrl.panSpeed = 1.4;
|
|
mapCtrl.screenSpacePanning = true;
|
|
mapCtrl.mouseButtons = { LEFT: THREE.MOUSE.PAN, MIDDLE: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN };
|
|
mapCtrl.touches = { ONE: THREE.TOUCH.PAN, TWO: THREE.TOUCH.DOLLY_PAN };
|
|
mapCtrl.minZoom = 0.5;
|
|
mapCtrl.maxZoom = 20;
|
|
mapCtrl.enableZoom = false; // custom handler below
|
|
mapCtrl.enabled = false;
|
|
|
|
// Smooth zoom for map view (orthographic)
|
|
let _mapZoomTarget = mapCamera.zoom;
|
|
const MAP_ZOOM_STEP = 0.92;
|
|
const MAP_ZOOM_SMOOTH = 0.12;
|
|
|
|
canvas.addEventListener('wheel', (e) => {
|
|
if (!mapCtrl.enabled) return;
|
|
e.preventDefault();
|
|
const dir = Math.sign(e.deltaY);
|
|
_mapZoomTarget *= dir < 0 ? 1 / MAP_ZOOM_STEP : MAP_ZOOM_STEP;
|
|
_mapZoomTarget = THREE.MathUtils.clamp(_mapZoomTarget, mapCtrl.minZoom, mapCtrl.maxZoom);
|
|
}, { passive: false });
|
|
|
|
// Pinch-to-zoom for map (touch)
|
|
let _mapPinchDist = 0;
|
|
canvas.addEventListener('touchstart', (e) => {
|
|
if (!mapCtrl.enabled || e.touches.length !== 2) { _mapPinchDist = 0; return; }
|
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
_mapPinchDist = Math.sqrt(dx * dx + dy * dy);
|
|
}, { passive: true });
|
|
|
|
canvas.addEventListener('touchmove', (e) => {
|
|
if (!mapCtrl.enabled || e.touches.length !== 2 || _mapPinchDist === 0) return;
|
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
const ratio = dist / _mapPinchDist;
|
|
_mapZoomTarget *= ratio;
|
|
_mapZoomTarget = THREE.MathUtils.clamp(_mapZoomTarget, mapCtrl.minZoom, mapCtrl.maxZoom);
|
|
_mapPinchDist = dist;
|
|
}, { passive: true });
|
|
|
|
canvas.addEventListener('touchend', () => { _mapPinchDist = 0; }, { passive: true });
|
|
|
|
export function tickMapZoom() {
|
|
const cur = mapCamera.zoom;
|
|
const next = THREE.MathUtils.lerp(cur, _mapZoomTarget, MAP_ZOOM_SMOOTH);
|
|
if (Math.abs(next - cur) < 0.0001) return;
|
|
mapCamera.zoom = next;
|
|
mapCamera.updateProjectionMatrix();
|
|
}
|