orbit/js/visuals/WandererStar.js

142 lines
4.7 KiB
JavaScript

/**
* binary companion star — a distant "other star" wandering slowly.
*
* Content layer (two world-space sprites: a bright core + a soft halo),
* NOT a filter: Canvas-safe and immune to the HUD. It sits FAR away
* (very low parallax ~0.08, so it barely moves as you fly — a distant
* light source) and drifts along a slow elliptical arc, seeded per system
* so each binary's companion wanders its own way. Both sprites use
* additive blend — it is a source of light, so it glows, never occludes.
* Depth 3 keeps it behind the art (planets 5, ship 10).
*
* Config: data/systems.json → types.binary.effect.wanderer.
*/
import Phaser from '../vendor/phaser.js';
import { canvasTexture } from '../utils/Textures.js';
import { toColor } from '../utils/Color.js';
const DEPTH = 3; // behind the art, in front of the starfield
/** A tight, bright core — the star's body. White (tint gives the color). */
function drawCore(ctx, size) {
const c = size / 2;
const g = ctx.createRadialGradient(c, c, 0, c, c, c);
g.addColorStop(0.0, 'rgba(255,255,255,1.0)');
g.addColorStop(0.5, 'rgba(255,255,255,0.9)');
g.addColorStop(1.0, 'rgba(255,255,255,0.0)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, size, size);
}
/** A wide, soft halo — the star's corona. White (tint gives the color). */
function drawHalo(ctx, size) {
const c = size / 2;
const g = ctx.createRadialGradient(c, c, 0, c, c, c);
g.addColorStop(0.0, 'rgba(255,255,255,0.55)');
g.addColorStop(0.4, 'rgba(255,255,255,0.28)');
g.addColorStop(1.0, 'rgba(255,255,255,0.0)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, size, size);
}
/**
* The companion star (core + halo) wandering a slow seeded arc.
*
* @param {object} scene Phaser scene
* @param {string} color star color (CSS hex)
* @param {object} cfg data/systems.json → effect.wanderer
* @param {{phase?:number, angle?:number, drift?:number}} fx per-system seeded 0..1 variation
*/
export class WandererStar {
constructor(scene, color, cfg, fx = {}) {
this.scene = scene;
this.color = toColor(color, 0x9fd0ff);
this.cfg = cfg ?? {};
this.fx = fx ?? {};
this.core = null;
this.halo = null;
this.tSec = 0;
}
/** Build the core + halo (idempotent). */
create() {
const scene = this.scene;
if (this.core) return;
const {
coreSize = [40, 70],
haloSize = [120, 200],
parallax = 0.08,
speed = [3, 8],
} = this.cfg;
const coreKey = canvasTexture(scene, 'fx-wanderer-core', 64, 64, (ctx) => drawCore(ctx, 64));
const haloKey = canvasTexture(scene, 'fx-wanderer-halo', 128, 128, (ctx) => drawHalo(ctx, 128));
// Seeded starting point (upper third of the view — a star in the sky)
// and a slow elliptical arc (the "wander").
const fx = this.fx;
const phase = fx.phase ?? 0.5;
const angle = fx.angle ?? 0.5;
const drift = fx.drift ?? 0.5;
const w = scene.scale.width;
const h = scene.scale.height;
const baseX = w * (0.2 + 0.6 * phase);
const baseY = h * (0.15 + 0.3 * angle);
const ampX = w * (0.05 + 0.12 * drift);
const ampY = h * (0.04 + 0.10 * drift);
const omega = ((speed[0] + (speed[1] - speed[0]) * drift) / w) * Math.PI * 2; // rad/s (slow)
this.core = scene.add
.image(baseX, baseY, coreKey)
.setDepth(DEPTH)
.setTint(this.color)
.setAlpha(0.95)
.setBlendMode(Phaser.BlendModes.ADD);
const cs = coreSize[0] + (coreSize[1] - coreSize[0]) * drift;
this.core.setScale(cs / 64);
this.halo = scene.add
.image(baseX, baseY, haloKey)
.setDepth(DEPTH)
.setTint(this.color)
.setAlpha(0.8)
.setBlendMode(Phaser.BlendModes.ADD);
const hs = haloSize[0] + (haloSize[1] - haloSize[0]) * drift;
this.halo.setScale(hs / 128);
this.baseX = baseX;
this.baseY = baseY;
this.ampX = ampX;
this.ampY = ampY;
this.omega = omega;
this.phase0 = angle * Math.PI * 2;
this.p = parallax;
}
/** Per-frame: slow elliptical wander + camera parallax. */
update(nowMs, dtMs) {
if (!this.core) return;
this.tSec += (dtMs || 0) / 1000;
const t = this.tSec;
const a = this.omega * t + this.phase0;
let x = this.baseX + this.ampX * Math.cos(a);
let y = this.baseY + this.ampY * Math.sin(a);
// very low parallax (distant): shifts a little as the camera flies
const cam = this.scene.cameras.main;
const camX = cam?.worldView?.x ?? 0;
const camY = cam?.worldView?.y ?? 0;
x += (camX - this.scene.scale.width / 2) * this.p;
y += (camY - this.scene.scale.height / 2) * this.p;
this.core.setPosition(x, y);
this.halo.setPosition(x, y);
}
/** Tear down. */
destroy() {
this.core?.destroy();
this.halo?.destroy();
this.core = null;
this.halo = null;
}
}