decla-red/frontend/src/scripts/landing-page-background.ts
2026-08-23 10:27:55 +01:00

162 lines
4.4 KiB
TypeScript

import { ReadonlyVec2, vec2, vec3 } from 'gl-matrix';
import {
CircleLight,
FilteringOptions,
Renderer,
renderNoise,
runAnimation,
WrapOptions,
} from 'sdf-2d';
import { settings, PlanetBase, Random, hsl } from 'shared';
import { PlanetShape } from './shapes/planet-shape';
const bluePlanet = 0;
const redPlanet = 0.85;
export class LandingPageBackground {
public readonly renderer: Promise<Renderer>;
private isActive = true;
private resolveRenderer!: (r: Renderer) => unknown;
private scene?: {
canvasSize: vec2;
topPlanet: PlanetShape;
bottomPlanet: PlanetShape;
planetAngle: number;
planetDistance: number;
};
constructor(canvas: HTMLCanvasElement) {
this.renderer = new Promise((r) => (this.resolveRenderer = r));
this.start(canvas);
}
private async start(canvas: HTMLCanvasElement): Promise<void> {
const noiseTexture = await renderNoise([256, 256], 1.2, 2);
runAnimation(
canvas,
[
{ ...PlanetShape.descriptor, shaderCombinationSteps: [0, 1, 2] },
{ ...CircleLight.descriptor, shaderCombinationSteps: [0, 2] },
],
this.gameLoop.bind(this),
{
shadowTraceCount: 16,
paletteSize: 1,
ambientLight: vec3.fromValues(0, 0, 0),
lightCutoffDistance: settings.lightCutoffDistance,
textures: {
noiseTexture: {
source: noiseTexture,
overrides: {
maxFilter: FilteringOptions.LINEAR,
wrapS: WrapOptions.MIRRORED_REPEAT,
wrapT: WrapOptions.MIRRORED_REPEAT,
},
},
},
},
);
}
private buildScene(canvasSize: ReadonlyVec2) {
Random.seed = 42;
const topPosition = vec2.scale(vec2.create(), canvasSize, 0.7);
const topPlanet = new PlanetShape(
PlanetBase.createPlanetVertices(
topPosition,
Random.getRandomInRange(150, 400),
Random.getRandomInRange(150, 400),
Random.getRandomInRange(10, 20),
),
redPlanet,
);
topPlanet.randomOffset = Random.getRandom();
const bottomPosition = vec2.scale(vec2.create(), canvasSize, 0.3);
const bottomPlanet = new PlanetShape(
PlanetBase.createPlanetVertices(
bottomPosition,
Random.getRandomInRange(150, 800),
Random.getRandomInRange(150, 400),
Random.getRandomInRange(10, 40),
),
bluePlanet,
);
bottomPlanet.randomOffset = Random.getRandom();
const between = vec2.subtract(vec2.create(), topPosition, bottomPosition);
this.scene = {
canvasSize: vec2.clone(canvasSize),
topPlanet,
bottomPlanet,
planetAngle: Math.atan2(between[1], between[0]),
planetDistance: vec2.length(between),
};
return this.scene;
}
private gameLoop(renderer: Renderer, time: DOMHighResTimeStamp): boolean {
this.resolveRenderer(renderer);
renderer.setViewArea(vec2.fromValues(0, renderer.canvasSize[1]), renderer.canvasSize);
const scene =
this.scene && vec2.exactEquals(this.scene.canvasSize, renderer.canvasSize)
? this.scene
: this.buildScene(renderer.canvasSize);
scene.topPlanet.rotation = (time / 1000) * 0.09;
scene.bottomPlanet.rotation = (time / 1000) * -0.06;
renderer.addDrawable(scene.topPlanet);
renderer.addDrawable(scene.bottomPlanet);
const lightDistance = scene.planetDistance * 1.2;
renderer.addDrawable(
new CircleLight(
this.calculateLightPosition(
renderer,
scene.planetAngle,
lightDistance,
-time / 3000,
),
hsl(25, 75, 60),
0.75,
),
);
renderer.addDrawable(
new CircleLight(
this.calculateLightPosition(
renderer,
scene.planetAngle,
lightDistance,
time / 2000 + Math.PI,
),
hsl(249, 79, 70),
0.25,
),
);
return this.isActive;
}
private calculateLightPosition(
renderer: Renderer,
angle: number,
length: number,
t: number,
): vec2 {
const lightPosition = vec2.fromValues(
length * Math.sin(t),
length * Math.sin(t) * Math.cos(t),
);
const canvasCenter = vec2.scale(vec2.create(), renderer.canvasSize, 0.5);
vec2.add(lightPosition, lightPosition, canvasCenter);
return vec2.rotate(lightPosition, lightPosition, canvasCenter, angle);
}
public destroy() {
this.isActive = false;
}
}