feat: neon galaxy view with bloom, hazard tints, clouds
This commit is contained in:
+17
-1
@@ -1 +1,17 @@
|
||||
console.log('Probability — boot ok');
|
||||
import { createWorld, advance } from './sim/world.js';
|
||||
import { createRenderer } from './render/app.js';
|
||||
import { GalaxyView } from './render/galaxyView.js';
|
||||
import { goTo } from './sim/player.js';
|
||||
|
||||
const world = createWorld('neon-1');
|
||||
const renderer = await createRenderer(document.getElementById('stage'));
|
||||
const galaxyView = new GalaxyView(renderer, world, {
|
||||
onSelectSystem: (id) => console.log('select system', id),
|
||||
onJump: (laneId) => goTo(world, { laneId }),
|
||||
});
|
||||
|
||||
const DAYS_PER_SEC = 0.25;
|
||||
renderer.app.ticker.add(() => {
|
||||
advance(world, world.t + (renderer.app.ticker.deltaMS / 1000) * DAYS_PER_SEC);
|
||||
galaxyView.update();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Application, Container } from 'pixi.js';
|
||||
import { AdvancedBloomFilter } from 'pixi-filters/advanced-bloom';
|
||||
|
||||
export async function createRenderer(stageEl) {
|
||||
const app = new Application();
|
||||
await app.init({ resizeTo: stageEl, background: '#05070d', antialias: true });
|
||||
stageEl.appendChild(app.canvas);
|
||||
|
||||
// Bloomed world geometry; crisp labels above it, outside the filter.
|
||||
const world = new Container();
|
||||
world.filters = [new AdvancedBloomFilter({ threshold: 0.18, bloomScale: 1.4, blur: 6, quality: 4 })];
|
||||
const labels = new Container();
|
||||
app.stage.addChild(world, labels);
|
||||
|
||||
return { app, world, labels };
|
||||
}
|
||||
|
||||
// Map sim hazard [0, 0.7] to a cyan->red neon tint.
|
||||
export function hazardColor(h) {
|
||||
const t = Math.min(1, h / 0.7);
|
||||
const r = Math.round(0 + t * 255);
|
||||
const g = Math.round(229 - t * 170);
|
||||
const b = Math.round(255 - t * 175);
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
// Fit sim coords (1000x700 layout space) into the canvas with padding.
|
||||
export function makeViewTransform(app) {
|
||||
const w = app.renderer.width, h = app.renderer.height;
|
||||
const scale = Math.min(w / 1060, h / 740);
|
||||
const ox = (w - 1000 * scale) / 2, oy = (h - 700 * scale) / 2;
|
||||
return { x: (sx) => ox + sx * scale, y: (sy) => oy + sy * scale, scale };
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Container, Graphics, Text } from 'pixi.js';
|
||||
import { hazardColor, makeViewTransform } from './app.js';
|
||||
import { playerClouds } from '../sim/clouds.js';
|
||||
import { playerSystem } from '../sim/player.js';
|
||||
import { otherEnd } from '../sim/galaxy.js';
|
||||
|
||||
export class GalaxyView {
|
||||
// onSelectSystem(systemId) -> UI intel panel; onJump(laneId) -> goTo lane
|
||||
constructor(renderer, world, { onSelectSystem, onJump }) {
|
||||
this.renderer = renderer;
|
||||
this.world = world;
|
||||
this.onSelectSystem = onSelectSystem;
|
||||
this.onJump = onJump;
|
||||
this.container = new Container();
|
||||
this.labelContainer = new Container();
|
||||
renderer.world.addChild(this.container);
|
||||
renderer.labels.addChild(this.labelContainer);
|
||||
|
||||
this.staticG = new Graphics();
|
||||
this.cloudG = new Graphics();
|
||||
this.dynamicG = new Graphics();
|
||||
this.container.addChild(this.staticG, this.cloudG, this.dynamicG);
|
||||
this.hitNodes = new Container();
|
||||
this.container.addChild(this.hitNodes);
|
||||
this.buildStatic();
|
||||
}
|
||||
|
||||
buildStatic() {
|
||||
const { galaxy } = this.world;
|
||||
const v = makeViewTransform(this.renderer.app);
|
||||
this.v = v;
|
||||
const g = this.staticG;
|
||||
g.clear();
|
||||
for (const lane of galaxy.lanes) {
|
||||
const a = galaxy.systems[lane.a], b = galaxy.systems[lane.b];
|
||||
g.moveTo(v.x(a.x), v.y(a.y)).lineTo(v.x(b.x), v.y(b.y))
|
||||
.stroke({ width: 2, color: hazardColor(lane.hazard), alpha: 0.55 });
|
||||
}
|
||||
for (const s of galaxy.systems) {
|
||||
g.circle(v.x(s.x), v.y(s.y), 7).fill({ color: 0xcfeeff });
|
||||
}
|
||||
this.labelContainer.removeChildren();
|
||||
this.hitNodes.removeChildren();
|
||||
for (const s of galaxy.systems) {
|
||||
const label = new Text({ text: s.name, style: { fill: '#7fa8c9', fontSize: 11, fontFamily: 'system-ui' } });
|
||||
label.position.set(v.x(s.x) + 10, v.y(s.y) - 6);
|
||||
this.labelContainer.addChild(label);
|
||||
|
||||
const hit = new Graphics().circle(0, 0, 16).fill({ color: 0xffffff, alpha: 0.001 });
|
||||
hit.position.set(v.x(s.x), v.y(s.y));
|
||||
hit.eventMode = 'static';
|
||||
hit.cursor = 'pointer';
|
||||
hit.on('pointertap', () => this.handleSystemTap(s.id));
|
||||
this.hitNodes.addChild(hit);
|
||||
}
|
||||
}
|
||||
|
||||
handleSystemTap(systemId) {
|
||||
const sim = this.world;
|
||||
const cur = playerSystem(sim);
|
||||
// Clicking a neighbor = jump intent; clicking anything = intel selection.
|
||||
const lane = sim.galaxy.lanes.find(
|
||||
(l) => (l.a === cur && l.b === systemId) || (l.b === cur && l.a === systemId)
|
||||
);
|
||||
if (lane) this.onJump(lane.id);
|
||||
this.onSelectSystem(systemId);
|
||||
}
|
||||
|
||||
lanePoint(lane, fromSys, f) {
|
||||
const { galaxy } = this.world;
|
||||
const a = galaxy.systems[fromSys];
|
||||
const b = galaxy.systems[otherEnd(lane, fromSys)];
|
||||
return { x: this.v.x(a.x + (b.x - a.x) * f), y: this.v.y(a.y + (b.y - a.y) * f) };
|
||||
}
|
||||
|
||||
update() {
|
||||
const sim = this.world;
|
||||
const v = this.v;
|
||||
const cur = playerSystem(sim);
|
||||
|
||||
// Probability clouds: glowing strokes along lane segments, alpha ~ weight.
|
||||
const cg = this.cloudG;
|
||||
cg.clear();
|
||||
for (const cloud of playerClouds(sim, cur)) {
|
||||
for (const seg of cloud.segs) {
|
||||
const lane = sim.galaxy.lanes[seg.laneId];
|
||||
const p0 = this.lanePoint(lane, seg.fromSys, seg.f0);
|
||||
const p1 = this.lanePoint(lane, seg.fromSys, seg.f1);
|
||||
const alpha = Math.min(0.85, 0.18 + seg.w * 0.9);
|
||||
cg.moveTo(p0.x, p0.y).lineTo(p1.x, p1.y)
|
||||
.stroke({ width: 7, color: 0xffa94d, alpha });
|
||||
}
|
||||
}
|
||||
|
||||
// Player marker: bright cyan triangle at current position.
|
||||
const dg = this.dynamicG;
|
||||
dg.clear();
|
||||
const pos = this.playerXY();
|
||||
if (pos) {
|
||||
dg.poly([pos.x, pos.y - 8, pos.x + 6, pos.y + 6, pos.x - 6, pos.y + 6]).fill({ color: 0x4dffd2 });
|
||||
}
|
||||
// Halo on the player's current system.
|
||||
if (cur != null) {
|
||||
const s = sim.galaxy.systems[cur];
|
||||
dg.circle(v.x(s.x), v.y(s.y), 12).stroke({ width: 2, color: 0x4dffd2, alpha: 0.8 });
|
||||
}
|
||||
}
|
||||
|
||||
playerXY() {
|
||||
const sim = this.world;
|
||||
const loc = sim.player.loc;
|
||||
const v = this.v;
|
||||
if (loc.kind === 'lane') {
|
||||
const lane = sim.galaxy.lanes[loc.laneId];
|
||||
const f = Math.min(1, (sim.t - loc.departT) / (loc.arriveT - loc.departT));
|
||||
return this.lanePoint(lane, loc.fromSys, f);
|
||||
}
|
||||
const sysId = playerSystem(sim);
|
||||
if (sysId == null) return null;
|
||||
const s = sim.galaxy.systems[sysId];
|
||||
return { x: v.x(s.x), y: v.y(s.y) };
|
||||
}
|
||||
|
||||
setVisible(on) {
|
||||
this.container.visible = on;
|
||||
this.labelContainer.visible = on;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user