This commit is contained in:
parent
a3b06062ff
commit
b2d893a225
3 changed files with 368 additions and 117 deletions
|
|
@ -21,7 +21,7 @@ import { RainScene } from './scenes/rain/rain-scene';
|
||||||
import { TunnelScene } from './scenes/tunnel-scene';
|
import { TunnelScene } from './scenes/tunnel-scene';
|
||||||
import './styles/index.scss';
|
import './styles/index.scss';
|
||||||
|
|
||||||
const scenes = [TunnelScene, MetaballScene, OrbitScene, RainScene, BlobScene];
|
const scenes = [TunnelScene, MetaballScene, RainScene, RainScene, BlobScene, OrbitScene];
|
||||||
Random.seed = 2;
|
Random.seed = 2;
|
||||||
|
|
||||||
glMatrix.setMatrixArrayType(Array);
|
glMatrix.setMatrixArrayType(Array);
|
||||||
|
|
|
||||||
|
|
@ -2,47 +2,159 @@ import { mat2d, vec2 } from 'gl-matrix';
|
||||||
import { Drawable, DrawableDescriptor } from 'sdf-2d';
|
import { Drawable, DrawableDescriptor } from 'sdf-2d';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A rocky celestial body: a circle whose radius is modulated by a few angular
|
* Every visual style a body can take. The style selects both the outline
|
||||||
* harmonics so its outline looks bumpy. One descriptor renders every body in
|
* (smooth disc, jagged asteroid, ring annulus) and the per-fragment surface
|
||||||
* the scene (sun, planets, asteroids) — each instance carries its own palette
|
* colouring inside the SDF shader — gradients, bands, continents — instead of
|
||||||
* colour, size, surface `seed` (rotate it over time to make the body spin) and
|
* a single flat palette colour.
|
||||||
* `roughness` (how jagged the outline is: ~0.5 for a planet, ~2.5 for an
|
*/
|
||||||
* asteroid).
|
export const BodyStyle = {
|
||||||
|
sun: 0, // radial white-hot → gold → ember gradient with simmering granules
|
||||||
|
rocky: 1, // two-tone continent blotches
|
||||||
|
banded: 2, // wavy gas-giant stripes
|
||||||
|
ice: 3, // swirling azure sheen
|
||||||
|
asteroid: 4, // jagged outline, speckled surface
|
||||||
|
rings: 5, // two concentric annuli (drawn around a planet)
|
||||||
|
moon: 6, // small cratered grey
|
||||||
|
star: 7, // tiny twinkle, colour boosted far above 1 to stay bright after
|
||||||
|
// being multiplied by the dim ambient lighting
|
||||||
|
} as const;
|
||||||
|
export type BodyStyleName = keyof typeof BodyStyle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One celestial body: sun, planet, moon, ring system, asteroid, comet
|
||||||
|
* particle, or background star. A single descriptor renders all of them.
|
||||||
|
*
|
||||||
|
* Each instance carries two palette colour indices (mixed per fragment by its
|
||||||
|
* style), a `seed` that animates its surface, and `litAngle` — the direction
|
||||||
|
* towards the sun — used to shade a day/night terminator. Outline roughness is
|
||||||
|
* derived from the style inside the shader.
|
||||||
*/
|
*/
|
||||||
export class Body extends Drawable {
|
export class Body extends Drawable {
|
||||||
// The peak fraction by which the harmonics below can push the outline
|
// The outline harmonics can push the surface outward by at most
|
||||||
// outward (0.06 + 0.04 + 0.02), used for a conservative culling bound.
|
// 0.12 × roughness, and the roughest style (asteroid) uses 2.6.
|
||||||
private static readonly MAX_WOBBLE = 0.12;
|
private static readonly MAX_OUTLINE_GROWTH = 1.45;
|
||||||
|
|
||||||
public static descriptor: DrawableDescriptor = {
|
public static descriptor: DrawableDescriptor = {
|
||||||
sdf: {
|
sdf: {
|
||||||
shader: `
|
shader: `
|
||||||
uniform vec2 bodyCenters[BODY_COUNT];
|
uniform vec2 bodyCenters[BODY_COUNT];
|
||||||
uniform float bodyRadii[BODY_COUNT];
|
uniform float bodyRadii[BODY_COUNT];
|
||||||
uniform float bodyColorIndices[BODY_COUNT];
|
uniform float bodyColors[BODY_COUNT];
|
||||||
uniform float bodySeeds[BODY_COUNT];
|
uniform float bodySeeds[BODY_COUNT];
|
||||||
uniform float bodyRoughness[BODY_COUNT];
|
uniform float bodyStyles[BODY_COUNT];
|
||||||
|
uniform float bodyLitAngles[BODY_COUNT];
|
||||||
|
|
||||||
|
float bodyNoise(vec2 p) {
|
||||||
|
return sin(p.x) * sin(p.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 bodySurfaceColor(
|
||||||
|
int style,
|
||||||
|
vec2 d,
|
||||||
|
float radius,
|
||||||
|
float seed,
|
||||||
|
float litAngle,
|
||||||
|
vec3 colorA,
|
||||||
|
vec3 colorB
|
||||||
|
) {
|
||||||
|
vec2 q = d / radius;
|
||||||
|
float r01 = length(q);
|
||||||
|
|
||||||
|
if (style == ${BodyStyle.sun}) {
|
||||||
|
// Three drifting interference lattices; the first two sit at
|
||||||
|
// nearby frequencies and slide in opposite directions, so the
|
||||||
|
// convection cells continuously merge, split and boil instead of
|
||||||
|
// gliding across the disc as one rigid pattern.
|
||||||
|
float granules =
|
||||||
|
0.8 * bodyNoise(q * 8.0 + vec2(seed, -seed * 0.6)) +
|
||||||
|
0.55 * bodyNoise(q * 8.9 - vec2(seed * 0.8, seed * 1.1)) +
|
||||||
|
0.45 * bodyNoise(q * 15.0 + vec2(seed * 1.4, -seed * 0.9));
|
||||||
|
vec3 c = mix(vec3(1.4, 1.32, 1.1), colorA, smoothstep(0.0, 0.6, r01));
|
||||||
|
c = mix(c, colorB, smoothstep(0.5, 1.0, r01 + granules * 0.15));
|
||||||
|
return c * (1.0 + 0.2 * granules);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style == ${BodyStyle.star}) {
|
||||||
|
return colorA * (3.75 + 1.75 * sin(seed));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every world shares one recipe: its surface coordinates rotate
|
||||||
|
// with the body's spin (seed grows over time), so stripes and
|
||||||
|
// blotches visibly roll around the disc, then a single field —
|
||||||
|
// radial ripples for rings, stripes for the banded and ice giants,
|
||||||
|
// blotches for everything rocky — mixes the two colours. The
|
||||||
|
// terminator and limb shading below use the unrotated coordinates,
|
||||||
|
// keeping the lighting locked to the sun while the surface turns.
|
||||||
|
vec2 qr = vec2(
|
||||||
|
q.x * cos(seed) - q.y * sin(seed),
|
||||||
|
q.x * sin(seed) + q.y * cos(seed)
|
||||||
|
);
|
||||||
|
float field;
|
||||||
|
if (style == ${BodyStyle.rings}) {
|
||||||
|
field = sin(r01 * 26.0 + seed);
|
||||||
|
} else if (style == ${BodyStyle.banded} || style == ${BodyStyle.ice}) {
|
||||||
|
field = sin(qr.y * 5.0 + seed * 0.3);
|
||||||
|
} else {
|
||||||
|
field = bodyNoise(qr * 3.4) + 0.6 * bodyNoise(qr * 7.3 + vec2(2.7, 1.3));
|
||||||
|
}
|
||||||
|
vec3 c = mix(colorB, colorA, smoothstep(-0.6, 0.6, field));
|
||||||
|
|
||||||
|
vec2 toSun = vec2(cos(litAngle), sin(litAngle));
|
||||||
|
float dayside = smoothstep(-0.85, 0.55, dot(q / max(r01, 1e-4), toSun));
|
||||||
|
c *= mix(0.22, 1.1, dayside);
|
||||||
|
if (style != ${BodyStyle.rings}) {
|
||||||
|
c *= 1.0 - 0.45 * smoothstep(0.5, 1.0, r01);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
float bodyMinDistance(vec2 target, out vec4 color) {
|
float bodyMinDistance(vec2 target, out vec4 color) {
|
||||||
color = readFromPalette(0);
|
color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
float minDistance = 1000.0;
|
float minDistance = 1000.0;
|
||||||
|
|
||||||
for (int i = 0; i < BODY_COUNT; i++) {
|
for (int i = 0; i < BODY_COUNT; i++) {
|
||||||
vec2 d = target - bodyCenters[i];
|
vec2 d = target - bodyCenters[i];
|
||||||
float angle = atan(d.y, d.x);
|
float radius = max(bodyRadii[i], 1e-5);
|
||||||
|
int style = int(bodyStyles[i] + 0.5);
|
||||||
float seed = bodySeeds[i];
|
float seed = bodySeeds[i];
|
||||||
|
float dist;
|
||||||
|
|
||||||
|
if (style == ${BodyStyle.rings}) {
|
||||||
|
float len = length(d);
|
||||||
|
dist = min(
|
||||||
|
abs(len - radius * 0.68) - radius * 0.1,
|
||||||
|
abs(len - radius * 0.9) - radius * 0.06
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
float angle = atan(d.y, d.x);
|
||||||
|
float roughness = style == ${BodyStyle.sun}
|
||||||
|
? 0.5
|
||||||
|
: style == ${BodyStyle.asteroid}
|
||||||
|
? 2.6
|
||||||
|
: style == ${BodyStyle.rocky}
|
||||||
|
? 0.25
|
||||||
|
: style == ${BodyStyle.moon}
|
||||||
|
? 0.35
|
||||||
|
: 0.06;
|
||||||
float wobble =
|
float wobble =
|
||||||
sin(angle * 3.0 + seed) * 0.06 +
|
sin(angle * 3.0 + seed) * 0.06 +
|
||||||
sin(angle * 7.0 + seed * 1.7) * 0.04 +
|
sin(angle * 7.0 + seed * 1.7) * 0.04 +
|
||||||
sin(angle * 17.0 - seed * 0.5) * 0.02;
|
sin(angle * 17.0 - seed * 0.5) * 0.02;
|
||||||
|
dist = length(d) - radius * (1.0 + wobble * roughness);
|
||||||
float dist =
|
}
|
||||||
length(d) - bodyRadii[i] * (1.0 + wobble * bodyRoughness[i]);
|
|
||||||
|
|
||||||
if (dist < minDistance) {
|
if (dist < minDistance) {
|
||||||
minDistance = dist;
|
minDistance = dist;
|
||||||
color = readFromPalette(int(bodyColorIndices[i] + 0.5));
|
float packedColor = bodyColors[i];
|
||||||
|
float indexA = floor(packedColor / 32.0);
|
||||||
|
vec3 colorA = readFromPalette(int(indexA + 0.5)).rgb;
|
||||||
|
vec3 colorB = readFromPalette(int(packedColor - indexA * 32.0 + 0.5)).rgb;
|
||||||
|
color = vec4(
|
||||||
|
bodySurfaceColor(
|
||||||
|
style, d, radius, seed, bodyLitAngles[i], colorA, colorB
|
||||||
|
),
|
||||||
|
1.0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,41 +166,50 @@ export class Body extends Drawable {
|
||||||
propertyUniformMapping: {
|
propertyUniformMapping: {
|
||||||
center: 'bodyCenters',
|
center: 'bodyCenters',
|
||||||
radius: 'bodyRadii',
|
radius: 'bodyRadii',
|
||||||
colorIndex: 'bodyColorIndices',
|
colors: 'bodyColors',
|
||||||
seed: 'bodySeeds',
|
seed: 'bodySeeds',
|
||||||
roughness: 'bodyRoughness',
|
style: 'bodyStyles',
|
||||||
|
litAngle: 'bodyLitAngles',
|
||||||
},
|
},
|
||||||
uniformCountMacroName: 'BODY_COUNT',
|
uniformCountMacroName: 'BODY_COUNT',
|
||||||
shaderCombinationSteps: [0, 4, 8, 16, 32],
|
shaderCombinationSteps: [0, 4, 8, 16, 32],
|
||||||
empty: new Body(vec2.create(), 0, 0, 0, 0),
|
empty: new Body(vec2.create(), 0, 'star', 0, 0, 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public center: vec2,
|
public center: vec2,
|
||||||
public radius: number,
|
public radius: number,
|
||||||
public colorIndex: number,
|
public style: BodyStyleName,
|
||||||
|
public colorA: number,
|
||||||
|
public colorB: number,
|
||||||
public seed: number,
|
public seed: number,
|
||||||
public roughness: number
|
public litAngle = 0
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public minDistance(target: vec2): number {
|
public minDistance(target: vec2): number {
|
||||||
// Conservative: bound by the most outward the bumpy outline can reach so a
|
// Conservative: bound by the most outward the bumpiest outline can reach
|
||||||
// body is never culled from a tile its surface might poke into.
|
// so a body is never culled from a tile its surface might poke into.
|
||||||
return (
|
return vec2.dist(this.center, target) - this.radius * Body.MAX_OUTLINE_GROWTH;
|
||||||
vec2.dist(this.center, target) -
|
|
||||||
this.radius * (1 + Body.MAX_WOBBLE * this.roughness)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {
|
protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {
|
||||||
|
// litAngle is a world-space direction: push it through the linear part of
|
||||||
|
// the transform so it survives the view mapping's scale and y-flip.
|
||||||
|
const dx = Math.cos(this.litAngle);
|
||||||
|
const dy = Math.sin(this.litAngle);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
center: vec2.transformMat2d(vec2.create(), this.center, transform2d),
|
center: vec2.transformMat2d(vec2.create(), this.center, transform2d),
|
||||||
radius: this.radius * transform1d,
|
radius: this.radius * transform1d,
|
||||||
colorIndex: this.colorIndex,
|
colors: this.colorA * 32 + this.colorB,
|
||||||
seed: this.seed,
|
seed: this.seed,
|
||||||
roughness: this.roughness,
|
style: BodyStyle[this.style],
|
||||||
|
litAngle: Math.atan2(
|
||||||
|
transform2d[1] * dx + transform2d[3] * dy,
|
||||||
|
transform2d[0] * dx + transform2d[2] * dy
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,91 +4,201 @@ import { prettyPrint } from '../../helper/pretty-print';
|
||||||
import { Random } from '../../helper/random';
|
import { Random } from '../../helper/random';
|
||||||
import { settings } from '../../settings';
|
import { settings } from '../../settings';
|
||||||
import { Scene } from '../scene';
|
import { Scene } from '../scene';
|
||||||
import { Body } from './body';
|
import { Body, BodyStyleName } from './body';
|
||||||
|
|
||||||
const PLANET_COUNT = 6;
|
// Every colour the scene uses; bodies reference entries by name and mix their
|
||||||
const ASTEROID_COUNT = 18;
|
// two colours per fragment in the shader, so nothing renders as a flat fill.
|
||||||
|
const palette = {
|
||||||
|
white: rgb(1, 1, 1),
|
||||||
|
sunGold: hsl(38, 100, 62),
|
||||||
|
sunEmber: hsl(14, 95, 46),
|
||||||
|
scorchedTan: hsl(32, 22, 60),
|
||||||
|
scorchedBrown: hsl(20, 25, 38),
|
||||||
|
venusCream: hsl(46, 85, 78),
|
||||||
|
venusAmber: hsl(33, 75, 58),
|
||||||
|
earthLand: hsl(135, 50, 45),
|
||||||
|
earthOcean: hsl(213, 85, 52),
|
||||||
|
marsRust: hsl(16, 80, 55),
|
||||||
|
marsShadow: hsl(22, 65, 34),
|
||||||
|
jovianCream: hsl(36, 70, 75),
|
||||||
|
jovianRust: hsl(16, 65, 52),
|
||||||
|
saturnSand: hsl(45, 65, 76),
|
||||||
|
saturnDust: hsl(36, 50, 58),
|
||||||
|
ringGold: hsl(44, 40, 70),
|
||||||
|
ringShadow: hsl(40, 28, 42),
|
||||||
|
iceAzure: hsl(200, 90, 68),
|
||||||
|
iceDeep: hsl(232, 70, 50),
|
||||||
|
moonGrey: hsl(220, 10, 68),
|
||||||
|
moonShadow: hsl(225, 12, 44),
|
||||||
|
};
|
||||||
|
type PaletteColor = keyof typeof palette;
|
||||||
|
const paletteIndex = (name: PaletteColor): number => Object.keys(palette).indexOf(name);
|
||||||
|
|
||||||
// Palette indices used for the bodies (see colorPalette below).
|
interface MoonSpec {
|
||||||
const SUN_COLOR = 5; // warm gold
|
orbit: number; // around the parent planet, as a fraction of the system radius
|
||||||
const ROCK_COLOR = 7; // brown-grey rock
|
size: number;
|
||||||
const PLANET_COLORS = [1, 2, 3, 4, 6]; // red, green, blue, purple, pink
|
speed: number; // rad/s
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlanetSpec {
|
||||||
|
orbit: number; // fraction of the system radius
|
||||||
|
size: number;
|
||||||
|
style: BodyStyleName;
|
||||||
|
colors: [PaletteColor, PaletteColor];
|
||||||
|
spin: number; // how fast the surface pattern drifts
|
||||||
|
ringSize?: number;
|
||||||
|
moons?: Array<MoonSpec>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A miniature solar system: small rocky worlds inside, then the giants —
|
||||||
|
// banded, ringed, and an ice giant — spread roughly evenly from the sun's
|
||||||
|
// doorstep to the screen edge. Planets all orbit prograde at Kepler speeds
|
||||||
|
// (∝ orbit⁻¹·⁵).
|
||||||
|
const PLANETS: Array<PlanetSpec> = [
|
||||||
|
{
|
||||||
|
orbit: 0.165,
|
||||||
|
size: 0.026,
|
||||||
|
style: 'rocky',
|
||||||
|
colors: ['scorchedTan', 'scorchedBrown'],
|
||||||
|
spin: 0.55,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orbit: 0.27,
|
||||||
|
size: 0.036,
|
||||||
|
style: 'banded',
|
||||||
|
colors: ['venusCream', 'venusAmber'],
|
||||||
|
spin: 0.4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orbit: 0.385,
|
||||||
|
size: 0.04,
|
||||||
|
style: 'rocky',
|
||||||
|
colors: ['earthLand', 'earthOcean'],
|
||||||
|
spin: 0.45,
|
||||||
|
moons: [{ orbit: 0.06, size: 0.01, speed: 2.4 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orbit: 0.49,
|
||||||
|
size: 0.03,
|
||||||
|
style: 'rocky',
|
||||||
|
colors: ['marsRust', 'marsShadow'],
|
||||||
|
spin: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orbit: 0.7,
|
||||||
|
size: 0.06,
|
||||||
|
style: 'banded',
|
||||||
|
colors: ['jovianCream', 'jovianRust'],
|
||||||
|
spin: 0.3,
|
||||||
|
moons: [
|
||||||
|
{ orbit: 0.085, size: 0.011, speed: 1.8 },
|
||||||
|
{ orbit: 0.108, size: 0.009, speed: 1.2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orbit: 0.825,
|
||||||
|
size: 0.042,
|
||||||
|
style: 'banded',
|
||||||
|
colors: ['saturnSand', 'saturnDust'],
|
||||||
|
spin: 0.32,
|
||||||
|
ringSize: 0.082,
|
||||||
|
},
|
||||||
|
{ orbit: 0.95, size: 0.036, style: 'ice', colors: ['iceAzure', 'iceDeep'], spin: 0.45 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ORBITAL_SPEED = 0.26; // rad/s at orbit = 1; scaled by Kepler's third law
|
||||||
|
|
||||||
interface OrbitingBody {
|
interface OrbitingBody {
|
||||||
drawable: InstanceType<typeof Body>;
|
drawable: Body;
|
||||||
radiusRatio: number; // orbit radius as a fraction of the system radius
|
orbit: number; // around the sun — or around `parent`, for moons and rings
|
||||||
phase: number;
|
phase: number;
|
||||||
speed: number;
|
speed: number;
|
||||||
sizeRatio: number; // body radius as a fraction of the system radius
|
size: number;
|
||||||
spin: number; // how fast the surface seed rotates
|
spin: number;
|
||||||
seedPhase: number;
|
seedPhase: number;
|
||||||
|
parent?: OrbitingBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OrbitScene implements Scene {
|
export class OrbitScene implements Scene {
|
||||||
private overlay: HTMLDivElement;
|
private overlay: HTMLDivElement;
|
||||||
public insights?: any;
|
public insights?: any;
|
||||||
|
|
||||||
// The sun is a bright solid body so it's actually visible: in this engine a
|
// The sun's visible disc carries its own white-hot → ember gradient; these
|
||||||
// light only shows where it reflects off a surface, so a bare light over the
|
// lights add the blown-out core, the warm corona reflecting off the lifted
|
||||||
// near-black "space" background would be invisible. The light sources sit at
|
// background, and the sunlight (plus shadows) on everything orbiting.
|
||||||
// its centre and illuminate the orbiting bodies. Because the sun is small
|
private sunCore = new CircleLight(vec2.create(), rgb(1.0, 0.96, 0.88), 0.1);
|
||||||
// relative to the orbits, it only mildly occludes its own light.
|
private sunGlow = new CircleLight(vec2.create(), rgb(1.35, 0.58, 0.18), 0.5);
|
||||||
private sun = new Body(vec2.create(), 0, SUN_COLOR, 0, 0.6);
|
|
||||||
private sunCore = new CircleLight(vec2.create(), rgb(1.0, 0.96, 0.85), 0.05);
|
private sun: OrbitingBody = {
|
||||||
private sunGlow = new CircleLight(vec2.create(), rgb(1.0, 0.62, 0.26), 0.55);
|
drawable: new Body(
|
||||||
|
vec2.create(),
|
||||||
|
0,
|
||||||
|
'sun',
|
||||||
|
paletteIndex('sunGold'),
|
||||||
|
paletteIndex('sunEmber'),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
orbit: 0,
|
||||||
|
phase: 0,
|
||||||
|
speed: 0,
|
||||||
|
size: 0.115,
|
||||||
|
spin: 0.9,
|
||||||
|
seedPhase: Random.getRandom() * 10,
|
||||||
|
};
|
||||||
|
|
||||||
private bodies: Array<OrbitingBody> = [];
|
private bodies: Array<OrbitingBody> = [];
|
||||||
|
|
||||||
public async run(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void> {
|
public async run(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void> {
|
||||||
this.overlay = overlay;
|
this.overlay = overlay;
|
||||||
|
|
||||||
for (let i = 0; i < PLANET_COUNT; i++) {
|
this.bodies.push(this.sun);
|
||||||
const radiusRatio = 0.26 + (0.64 / PLANET_COUNT) * i;
|
|
||||||
|
PLANETS.forEach((spec) => {
|
||||||
|
const planet = this.createOrbitingBody(
|
||||||
|
spec.orbit,
|
||||||
|
spec.size,
|
||||||
|
spec.style,
|
||||||
|
spec.colors,
|
||||||
|
spec.spin
|
||||||
|
);
|
||||||
|
this.bodies.push(planet);
|
||||||
|
|
||||||
|
if (spec.ringSize) {
|
||||||
this.bodies.push({
|
this.bodies.push({
|
||||||
drawable: new Body(
|
...this.createOrbitingBody(
|
||||||
vec2.create(),
|
|
||||||
0,
|
0,
|
||||||
PLANET_COLORS[i % PLANET_COLORS.length],
|
spec.ringSize,
|
||||||
Random.getRandom() * 10,
|
'rings',
|
||||||
1.0
|
['ringGold', 'ringShadow'],
|
||||||
|
0
|
||||||
),
|
),
|
||||||
radiusRatio,
|
parent: planet,
|
||||||
phase: Random.getRandom() * Math.PI * 2,
|
|
||||||
speed: 0.5 / Math.pow(radiusRatio, 1.5),
|
|
||||||
sizeRatio: 0.04 + 0.022 * (i % 3),
|
|
||||||
spin: Random.getRandomInRange(-0.5, 0.5),
|
|
||||||
seedPhase: Random.getRandom() * 10,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// An asteroid belt: a ring of small, jagged, fast-tumbling rocks.
|
spec.moons?.forEach((moon) => {
|
||||||
for (let i = 0; i < ASTEROID_COUNT; i++) {
|
|
||||||
const radiusRatio = Random.getRandomInRange(0.5, 0.62);
|
|
||||||
this.bodies.push({
|
this.bodies.push({
|
||||||
drawable: new Body(
|
...this.createOrbitingBody(
|
||||||
vec2.create(),
|
moon.orbit,
|
||||||
0,
|
moon.size,
|
||||||
ROCK_COLOR,
|
'moon',
|
||||||
Random.getRandom() * 10,
|
['moonGrey', 'moonShadow'],
|
||||||
2.6
|
0.8
|
||||||
),
|
),
|
||||||
radiusRatio,
|
speed: moon.speed,
|
||||||
phase: Random.getRandom() * Math.PI * 2,
|
parent: planet,
|
||||||
speed: 0.5 / Math.pow(radiusRatio, 1.5),
|
});
|
||||||
sizeRatio: Random.getRandomInRange(0.014, 0.026),
|
});
|
||||||
spin: (Random.getRandom() > 0.5 ? 1 : -1) * Random.getRandomInRange(0.8, 2.2),
|
|
||||||
seedPhase: Random.getRandom() * 10,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const bodyCount = this.bodies.length + 1; // + the sun
|
const bodyCount = this.bodies.length;
|
||||||
|
|
||||||
await runAnimation(
|
await runAnimation(
|
||||||
canvas,
|
canvas,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
...Body.descriptor,
|
...Body.descriptor,
|
||||||
shaderCombinationSteps: [0, 4, 8, 16, bodyCount],
|
shaderCombinationSteps: [...new Set([0, 4, 8, 16, 32, bodyCount])],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...CircleLight.descriptor,
|
...CircleLight.descriptor,
|
||||||
|
|
@ -98,23 +208,40 @@ export class OrbitScene implements Scene {
|
||||||
this.drawNextFrame.bind(this),
|
this.drawNextFrame.bind(this),
|
||||||
{
|
{
|
||||||
enableHighDpiRendering: true,
|
enableHighDpiRendering: true,
|
||||||
motionBlur: 0.6,
|
motionBlur: 0.5,
|
||||||
ambientLight: rgb(0.17, 0.17, 0.23),
|
lightPenetrationRatio: 0.8,
|
||||||
backgroundColor: rgb(0.02, 0.02, 0.05),
|
ambientLight: rgb(0.07, 0.075, 0.145),
|
||||||
colorPalette: [
|
backgroundColor: rgb(0.035, 0.035, 0.07),
|
||||||
rgb(1, 1, 1), // 0 white (unused fallback)
|
colorPalette: Object.values(palette),
|
||||||
hsl(8, 85, 62), // 1 red
|
|
||||||
hsl(150, 65, 55), // 2 green
|
|
||||||
hsl(205, 85, 62), // 3 blue
|
|
||||||
hsl(275, 70, 68), // 4 purple
|
|
||||||
hsl(40, 95, 60), // 5 gold (sun)
|
|
||||||
hsl(330, 80, 65), // 6 pink
|
|
||||||
hsl(28, 25, 45), // 7 brown-grey rock
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createOrbitingBody(
|
||||||
|
orbit: number,
|
||||||
|
size: number,
|
||||||
|
style: BodyStyleName,
|
||||||
|
colors: [PaletteColor, PaletteColor],
|
||||||
|
spin: number
|
||||||
|
): OrbitingBody {
|
||||||
|
return {
|
||||||
|
drawable: new Body(
|
||||||
|
vec2.create(),
|
||||||
|
0,
|
||||||
|
style,
|
||||||
|
paletteIndex(colors[0]),
|
||||||
|
paletteIndex(colors[1]),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
orbit,
|
||||||
|
phase: Random.getRandom() * Math.PI * 2,
|
||||||
|
speed: orbit > 0 ? ORBITAL_SPEED / Math.pow(orbit, 1.5) : 0,
|
||||||
|
size,
|
||||||
|
spin,
|
||||||
|
seedPhase: Random.getRandom() * 10,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private drawNextFrame(
|
private drawNextFrame(
|
||||||
renderer: Renderer,
|
renderer: Renderer,
|
||||||
currentTime: DOMHighResTimeStamp,
|
currentTime: DOMHighResTimeStamp,
|
||||||
|
|
@ -139,27 +266,30 @@ export class OrbitScene implements Scene {
|
||||||
const maxRadius = Math.min(viewAreaWidth, viewAreaHeight) / 2;
|
const maxRadius = Math.min(viewAreaWidth, viewAreaHeight) / 2;
|
||||||
const time = currentTime / 1000;
|
const time = currentTime / 1000;
|
||||||
|
|
||||||
// The sun sits in the middle; the light sources live at its centre and a
|
|
||||||
// gentle pulse makes the corona breathe.
|
|
||||||
vec2.copy(this.sun.center, center);
|
|
||||||
this.sun.radius = maxRadius * 0.13;
|
|
||||||
this.sun.seed = time * 0.15;
|
|
||||||
vec2.copy(this.sunCore.center, center);
|
vec2.copy(this.sunCore.center, center);
|
||||||
vec2.copy(this.sunGlow.center, center);
|
vec2.copy(this.sunGlow.center, center);
|
||||||
this.sunGlow.intensity = 0.55 + 0.05 * Math.sin(time * 1.7);
|
this.sunGlow.intensity = 0.5 + 0.05 * Math.sin(time * 1.7);
|
||||||
|
|
||||||
|
// Parents are pushed before their moons and rings, so each parent's
|
||||||
|
// position is already up to date when its satellites read it.
|
||||||
this.bodies.forEach((body) => {
|
this.bodies.forEach((body) => {
|
||||||
const angle = body.phase + time * body.speed;
|
const angle = body.phase + time * body.speed;
|
||||||
|
const origin = body.parent?.drawable.center ?? center;
|
||||||
vec2.set(
|
vec2.set(
|
||||||
body.drawable.center,
|
body.drawable.center,
|
||||||
center.x + Math.cos(angle) * body.radiusRatio * maxRadius,
|
origin[0] + Math.cos(angle) * body.orbit * maxRadius,
|
||||||
center.y + Math.sin(angle) * body.radiusRatio * maxRadius
|
origin[1] + Math.sin(angle) * body.orbit * maxRadius
|
||||||
);
|
);
|
||||||
body.drawable.radius = body.sizeRatio * maxRadius;
|
// The sun breathes gently; everything else keeps its size.
|
||||||
|
const pulse = body === this.sun ? 1 + 0.015 * Math.sin(time * 2.4) : 1;
|
||||||
|
body.drawable.radius = body.size * maxRadius * pulse;
|
||||||
body.drawable.seed = body.seedPhase + time * body.spin;
|
body.drawable.seed = body.seedPhase + time * body.spin;
|
||||||
|
body.drawable.litAngle = Math.atan2(
|
||||||
|
center[1] - body.drawable.center[1],
|
||||||
|
center[0] - body.drawable.center[0]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
renderer.addDrawable(this.sun);
|
|
||||||
this.bodies.forEach((body) => renderer.addDrawable(body.drawable));
|
this.bodies.forEach((body) => renderer.addDrawable(body.drawable));
|
||||||
renderer.addDrawable(this.sunCore);
|
renderer.addDrawable(this.sunCore);
|
||||||
renderer.addDrawable(this.sunGlow);
|
renderer.addDrawable(this.sunGlow);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue