release: bump version to 0.29.0

This commit is contained in:
linkong
2026-04-20 17:43:59 +08:00
parent ae77b06c3c
commit fe45a99cbd
16 changed files with 787 additions and 90 deletions

View File

@@ -0,0 +1,487 @@
import * as THREE from "three";
import * as Astronomy from "astronomy-engine";
import { CELESTIAL_CONFIG, EARTH_CONFIG } from "./constants.js";
const textureLoader = new THREE.TextureLoader();
const defaultSunDirection = new THREE.Vector3(1, 0.2, 0.4).normalize();
const defaultMoonDirection = new THREE.Vector3(-0.6, 0.45, -0.2).normalize();
let celestialRoot = null;
let skySphere = null;
let brightStarsGroup = null;
let sunSprite = null;
let moonSprite = null;
let sunHaloSprite = null;
let moonHaloSprite = null;
let brightStarTexture = null;
let sunDirection = defaultSunDirection.clone();
let moonDirection = defaultMoonDirection.clone();
let lastUpdatedAt = 0;
let linkedSunLight = null;
let linkedBackLight = null;
let linkedEarth = null;
let brightStarSprites = [];
let celestialRotationQuaternion = new THREE.Quaternion();
let celestialViewQuaternion = new THREE.Quaternion();
let runtimeOrientationEuler = {
...CELESTIAL_CONFIG.orientationEulerRad,
};
let runtimeFollowConfig = {
...CELESTIAL_CONFIG.followEarthRotation,
};
const scratchEuler = new THREE.Euler(0, 0, 0, "YXZ");
function getCelestialEuler() {
const { x, y, z } = runtimeOrientationEuler;
return new THREE.Euler(x, y, z, "YXZ");
}
function refreshCelestialOrientation() {
celestialRotationQuaternion.setFromEuler(getCelestialEuler());
refreshCelestialView();
}
function refreshCelestialView() {
if (linkedEarth && runtimeFollowConfig.enabled) {
const followX = runtimeFollowConfig.x
? (linkedEarth.rotation.x - EARTH_CONFIG.tiltRad) * (runtimeFollowConfig.invertX ? -1 : 1)
: 0;
const followY = runtimeFollowConfig.y
? linkedEarth.rotation.y * (runtimeFollowConfig.invertY ? -1 : 1)
: 0;
const followZ = runtimeFollowConfig.z
? linkedEarth.rotation.z * (runtimeFollowConfig.invertZ ? -1 : 1)
: 0;
scratchEuler.set(
followX,
followY,
followZ,
"YXZ",
);
celestialViewQuaternion.setFromEuler(scratchEuler);
} else {
celestialViewQuaternion.identity();
}
if (celestialRoot) {
celestialRoot.quaternion
.copy(celestialRotationQuaternion)
.multiply(celestialViewQuaternion);
}
}
function applyCelestialOrientation(direction) {
return direction
.clone()
.applyQuaternion(celestialRotationQuaternion)
.applyQuaternion(celestialViewQuaternion)
.normalize();
}
function configureTextureEncoding(texture) {
if (!texture) return;
if ("colorSpace" in texture && THREE.SRGBColorSpace) {
texture.colorSpace = THREE.SRGBColorSpace;
} else if ("encoding" in texture && THREE.sRGBEncoding) {
texture.encoding = THREE.sRGBEncoding;
}
}
function createDiscTexture(stops, size = 256) {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
const gradient = ctx.createRadialGradient(
size / 2,
size / 2,
0,
size / 2,
size / 2,
size / 2,
);
stops.forEach(([offset, color]) => gradient.addColorStop(offset, color));
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size, size);
const texture = new THREE.CanvasTexture(canvas);
configureTextureEncoding(texture);
texture.needsUpdate = true;
return texture;
}
function createSprite({ texture, scale, opacity = 1, name, color = 0xffffff }) {
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity,
depthWrite: false,
depthTest: true,
toneMapped: false,
color,
});
const sprite = new THREE.Sprite(material);
sprite.name = name;
sprite.scale.setScalar(scale);
sprite.renderOrder = 100;
return sprite;
}
function createSkySphere() {
const geometry = new THREE.SphereGeometry(
CELESTIAL_CONFIG.skyRadius,
64,
64,
);
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
side: THREE.BackSide,
transparent: CELESTIAL_CONFIG.skyOpacity < 1,
opacity: CELESTIAL_CONFIG.skyOpacity,
depthWrite: false,
depthTest: false,
fog: false,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.name = "celestial-sky-sphere";
mesh.renderOrder = -1000;
mesh.raycast = () => {};
textureLoader.load(
CELESTIAL_CONFIG.starMapUrl,
(texture) => {
configureTextureEncoding(texture);
material.map = texture;
material.needsUpdate = true;
},
undefined,
() => {
console.warn("Failed to load celestial star map texture");
},
);
return mesh;
}
function geoVectorToWorldDirection(vector) {
return new THREE.Vector3(vector.x, vector.z, vector.y).normalize();
}
function raDecToWorldDirection(raDeg, decDeg) {
const raRad = THREE.MathUtils.degToRad(raDeg);
const decRad = THREE.MathUtils.degToRad(decDeg);
const x = Math.cos(decRad) * Math.cos(raRad);
const y = Math.sin(decRad);
const z = Math.cos(decRad) * Math.sin(raRad);
return new THREE.Vector3(x, z, y).normalize();
}
function colorFromBvIndex(colorIndex) {
if (colorIndex <= -0.2) return 0xa8c9ff;
if (colorIndex <= 0.0) return 0xc8dcff;
if (colorIndex <= 0.3) return 0xf4f7ff;
if (colorIndex <= 0.7) return 0xfff4dc;
if (colorIndex <= 1.1) return 0xffdfb0;
if (colorIndex <= 1.5) return 0xffc47f;
return 0xffa35e;
}
function scaleFromMagnitude(mag) {
const brightness = THREE.MathUtils.clamp(
1 - (mag - (-1.5)) / (CELESTIAL_CONFIG.brightStarMinMag - (-1.5)),
0,
1,
);
return THREE.MathUtils.lerp(
CELESTIAL_CONFIG.brightStarMinScale,
CELESTIAL_CONFIG.brightStarMaxScale,
Math.pow(brightness, 0.72),
);
}
function createBrightStarTexture() {
return createDiscTexture([
[0, "rgba(255,255,255,1)"],
[0.18, "rgba(255,255,255,0.98)"],
[0.46, "rgba(215,230,255,0.42)"],
[1, "rgba(128,168,255,0)"],
], 128);
}
function createBrightStarsGroup() {
const group = new THREE.Group();
group.name = "bright-stars-group";
group.visible = true;
return group;
}
async function loadBrightStars() {
try {
const response = await fetch(CELESTIAL_CONFIG.brightStarsUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const stars = await response.json();
const filteredStars = stars
.filter((star) => Number.isFinite(star?.raDeg) && Number.isFinite(star?.decDeg))
.filter((star) => star.mag <= CELESTIAL_CONFIG.brightStarMinMag)
.sort((a, b) => a.mag - b.mag)
.slice(0, CELESTIAL_CONFIG.brightStarMaxCount);
brightStarTexture = createBrightStarTexture();
brightStarSprites = filteredStars.map((star) => {
const sprite = createSprite({
texture: brightStarTexture,
scale: scaleFromMagnitude(star.mag),
opacity: CELESTIAL_CONFIG.brightStarOpacity,
name: `bright-star-${star.name}`,
color: colorFromBvIndex(star.colorIndex ?? 0.4),
});
sprite.position
.copy(raDecToWorldDirection(star.raDeg, star.decDeg))
.multiplyScalar(CELESTIAL_CONFIG.brightStarDistance);
sprite.userData.star = star;
return sprite;
});
brightStarSprites.forEach((sprite) => brightStarsGroup?.add(sprite));
} catch (error) {
console.warn("Failed to load bright star layer", error);
}
}
function computeBodyDirection(body, date) {
const vector = Astronomy.GeoVector(body, date, false);
return geoVectorToWorldDirection(vector);
}
function updateSpritePositions() {
if (sunSprite) {
sunSprite.position.copy(sunDirection).multiplyScalar(CELESTIAL_CONFIG.sunDistance);
}
if (sunHaloSprite) {
sunHaloSprite.position.copy(sunDirection).multiplyScalar(CELESTIAL_CONFIG.sunDistance);
}
if (moonSprite) {
moonSprite.position.copy(moonDirection).multiplyScalar(CELESTIAL_CONFIG.moonDistance);
}
if (moonHaloSprite) {
moonHaloSprite.position.copy(moonDirection).multiplyScalar(CELESTIAL_CONFIG.moonDistance);
}
}
function updateLighting() {
const calibratedSunDirection = applyCelestialOrientation(sunDirection);
if (linkedSunLight) {
linkedSunLight.color.setHex(CELESTIAL_CONFIG.sunLightColor);
linkedSunLight.intensity = CELESTIAL_CONFIG.sunLightIntensity;
linkedSunLight.position
.copy(calibratedSunDirection)
.multiplyScalar(CELESTIAL_CONFIG.sunLightDistance);
}
if (linkedBackLight) {
linkedBackLight.color.setHex(CELESTIAL_CONFIG.backLightColor);
linkedBackLight.intensity = CELESTIAL_CONFIG.backLightIntensity;
linkedBackLight.position
.copy(calibratedSunDirection)
.multiplyScalar(-CELESTIAL_CONFIG.sunLightDistance * 0.7);
}
}
function computeCelestialState(date = new Date()) {
sunDirection = computeBodyDirection(Astronomy.Body.Sun, date);
moonDirection = computeBodyDirection(Astronomy.Body.Moon, date);
updateSpritePositions();
updateLighting();
lastUpdatedAt = date.getTime();
}
export function initCelestialLayer(
scene,
{ camera = null, sunLight = null, backLight = null, earth = null } = {},
) {
if (!scene || !CELESTIAL_CONFIG.enabled) return null;
disposeCelestialLayer();
linkedSunLight = sunLight;
linkedBackLight = backLight;
linkedEarth = earth;
celestialRoot = new THREE.Group();
celestialRoot.name = "celestial-root";
celestialRoot.position.set(0, 0, 0);
refreshCelestialOrientation();
skySphere = createSkySphere();
brightStarsGroup = createBrightStarsGroup();
const sunTexture = createDiscTexture([
[0, "rgba(255,255,255,1)"],
[0.08, "rgba(255,251,242,1)"],
[0.22, "rgba(255,242,205,0.98)"],
[0.52, "rgba(255,211,117,0.9)"],
[0.82, "rgba(255,162,54,0.18)"],
[1, "rgba(255,120,32,0)"],
]);
const sunHaloTexture = createDiscTexture([
[0, "rgba(255,245,214,0.9)"],
[0.2, "rgba(255,220,154,0.54)"],
[0.52, "rgba(255,160,72,0.16)"],
[1, "rgba(255,120,32,0)"],
], 512);
const moonTexture = createDiscTexture([
[0, "rgba(255,255,255,0.98)"],
[0.42, "rgba(229,236,248,0.92)"],
[0.76, "rgba(164,178,202,0.34)"],
[1, "rgba(80,92,118,0)"],
]);
const moonHaloTexture = createDiscTexture([
[0, "rgba(226,235,250,0.42)"],
[0.38, "rgba(188,203,230,0.16)"],
[1, "rgba(120,136,170,0)"],
], 384);
sunSprite = createSprite({
texture: sunTexture,
scale: CELESTIAL_CONFIG.sunScale,
name: "sun-sprite",
});
sunHaloSprite = createSprite({
texture: sunHaloTexture,
scale: CELESTIAL_CONFIG.sunHaloScale,
opacity: 0.78,
name: "sun-halo-sprite",
});
moonSprite = createSprite({
texture: moonTexture,
scale: CELESTIAL_CONFIG.moonScale,
opacity: 0.98,
name: "moon-sprite",
});
moonHaloSprite = createSprite({
texture: moonHaloTexture,
scale: CELESTIAL_CONFIG.moonHaloScale,
opacity: 0.52,
name: "moon-halo-sprite",
});
sunHaloSprite.renderOrder = 98;
moonHaloSprite.renderOrder = 98;
celestialRoot.add(skySphere);
celestialRoot.add(brightStarsGroup);
celestialRoot.add(sunHaloSprite);
celestialRoot.add(moonHaloSprite);
celestialRoot.add(sunSprite);
celestialRoot.add(moonSprite);
scene.add(celestialRoot);
computeCelestialState(new Date());
loadBrightStars();
return {
root: celestialRoot,
getSunDirection: () => sunDirection.clone(),
getMoonDirection: () => moonDirection.clone(),
};
}
export function updateCelestialLayer(date = new Date(), camera = null) {
if (!celestialRoot) return;
refreshCelestialView();
const now = date.getTime();
if (!lastUpdatedAt || now - lastUpdatedAt >= CELESTIAL_CONFIG.updateIntervalMs) {
computeCelestialState(date);
} else {
updateLighting();
updateSpritePositions();
}
}
export function getSunDirection() {
return applyCelestialOrientation(sunDirection);
}
export function getMoonDirection() {
return applyCelestialOrientation(moonDirection);
}
export function getCelestialDebugState() {
return {
orientationEulerRad: { ...runtimeOrientationEuler },
followEarthRotation: { ...runtimeFollowConfig },
};
}
export function setCelestialOrientation(nextEuler = {}) {
runtimeOrientationEuler = {
...runtimeOrientationEuler,
...nextEuler,
};
refreshCelestialOrientation();
updateLighting();
return getCelestialDebugState();
}
export function setCelestialFollow(nextFollow = {}) {
runtimeFollowConfig = {
...runtimeFollowConfig,
...nextFollow,
};
refreshCelestialView();
updateLighting();
return getCelestialDebugState();
}
export function disposeCelestialLayer() {
if (celestialRoot?.parent) {
celestialRoot.parent.remove(celestialRoot);
}
[skySphere, sunSprite, moonSprite, sunHaloSprite, moonHaloSprite, ...brightStarSprites].forEach((object) => {
if (!object) return;
if (object.geometry) object.geometry.dispose();
if (object.material) {
if (object.material.map) object.material.map.dispose?.();
object.material.dispose();
}
});
skySphere = null;
brightStarsGroup = null;
sunSprite = null;
moonSprite = null;
sunHaloSprite = null;
moonHaloSprite = null;
celestialRoot = null;
brightStarSprites = [];
brightStarTexture?.dispose?.();
brightStarTexture = null;
lastUpdatedAt = 0;
linkedSunLight = null;
linkedBackLight = null;
linkedEarth = null;
sunDirection.copy(defaultSunDirection);
moonDirection.copy(defaultMoonDirection);
runtimeOrientationEuler = {
...CELESTIAL_CONFIG.orientationEulerRad,
};
runtimeFollowConfig = {
...CELESTIAL_CONFIG.followEarthRotation,
};
}