Compare commits

..

No commits in common. "b2d893a225ee86480052975d9c70cbcb0ab0230b" and "d8cbffcfd523460343c9b8e2304bf1189802d83e" have entirely different histories.

5 changed files with 139 additions and 392 deletions

View file

@ -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, RainScene, RainScene, BlobScene, OrbitScene]; const scenes = [TunnelScene, MetaballScene, OrbitScene, RainScene, BlobScene];
Random.seed = 2; Random.seed = 2;
glMatrix.setMatrixArrayType(Array); glMatrix.setMatrixArrayType(Array);
@ -82,7 +82,7 @@ const main = async () => {
try { try {
let i = 0; let i = 0;
for (; ;) { for (;;) {
const currentScene = new scenes[i++ % scenes.length](); const currentScene = new scenes[i++ % scenes.length]();
await currentScene.run(canvas, overlay); await currentScene.run(canvas, overlay);

View file

@ -2,159 +2,47 @@ import { mat2d, vec2 } from 'gl-matrix';
import { Drawable, DrawableDescriptor } from 'sdf-2d'; import { Drawable, DrawableDescriptor } from 'sdf-2d';
/** /**
* Every visual style a body can take. The style selects both the outline * A rocky celestial body: a circle whose radius is modulated by a few angular
* (smooth disc, jagged asteroid, ring annulus) and the per-fragment surface * harmonics so its outline looks bumpy. One descriptor renders every body in
* colouring inside the SDF shader gradients, bands, continents instead of * the scene (sun, planets, asteroids) each instance carries its own palette
* a single flat palette colour. * colour, size, surface `seed` (rotate it over time to make the body spin) and
*/ * `roughness` (how jagged the outline is: ~0.5 for a planet, ~2.5 for an
export const BodyStyle = { * asteroid).
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 outline harmonics can push the surface outward by at most // The peak fraction by which the harmonics below can push the outline
// 0.12 × roughness, and the roughest style (asteroid) uses 2.6. // outward (0.06 + 0.04 + 0.02), used for a conservative culling bound.
private static readonly MAX_OUTLINE_GROWTH = 1.45; private static readonly MAX_WOBBLE = 0.12;
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 bodyColors[BODY_COUNT]; uniform float bodyColorIndices[BODY_COUNT];
uniform float bodySeeds[BODY_COUNT]; uniform float bodySeeds[BODY_COUNT];
uniform float bodyStyles[BODY_COUNT]; uniform float bodyRoughness[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 = vec4(0.0, 0.0, 0.0, 1.0); color = readFromPalette(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 radius = max(bodyRadii[i], 1e-5); float angle = atan(d.y, d.x);
int style = int(bodyStyles[i] + 0.5);
float seed = bodySeeds[i]; float seed = bodySeeds[i];
float dist;
if (style == ${BodyStyle.rings}) { float wobble =
float len = length(d); sin(angle * 3.0 + seed) * 0.06 +
dist = min( sin(angle * 7.0 + seed * 1.7) * 0.04 +
abs(len - radius * 0.68) - radius * 0.1, sin(angle * 17.0 - seed * 0.5) * 0.02;
abs(len - radius * 0.9) - radius * 0.06
); float dist =
} else { length(d) - bodyRadii[i] * (1.0 + wobble * bodyRoughness[i]);
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 =
sin(angle * 3.0 + seed) * 0.06 +
sin(angle * 7.0 + seed * 1.7) * 0.04 +
sin(angle * 17.0 - seed * 0.5) * 0.02;
dist = length(d) - radius * (1.0 + wobble * roughness);
}
if (dist < minDistance) { if (dist < minDistance) {
minDistance = dist; minDistance = dist;
float packedColor = bodyColors[i]; color = readFromPalette(int(bodyColorIndices[i] + 0.5));
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
);
} }
} }
@ -166,50 +54,41 @@ export class Body extends Drawable {
propertyUniformMapping: { propertyUniformMapping: {
center: 'bodyCenters', center: 'bodyCenters',
radius: 'bodyRadii', radius: 'bodyRadii',
colors: 'bodyColors', colorIndex: 'bodyColorIndices',
seed: 'bodySeeds', seed: 'bodySeeds',
style: 'bodyStyles', roughness: 'bodyRoughness',
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, 'star', 0, 0, 0), empty: new Body(vec2.create(), 0, 0, 0, 0),
}; };
constructor( constructor(
public center: vec2, public center: vec2,
public radius: number, public radius: number,
public style: BodyStyleName, public colorIndex: number,
public colorA: number,
public colorB: number,
public seed: number, public seed: number,
public litAngle = 0 public roughness: number
) { ) {
super(); super();
} }
public minDistance(target: vec2): number { public minDistance(target: vec2): number {
// Conservative: bound by the most outward the bumpiest outline can reach // Conservative: bound by the most outward the bumpy outline can reach so a
// so a body is never culled from a tile its surface might poke into. // body is never culled from a tile its surface might poke into.
return vec2.dist(this.center, target) - this.radius * Body.MAX_OUTLINE_GROWTH; return (
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,
colors: this.colorA * 32 + this.colorB, colorIndex: this.colorIndex,
seed: this.seed, seed: this.seed,
style: BodyStyle[this.style], roughness: this.roughness,
litAngle: Math.atan2(
transform2d[1] * dx + transform2d[3] * dy,
transform2d[0] * dx + transform2d[2] * dy
),
}; };
} }
} }

View file

@ -4,201 +4,91 @@ 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, BodyStyleName } from './body'; import { Body } from './body';
// Every colour the scene uses; bodies reference entries by name and mix their const PLANET_COUNT = 6;
// two colours per fragment in the shader, so nothing renders as a flat fill. const ASTEROID_COUNT = 18;
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);
interface MoonSpec { // Palette indices used for the bodies (see colorPalette below).
orbit: number; // around the parent planet, as a fraction of the system radius const SUN_COLOR = 5; // warm gold
size: number; const ROCK_COLOR = 7; // brown-grey rock
speed: number; // rad/s const PLANET_COLORS = [1, 2, 3, 4, 6]; // red, green, blue, purple, pink
}
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: Body; drawable: InstanceType<typeof Body>;
orbit: number; // around the sun — or around `parent`, for moons and rings radiusRatio: number; // orbit radius as a fraction of the system radius
phase: number; phase: number;
speed: number; speed: number;
size: number; sizeRatio: number; // body radius as a fraction of the system radius
spin: number; spin: number; // how fast the surface seed rotates
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's visible disc carries its own white-hot → ember gradient; these // The sun is a bright solid body so it's actually visible: in this engine a
// lights add the blown-out core, the warm corona reflecting off the lifted // light only shows where it reflects off a surface, so a bare light over the
// background, and the sunlight (plus shadows) on everything orbiting. // near-black "space" background would be invisible. The light sources sit at
private sunCore = new CircleLight(vec2.create(), rgb(1.0, 0.96, 0.88), 0.1); // its centre and illuminate the orbiting bodies. Because the sun is small
private sunGlow = new CircleLight(vec2.create(), rgb(1.35, 0.58, 0.18), 0.5); // relative to the orbits, it only mildly occludes its own light.
private sun = new Body(vec2.create(), 0, SUN_COLOR, 0, 0.6);
private sun: OrbitingBody = { private sunCore = new CircleLight(vec2.create(), rgb(1.0, 0.96, 0.85), 0.05);
drawable: new Body( private sunGlow = new CircleLight(vec2.create(), rgb(1.0, 0.62, 0.26), 0.55);
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;
this.bodies.push(this.sun); for (let i = 0; i < PLANET_COUNT; i++) {
const radiusRatio = 0.26 + (0.64 / PLANET_COUNT) * i;
PLANETS.forEach((spec) => { this.bodies.push({
const planet = this.createOrbitingBody( drawable: new Body(
spec.orbit, vec2.create(),
spec.size, 0,
spec.style, PLANET_COLORS[i % PLANET_COLORS.length],
spec.colors, Random.getRandom() * 10,
spec.spin 1.0
); ),
this.bodies.push(planet); radiusRatio,
phase: Random.getRandom() * Math.PI * 2,
if (spec.ringSize) { speed: 0.5 / Math.pow(radiusRatio, 1.5),
this.bodies.push({ sizeRatio: 0.04 + 0.022 * (i % 3),
...this.createOrbitingBody( spin: Random.getRandomInRange(-0.5, 0.5),
0, seedPhase: Random.getRandom() * 10,
spec.ringSize,
'rings',
['ringGold', 'ringShadow'],
0
),
parent: planet,
});
}
spec.moons?.forEach((moon) => {
this.bodies.push({
...this.createOrbitingBody(
moon.orbit,
moon.size,
'moon',
['moonGrey', 'moonShadow'],
0.8
),
speed: moon.speed,
parent: planet,
});
}); });
}); }
const bodyCount = this.bodies.length; // An asteroid belt: a ring of small, jagged, fast-tumbling rocks.
for (let i = 0; i < ASTEROID_COUNT; i++) {
const radiusRatio = Random.getRandomInRange(0.5, 0.62);
this.bodies.push({
drawable: new Body(
vec2.create(),
0,
ROCK_COLOR,
Random.getRandom() * 10,
2.6
),
radiusRatio,
phase: Random.getRandom() * Math.PI * 2,
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
await runAnimation( await runAnimation(
canvas, canvas,
[ [
{ {
...Body.descriptor, ...Body.descriptor,
shaderCombinationSteps: [...new Set([0, 4, 8, 16, 32, bodyCount])], shaderCombinationSteps: [0, 4, 8, 16, bodyCount],
}, },
{ {
...CircleLight.descriptor, ...CircleLight.descriptor,
@ -208,40 +98,23 @@ export class OrbitScene implements Scene {
this.drawNextFrame.bind(this), this.drawNextFrame.bind(this),
{ {
enableHighDpiRendering: true, enableHighDpiRendering: true,
motionBlur: 0.5, motionBlur: 0.6,
lightPenetrationRatio: 0.8, ambientLight: rgb(0.17, 0.17, 0.23),
ambientLight: rgb(0.07, 0.075, 0.145), backgroundColor: rgb(0.02, 0.02, 0.05),
backgroundColor: rgb(0.035, 0.035, 0.07), colorPalette: [
colorPalette: Object.values(palette), rgb(1, 1, 1), // 0 white (unused fallback)
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,
@ -266,30 +139,27 @@ 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.5 + 0.05 * Math.sin(time * 1.7); this.sunGlow.intensity = 0.55 + 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,
origin[0] + Math.cos(angle) * body.orbit * maxRadius, center.x + Math.cos(angle) * body.radiusRatio * maxRadius,
origin[1] + Math.sin(angle) * body.orbit * maxRadius center.y + Math.sin(angle) * body.radiusRatio * maxRadius
); );
// The sun breathes gently; everything else keeps its size. body.drawable.radius = body.sizeRatio * maxRadius;
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);

View file

@ -9,9 +9,11 @@ const WIND_SLANT = 0.2;
export class RainScene implements Scene { export class RainScene implements Scene {
private droplets: Array<DropletWrapper> = []; private droplets: Array<DropletWrapper> = [];
// The moon: a single pale, cold light hanging high in the sky and the scene's private lights = [
// only light source — the falling rain only shows where its streaks catch it. new CircleLight(vec2.create(), rgb255(184, 41, 255), 1.5),
private moon = new CircleLight(vec2.create(), rgb255(200, 214, 255), 2.5); new CircleLight(vec2.create(), rgb255(255, 31, 109), 1.5),
new CircleLight(vec2.create(), rgb255(64, 110, 255), 1.5),
];
private overlay: HTMLDivElement; private overlay: HTMLDivElement;
public insights?: any; public insights?: any;
@ -39,18 +41,14 @@ export class RainScene implements Scene {
}, },
{ {
...CircleLight.descriptor, ...CircleLight.descriptor,
shaderCombinationSteps: [0, 1], shaderCombinationSteps: [0, 3],
}, },
], ],
this.drawNextFrame.bind(this), this.drawNextFrame.bind(this),
{ {
// A dark night sky so the moon is the dominant light. backgroundColor: rgb(0.5, 0.5, 0.5),
backgroundColor: rgb(0.09, 0.12, 0.19), ambientLight: rgb(0.2, 0.2, 0.23),
ambientLight: rgb(0.2, 0.22, 0.28),
enableHighDpiRendering: true, enableHighDpiRendering: true,
// Keep most of the previous frame each render so the droplets smear
// into long streaks of rain.
motionBlur: 0.9,
// More, smaller tiles keep the droplet count per tile low. // More, smaller tiles keep the droplet count per tile low.
tileMultiplier: 12, tileMultiplier: 12,
} }
@ -72,17 +70,19 @@ export class RainScene implements Scene {
const viewAreaSize = renderer.viewAreaSize; const viewAreaSize = renderer.viewAreaSize;
// The moon drifts back and forth along the top edge; the rain falls past // each light sweeps within its own third of the screen
// it and lights up. this.lights.forEach((light, i) => {
vec2.set( vec2.set(
this.moon.center, light.center,
viewAreaSize.x * (0.5 + 0.5 * Math.sin(currentTime / 2500)), viewAreaSize.x *
viewAreaSize.y * 0.95 ((i + 0.5) / 3 + (1 / 6) * Math.sin(currentTime / 900 + (i * Math.PI * 2) / 3)),
); 0
);
});
this.droplets.forEach((d) => d.animate(currentTime, viewAreaSize, WIND_SLANT)); this.droplets.forEach((d) => d.animate(currentTime, viewAreaSize, WIND_SLANT));
[...this.droplets.map((d) => d.drawable), this.moon].forEach((d) => [...this.droplets.map((d) => d.drawable), ...this.lights].forEach((d) =>
renderer.addDrawable(d) renderer.addDrawable(d)
); );

View file

@ -20,7 +20,7 @@ const htmlLoaderOptions = JSON.stringify({
minimize: false, minimize: false,
}); });
module.exports = (env, argv) => ({ module.exports = {
entry: { entry: {
index: PATHS.entryPoint, index: PATHS.entryPoint,
}, },
@ -41,7 +41,6 @@ module.exports = (env, argv) => ({
}, },
devServer: { devServer: {
host: '0.0.0.0', host: '0.0.0.0',
port: 9999,
allowedHosts: 'all', allowedHosts: 'all',
}, },
plugins: [ plugins: [
@ -62,9 +61,8 @@ module.exports = (env, argv) => ({
useShortDoctype: true, useShortDoctype: true,
}, },
}), }),
...(argv.mode === 'production' new HtmlInlineScriptPlugin(),
? [new HtmlInlineScriptPlugin(), new HTMLInlineCSSWebpackPlugin()] new HTMLInlineCSSWebpackPlugin(),
: []),
], ],
module: { module: {
rules: [ rules: [
@ -129,4 +127,4 @@ module.exports = (env, argv) => ({
resolve: { resolve: {
extensions: ['.ts', '.js'], extensions: ['.ts', '.js'],
}, },
}); };