feat: tactical icons, follow-camera orbit, and ship info panel

This commit is contained in:
2026-03-18 22:45:33 -04:00
parent f98c47a8a7
commit aa4a6930ba
17 changed files with 154 additions and 118 deletions

View File

@@ -293,62 +293,6 @@ function createStarSparkleTexture(documentRef: Document): THREE.CanvasTexture {
return texture;
}
function createMilkyWayTexture(documentRef: Document): THREE.CanvasTexture {
const canvas = documentRef.createElement("canvas");
canvas.width = 1024;
canvas.height = 256;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Unable to create milky way texture");
}
const background = context.createLinearGradient(0, 0, 1024, 0);
background.addColorStop(0, "rgba(0,0,0,0)");
background.addColorStop(0.1, "rgba(150,110,255,0.08)");
background.addColorStop(0.32, "rgba(120,210,255,0.14)");
background.addColorStop(0.5, "rgba(255,240,220,0.28)");
background.addColorStop(0.68, "rgba(255,165,210,0.16)");
background.addColorStop(0.88, "rgba(115,155,255,0.08)");
background.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = background;
context.fillRect(0, 0, 1024, 256);
for (let index = 0; index < 220; index += 1) {
const x = THREE.MathUtils.randFloat(0, 1024);
const y = 128 + THREE.MathUtils.randFloatSpread(78);
const radiusX = THREE.MathUtils.randFloat(40, 180);
const radiusY = THREE.MathUtils.randFloat(8, 28);
const alpha = THREE.MathUtils.randFloat(0.025, 0.09);
const hue = THREE.MathUtils.randFloat(0.52, 0.76);
const color = new THREE.Color().setHSL(hue, THREE.MathUtils.randFloat(0.25, 0.6), THREE.MathUtils.randFloat(0.72, 0.9));
const puff = context.createRadialGradient(x, y, 0, x, y, radiusX);
puff.addColorStop(0, `rgba(${Math.round(color.r * 255)},${Math.round(color.g * 255)},${Math.round(color.b * 255)},${alpha})`);
puff.addColorStop(0.55, `rgba(${Math.round(color.r * 255)},${Math.round(color.g * 255)},${Math.round(color.b * 255)},${alpha * 0.45})`);
puff.addColorStop(1, "rgba(0,0,0,0)");
context.save();
context.translate(x, y);
context.scale(1, radiusY / radiusX);
context.fillStyle = puff;
context.beginPath();
context.arc(0, 0, radiusX, 0, Math.PI * 2);
context.fill();
context.restore();
}
for (let index = 0; index < 540; index += 1) {
const x = THREE.MathUtils.randFloat(0, 1024);
const y = 128 + THREE.MathUtils.randFloatSpread(54);
const alpha = THREE.MathUtils.randFloat(0.12, 0.65);
const size = THREE.MathUtils.randFloat(0.8, 2.4);
context.fillStyle = `rgba(255,255,255,${alpha})`;
context.fillRect(x, y, size, size);
}
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
function sampleBackdropStarColor(): THREE.Color {
const roll = Math.random();
if (roll < 0.1) {
@@ -595,39 +539,6 @@ export function createNebulaClouds(texture: THREE.Texture): THREE.Sprite[] {
});
}
export function createMilkyWayBand(documentRef: Document): THREE.Group {
const radius = 33800;
const texture = createMilkyWayTexture(documentRef);
const root = new THREE.Group();
const planeNormal = new THREE.Vector3(0.24, 0.92, -0.3).normalize();
const tangent = new THREE.Vector3().crossVectors(planeNormal, new THREE.Vector3(0, 0, 1));
if (tangent.lengthSq() < 1e-6) {
tangent.set(1, 0, 0);
}
tangent.normalize();
const bitangent = new THREE.Vector3().crossVectors(planeNormal, tangent).normalize();
for (let index = 0; index < 8; index += 1) {
const angle = (index / 8) * Math.PI * 2;
const direction = tangent.clone().multiplyScalar(Math.cos(angle)).add(bitangent.clone().multiplyScalar(Math.sin(angle)));
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: index % 2 === 0 ? 0.22 : 0.15,
depthWrite: false,
blending: THREE.AdditiveBlending,
color: index % 3 === 0 ? "#ffd3f1" : index % 3 === 1 ? "#c8d8ff" : "#ffffff",
fog: false,
}));
sprite.position.copy(direction.multiplyScalar(radius));
sprite.scale.set(16500, 4300 + (index % 3) * 800, 1);
sprite.material.rotation = angle + Math.PI / 2;
root.add(sprite);
}
return root;
}
export function createTacticalIcon(documentRef: Document, color: string, size: number): SceneNode {
const canvas = documentRef.createElement("canvas");
canvas.width = 64;
@@ -657,6 +568,38 @@ export function createTacticalIcon(documentRef: Document, color: string, size: n
return createSceneNode(sprite);
}
export function createShipTacticalIcon(documentRef: Document, color: string, size: number): SceneNode {
const canvas = documentRef.createElement("canvas");
canvas.width = 128;
canvas.height = 96;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Unable to create ship tactical icon");
}
context.clearRect(0, 0, canvas.width, canvas.height);
context.strokeStyle = color;
context.fillStyle = "rgba(7, 16, 30, 0.7)";
context.lineWidth = 5;
context.beginPath();
context.arc(34, 48, 18, 0, Math.PI * 2);
context.stroke();
const texture = new THREE.CanvasTexture(canvas);
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthWrite: false,
depthTest: false,
color: "#ffffff",
}));
sprite.center.set(0.28, 0.5);
sprite.scale.set(size * 1.7, size * 1.275, 1);
sprite.visible = false;
return createSceneNode(sprite);
}
export function createStarDot(documentRef: Document, color: string): SceneNode {
const canvas = documentRef.createElement("canvas");