feat: build procedural lifecycle-safe drive scene

This commit is contained in:
2026-09-02 04:53:55 +03:00
parent cc0887fee0
commit fd8347fbdc
14 changed files with 1822 additions and 0 deletions

79
src/scene/world/sign.ts Normal file
View File

@@ -0,0 +1,79 @@
import * as THREE from 'three';
import type { SectionId } from '@/content/types';
import type { SignSnapshot } from '@/journey/types';
import type { SceneResourceBag } from '@/scene/resources';
const routeLabels: Readonly<Record<SectionId, string>> = {
start: '00 / START',
profile: '01 / PROFILE',
experience: '02 / EXPERIENCE',
infrastructure: '03 / INFRASTRUCTURE',
'selected-builds': '04 / SELECTED BUILDS',
'additional-work': '05 / ADDITIONAL WORK',
toolkit: '06 / TOOLKIT',
contact: '07 / DESTINATION',
};
export function routeLabelFor(id: SectionId): string {
return routeLabels[id];
}
export function createSign(resources: SceneResourceBag) {
const group = new THREE.Group();
group.visible = false;
const geometry = resources.own('geometry', new THREE.PlaneGeometry(8.4, 2.2));
const canvas = document.createElement('canvas');
canvas.width = 768;
canvas.height = 192;
const context = canvas.getContext('2d');
const texture = resources.own('texture', new THREE.CanvasTexture(canvas));
texture.colorSpace = THREE.SRGBColorSpace;
const material = resources.own('material', new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.96,
side: THREE.DoubleSide,
map: texture,
}));
const panel = new THREE.Mesh(geometry, material);
group.add(panel);
const thrusterGeometry = resources.own('geometry', new THREE.ConeGeometry(0.18, 0.72, 8));
const thrusterMaterial = resources.own('material', new THREE.MeshBasicMaterial({ color: 0x44e8f5 }));
for (const x of [-3.5, 3.5]) {
const thruster = new THREE.Mesh(thrusterGeometry, thrusterMaterial);
thruster.rotation.x = Math.PI / 2;
thruster.position.set(x, -1.22, 0.08);
group.add(thruster);
}
let target: SectionId | null = null;
const updateTexture = (id: SectionId): void => {
if (target === id) return;
target = id;
if (!context) return;
context.fillStyle = '#07111b';
context.fillRect(0, 0, canvas.width, canvas.height);
context.strokeStyle = '#35ddeb';
context.lineWidth = 7;
context.strokeRect(8, 8, canvas.width - 16, canvas.height - 16);
context.fillStyle = '#e8fbff';
context.font = '600 46px ui-monospace, monospace';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(routeLabelFor(id), canvas.width / 2, canvas.height / 2);
texture.needsUpdate = true;
};
return {
group,
update(sign: SignSnapshot | null) {
group.visible = sign !== null;
if (!sign) return;
updateTexture(sign.targetId);
const phaseOffset = sign.phase === 'approach' ? -26 : sign.phase === 'stabilized' ? -5 : 8;
group.position.set(7.2, 4.1, phaseOffset + sign.progress * 8);
group.rotation.y = -0.12;
},
};
}