orbit/js/visuals/WandererStar.js

188 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* binary companion star — a distant "other star" wandering slowly in the sky.
*
* Content layer (two world-space sprites: a bright core + a soft halo),
* NOT a filter: Canvas-safe and immune to the HUD. It is a FAR, distant
* light source: very low parallax (~0.08), so it barely drifts as you
* fly, and it wanders a slow elliptical arc seeded per system (each
* binary's companion walks its own path). 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).
*
* Placement (the fix for "it was off-screen"): the star is ANCHORED TO
* THE CAMERA VIEW, not to a fixed box near the system star. Each frame its
* view-space position is (home + wander + drift), wrapped back into the
* view (Starfield pattern) so it stays in the sky no matter how far the
* ship flies — then converted to world space as camera + offset. The
* drift trails the camera at (1 parallax), so on screen it slowly
* wanders opposite to travel — a distant star, never a lens sticker.
*
* 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
const MARGIN = 160; // keep the star's center this far inside the view, each side
/** 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, anchored
* to the current camera view.
*
* @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;
this.driftX = 0;
this.driftY = 0;
this.lastScrollX = 0;
this.lastScrollY = 0;
}
/** Build the core + halo (idempotent). */
create() {
const scene = this.scene;
if (this.core) return;
const {
coreSize = [60, 90],
haloSize = [200, 300],
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 HOME point in VIEW space (upper third of the view — a star in
// the sky) and a slow elliptical WANDER around it.
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 homeX = w * (0.2 + 0.6 * phase);
const homeY = h * (0.12 + 0.3 * angle);
const ampX = w * (0.05 + 0.12 * drift);
const ampY = h * (0.04 + 0.1 * drift);
const omega = ((speed[0] + (speed[1] - speed[0]) * drift) / w) * Math.PI * 2; // rad/s (slow)
this.homeX = homeX;
this.homeY = homeY;
this.ampX = ampX;
this.ampY = ampY;
this.omega = omega;
this.phase0 = angle * Math.PI * 2;
this.p = parallax;
// Start at the home point in world space (camera + view offset).
const cam = scene.cameras.main;
const x0 = (cam?.scrollX ?? 0) + homeX;
const y0 = (cam?.scrollY ?? 0) + homeY;
this.core = scene.add
.image(x0, y0, coreKey)
.setDepth(DEPTH)
.setTint(this.color)
.setAlpha(1.0)
.setBlendMode(Phaser.BlendModes.ADD);
const cs = coreSize[0] + (coreSize[1] - coreSize[0]) * drift;
this.core.setScale(cs / 64);
this.halo = scene.add
.image(x0, y0, haloKey)
.setDepth(DEPTH)
.setTint(this.color)
.setAlpha(0.85)
.setBlendMode(Phaser.BlendModes.ADD);
const hs = haloSize[0] + (haloSize[1] - haloSize[0]) * drift;
this.halo.setScale(hs / 128);
this.lastScrollX = cam?.scrollX ?? 0;
this.lastScrollY = cam?.scrollY ?? 0;
}
/**
* Per-frame: anchor to the current view, add the slow elliptical wander,
* trail the camera at (1 parallax), and wrap back into the view so the
* star never escapes the sky. Call after the camera has moved.
*/
update(nowMs, dtMs) {
if (!this.core) return;
const cam = this.scene.cameras.main;
const camX = cam?.scrollX ?? 0;
const camY = cam?.scrollY ?? 0;
// The camera delta; the star trails it at (1 p), so on screen it
// drifts by p of the motion (opposite to travel, like Starfield).
const dx = camX - this.lastScrollX;
const dy = camY - this.lastScrollY;
this.lastScrollX = camX;
this.lastScrollY = camY;
this.driftX -= dx * this.p;
this.driftY -= dy * this.p;
this.tSec += (dtMs || 0) / 1000;
const a = this.omega * this.tSec + this.phase0;
const w = this.scene.scale.width;
const h = this.scene.scale.height;
// View-space position, wrapped into the view (+ margin) — a distant
// star that wanders, but stays in the sky.
const vx = wrapIn(this.homeX + this.ampX * Math.cos(a) + this.driftX, -MARGIN, w + 2 * MARGIN);
const vy = wrapIn(this.homeY + this.ampY * Math.sin(a) + this.driftY, -MARGIN, h + 2 * MARGIN);
// World = camera + view offset.
const x = camX + vx;
const y = camY + vy;
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;
}
}
/** Maps any coordinate into [left, left + span) — safe for huge deltas too. */
function wrapIn(v, left, span) {
return left + ((((v - left) % span) + span) % span);
}