orbit/js/visuals/Starfield.js

129 lines
4.3 KiB
JavaScript
Raw Permalink 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.

import Phaser from '../vendor/phaser.js';
import { config } from '../config/Config.js';
import { toColor } from '../utils/Color.js';
const STAR_KEY = '__star';
const MIN_SCALE = 0.4; // smallest star
const MAX_SCALE = 1.5; // largest star
const MARGIN = 8; // px of starfield beyond the camera view, each side
/**
* Decorative parallax starfield for infinite space.
*
* All motion is camera-driven (the camera follows the ship — see
* GameScene.updateCamera). Each frame the field is shifted by (1 p) of
* the camera delta, so on screen the stars stream opposite to the
* direction of travel, near ("bigger") stars faster than far ones
* (p = the star's parallax factor). Ship still → stars still.
*
* Because the world is unbounded, stars are kept in a window around the
* *current camera view* and wrapped back in when they leave it — the
* field stays full and evenly distributed no matter how far the ship
* flies.
*
* Tuned by data/game.json → starfield { enabled, count, parallax, colors }.
*/
export class Starfield {
constructor(scene) {
this.scene = scene;
this.cfg = config.get('game.starfield', {});
const p = this.cfg.parallax;
this.parallax = Array.isArray(p) && p.length === 2 ? p : [0.15, 0.85];
this.stars = [];
this.lastScrollX = 0;
this.lastScrollY = 0;
}
create() {
if (!this.cfg.enabled) return;
const scene = this.scene;
const width = scene.scale.width;
const height = scene.scale.height;
const cam = scene.cameras.main;
// Tiny 4x4 white dot, shared by every star.
if (!scene.textures.exists(STAR_KEY)) {
const g = scene.make.graphics({ add: false });
g.fillStyle(0xffffff, 1);
g.fillCircle(2, 2, 2);
g.generateTexture(STAR_KEY, 4, 4);
g.destroy();
}
const count = this.cfg.count ?? 200;
const colors = (this.cfg.colors ?? ['#ffffff']).map((c) => toColor(c));
const [parMin, parMax] = this.parallax;
// Stars start in a window around the current camera view; update()
// keeps them wrapped into it as the camera flies.
const left = cam.scrollX - MARGIN;
const top = cam.scrollY - MARGIN;
for (let i = 0; i < count; i++) {
const star = scene.add.image(
Phaser.Math.FloatBetween(left, left + width + 2 * MARGIN),
Phaser.Math.FloatBetween(top, top + height + 2 * MARGIN),
STAR_KEY,
);
// Bigger stars are "closer" → they parallax more.
const s = Phaser.Math.FloatBetween(MIN_SCALE, MAX_SCALE);
const t = (s - MIN_SCALE) / (MAX_SCALE - MIN_SCALE);
const parallax = Phaser.Math.Linear(parMin, parMax, t);
star.parallax = parallax;
star
.setScale(s)
.setAlpha(Phaser.Math.FloatBetween(0.2, 0.85))
.setTint(colors[Phaser.Math.Between(0, colors.length - 1)])
.setDepth(Math.round(2 * t)); // 0 = far … 2 = near (ship draws above, depth 10)
this.stars.push(star);
}
this.lastScrollX = cam.scrollX;
this.lastScrollY = cam.scrollY;
}
/**
* Shift the field opposite to the camera motion and wrap it back into
* the current view. Call once per frame, after the camera has moved.
*/
update() {
if (!this.cfg.enabled || this.stars.length === 0) return;
const cam = this.scene.cameras.main;
const dx = cam.scrollX - this.lastScrollX;
const dy = cam.scrollY - this.lastScrollY;
this.lastScrollX = cam.scrollX;
this.lastScrollY = cam.scrollY;
if (dx === 0 && dy === 0) return;
const spanX = this.scene.scale.width + 2 * MARGIN;
const spanY = this.scene.scale.height + 2 * MARGIN;
const left = cam.scrollX - MARGIN;
const top = cam.scrollY - MARGIN;
for (const star of this.stars) {
// Move by (1 p) of the camera delta, so on screen the star drifts
// by exactly p of it: opposite to travel, near stars faster.
star.x += dx * (1 - star.parallax);
star.y += dy * (1 - star.parallax);
// Keep the field full: wrap into the current camera window.
star.x = wrapIn(star.x, left, spanX);
star.y = wrapIn(star.y, top, spanY);
}
}
destroy() {
for (const star of this.stars) star.destroy();
this.stars = [];
}
}
/** Maps any coordinate into [left, left + span) — safe for huge deltas too. */
function wrapIn(v, left, span) {
return left + ((((v - left) % span) + span) % span);
}