@
Add star-map Angular app, ETL pipeline, and caveman plugin Angular 3D star map (galaxy/system/body views, Three.js rendering, navigation store) plus the NASA ETL tooling that builds the star, exoplanet and solar-system datasets, Playwright e2e suite, and the cs:caveman Claude Code plugin (command, agent, skill). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> @
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import * as THREE from 'three/webgpu';
|
||||
|
||||
/**
|
||||
* Applies a real Milky Way panorama (see `texture-catalog.ts`/`README.md`) as the scene's
|
||||
* background, mapped equirectangularly so it wraps the camera like a real sky rather than a
|
||||
* flat image. Loading is non-blocking: the scene renders immediately with its previous
|
||||
* background and swaps in the photo once it decodes.
|
||||
*/
|
||||
export function applyMilkyWaySkybox(scene: THREE.Scene, path: string): void {
|
||||
new THREE.TextureLoader().load(
|
||||
path,
|
||||
(texture) => {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
scene.background = texture;
|
||||
},
|
||||
undefined,
|
||||
(error) => console.error(`Failed to load the skybox texture "${path}".`, error)
|
||||
);
|
||||
}
|
||||
|
||||
const glowSpriteCache = new Map<string, THREE.Texture>();
|
||||
|
||||
/**
|
||||
* A soft radial-gradient canvas texture, cached per color, used to fake atmosphere/corona glow.
|
||||
* Returns `undefined` if 2D canvas rendering isn't available (e.g. under a test/jsdom
|
||||
* environment with no canvas backend) so callers can fall back to a flat-color sprite instead.
|
||||
*/
|
||||
function glowSpriteTexture(color: THREE.ColorRepresentation): THREE.Texture | undefined {
|
||||
const key = new THREE.Color(color).getHexString();
|
||||
const cached = glowSpriteCache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const size = 128;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rgb = new THREE.Color(color);
|
||||
const [r, g, b] = [Math.round(rgb.r * 255), Math.round(rgb.g * 255), Math.round(rgb.b * 255)];
|
||||
|
||||
const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
||||
gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.85)`);
|
||||
gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.35)`);
|
||||
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
||||
context.fillStyle = gradient;
|
||||
context.fillRect(0, 0, size, size);
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
glowSpriteCache.set(key, texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a soft additive-blended glow halo (used for planetary atmospheres and the Sun's
|
||||
* corona) sized relative to the given object radius. Cheap billboard-sprite approximation
|
||||
* rather than a view-angle-correct Fresnel shader, chosen to stay within built-in material
|
||||
* types the WebGPU backend renders natively (see plan risk on TSL/shader maturity). Falls back
|
||||
* to a flat-colored (gradient-less) sprite if canvas rendering is unavailable.
|
||||
*/
|
||||
export function createGlowSprite(color: THREE.ColorRepresentation, radius: number, scale: number): THREE.Sprite {
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: glowSpriteTexture(color),
|
||||
color: color,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending
|
||||
});
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.setScalar(radius * scale);
|
||||
return sprite;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import * as THREE from 'three/webgpu';
|
||||
|
||||
/**
|
||||
* Real NASA/ESA/USGS photography baked into `src/assets/textures/bodies/` at build time,
|
||||
* keyed by the same ids used in `bodies.json`. Bodies without an entry here (most exoplanets,
|
||||
* a few moons whose photo wasn't sourced this round, and any future body) fall back to
|
||||
* `proceduralBodyTexture()` below rather than a flat color.
|
||||
*
|
||||
* Provenance (all public domain NASA/JPL or CC BY 4.0 Solar System Scope, via Wikimedia
|
||||
* Commons — see each file's Commons page for the original credit line):
|
||||
* mercury/venus/earth/mars/saturn/uranus/neptune/moon/sun/saturn-ring/skybox — Solar System
|
||||
* Scope texture pack (CC BY 4.0); jupiter — Solar System Scope 8k pack (CC BY 4.0); pluto —
|
||||
* NASA/JHUAPL/SwRI New Horizons true-color mosaic; deimos — NASA/JPL/University of Arizona
|
||||
* MRO HiRISE; io — NASA/JPL Galileo highest-resolution true-color mosaic; titan — NASA/JPL
|
||||
* Cassini true-color view.
|
||||
*/
|
||||
const BODY_TEXTURE_PATHS: Record<string, string> = {
|
||||
mercury: 'assets/textures/bodies/mercury.jpg',
|
||||
venus: 'assets/textures/bodies/venus.jpg',
|
||||
earth: 'assets/textures/bodies/earth.jpg',
|
||||
mars: 'assets/textures/bodies/mars.jpg',
|
||||
jupiter: 'assets/textures/bodies/jupiter.jpg',
|
||||
saturn: 'assets/textures/bodies/saturn.jpg',
|
||||
uranus: 'assets/textures/bodies/uranus.jpg',
|
||||
neptune: 'assets/textures/bodies/neptune.jpg',
|
||||
pluto: 'assets/textures/bodies/pluto.jpg',
|
||||
moon: 'assets/textures/bodies/moon.jpg',
|
||||
deimos: 'assets/textures/bodies/deimos.jpg',
|
||||
io: 'assets/textures/bodies/io.jpg',
|
||||
titan: 'assets/textures/bodies/titan.jpg'
|
||||
};
|
||||
|
||||
/** The Sun isn't a `BodyRecord` (it's the system's star marker), so it's looked up separately. */
|
||||
export const SUN_TEXTURE_PATH = 'assets/textures/bodies/sun.jpg';
|
||||
export const SATURN_RING_TEXTURE_PATH = 'assets/textures/bodies/saturn_ring.png';
|
||||
export const MILKY_WAY_SKYBOX_PATH = 'assets/textures/skybox/milkyway.jpg';
|
||||
|
||||
/** True for the handful of bodies that have real photographic atmospheres worth glowing. */
|
||||
const ATMOSPHERE_BY_ID: Record<string, THREE.ColorRepresentation> = {
|
||||
venus: 0xf3dfa6,
|
||||
earth: 0x7fb8ff,
|
||||
mars: 0xd9a066,
|
||||
jupiter: 0xe8d3ad,
|
||||
saturn: 0xe0d2a8,
|
||||
uranus: 0x9fe8e8,
|
||||
neptune: 0x5b7fff,
|
||||
titan: 0xf0b25c
|
||||
};
|
||||
|
||||
export function bodyTexturePath(id: string): string | undefined {
|
||||
return BODY_TEXTURE_PATHS[id];
|
||||
}
|
||||
|
||||
export function atmosphereColorFor(id: string): THREE.ColorRepresentation | undefined {
|
||||
return ATMOSPHERE_BY_ID[id];
|
||||
}
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
const loadedTextures = new Map<string, THREE.Texture>();
|
||||
|
||||
/**
|
||||
* Loads (and caches) a texture by asset path, applying `colorSpace` so JPEG/PNG source
|
||||
* photography matches Three.js's expected sRGB working space. Non-blocking: the texture is
|
||||
* returned immediately and updates in place once the image data arrives (or errors, which is
|
||||
* logged rather than thrown so a slow/unavailable network never breaks the scene).
|
||||
*/
|
||||
export function loadCachedTexture(path: string): THREE.Texture {
|
||||
const cached = loadedTextures.get(path);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const texture = textureLoader.load(
|
||||
path,
|
||||
undefined,
|
||||
undefined,
|
||||
(error) => console.error(`Failed to load texture "${path}".`, error)
|
||||
);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
loadedTextures.set(path, texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
const proceduralTextureCache = new Map<string, THREE.CanvasTexture>();
|
||||
|
||||
/**
|
||||
* Generates a simple procedural surface for bodies with no real photograph available — mainly
|
||||
* exoplanets, whose actual surfaces have never been directly imaged. This is an honest artistic
|
||||
* stand-in (mottled bands tinted by the body's classification color), not a fabricated "real"
|
||||
* texture, and is cached per color so repeated exoplanets of the same kind share one canvas.
|
||||
* Returns `undefined` if 2D canvas rendering isn't available (e.g. under a test/jsdom
|
||||
* environment with no canvas backend); callers should fall back to a flat material color.
|
||||
*/
|
||||
export function proceduralBodyTexture(baseColor: THREE.ColorRepresentation): THREE.CanvasTexture | undefined {
|
||||
const key = new THREE.Color(baseColor).getHexString();
|
||||
const cached = proceduralTextureCache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const size = 256;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const base = new THREE.Color(baseColor);
|
||||
const light = base.clone().offsetHSL(0, -0.15, 0.14);
|
||||
const dark = base.clone().offsetHSL(0, 0.05, -0.16);
|
||||
|
||||
context.fillStyle = `#${base.getHexString()}`;
|
||||
context.fillRect(0, 0, size, size);
|
||||
|
||||
// A handful of horizontal-ish noisy bands, reminiscent of banded gas giants / mottled rock,
|
||||
// without claiming to depict any specific real surface feature.
|
||||
let seed = key.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) || 1;
|
||||
const random = () => {
|
||||
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
||||
return seed / 0x7fffffff;
|
||||
};
|
||||
|
||||
const bandCount = 10;
|
||||
for (let i = 0; i < bandCount; i++) {
|
||||
const y = (i / bandCount) * size + random() * (size / bandCount) * 0.4;
|
||||
const height = size / bandCount * (0.5 + random() * 0.6);
|
||||
context.fillStyle = `#${(random() > 0.5 ? light : dark).getHexString()}`;
|
||||
context.globalAlpha = 0.35 + random() * 0.25;
|
||||
context.fillRect(0, y, size, height);
|
||||
}
|
||||
context.globalAlpha = 1;
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
texture.wrapS = THREE.RepeatWrapping;
|
||||
proceduralTextureCache.set(key, texture);
|
||||
return texture;
|
||||
}
|
||||
Reference in New Issue
Block a user