From 61244453e3350ff49acb5a99a822f0825ae1c53a Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Fri, 12 Jun 2026 14:47:51 +0000 Subject: [PATCH] feat: neon galaxy view with bloom, hazard tints, clouds --- src/main.js | 18 +++++- src/render/app.js | 33 ++++++++++ src/render/galaxyView.js | 128 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/render/app.js create mode 100644 src/render/galaxyView.js diff --git a/src/main.js b/src/main.js index 9597bc6..f73cd26 100644 --- a/src/main.js +++ b/src/main.js @@ -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(); +}); diff --git a/src/render/app.js b/src/render/app.js new file mode 100644 index 0000000..d82af1a --- /dev/null +++ b/src/render/app.js @@ -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 }; +} diff --git a/src/render/galaxyView.js b/src/render/galaxyView.js new file mode 100644 index 0000000..4c6f942 --- /dev/null +++ b/src/render/galaxyView.js @@ -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; + } +}