Files
2026-09-25 17:02:24 +03:00

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();
}