Compare commits
No commits in common. "d8cbffcfd523460343c9b8e2304bf1189802d83e" and "414149293abcdc427d9ee50f1490bd33a7338cf3" have entirely different histories.
d8cbffcfd5
...
414149293a
12 changed files with 92 additions and 369 deletions
5
.firebaserc
Normal file
5
.firebaserc
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "schmelczer-dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,7 +28,10 @@ jobs:
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
# webpack 4 needs the legacy OpenSSL provider on Node 17+
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
- name: Copy build to host pages mount (sdf2d)
|
- name: Copy build to host pages mount (sdf2d)
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
|
|
||||||
17
firebase.json
Normal file
17
firebase.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"hosting": {
|
||||||
|
"public": "dist",
|
||||||
|
"ignore": [
|
||||||
|
"firebase.json",
|
||||||
|
"**/.*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
],
|
||||||
|
"site": "sdf2d-schmelczer-dev",
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "**",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
48
package-lock.json
generated
48
package-lock.json
generated
|
|
@ -29,7 +29,7 @@
|
||||||
"resolve-url-loader": "^5.0.0",
|
"resolve-url-loader": "^5.0.0",
|
||||||
"sass": "^1.100.0",
|
"sass": "^1.100.0",
|
||||||
"sass-loader": "^17.0.0",
|
"sass-loader": "^17.0.0",
|
||||||
"sdf-2d": "^0.8.0",
|
"sdf-2d": "^0.7.6",
|
||||||
"source-map-loader": "^5.0.0",
|
"source-map-loader": "^5.0.0",
|
||||||
"ts-loader": "^9.6.0",
|
"ts-loader": "^9.6.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
|
|
@ -39,34 +39,6 @@
|
||||||
"webpack-dev-server": "^5.2.4"
|
"webpack-dev-server": "^5.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../sdf-2d": {
|
|
||||||
"version": "0.8.0",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"gl-matrix": "^3.4.4",
|
|
||||||
"resize-observer-polyfill": "^1.5.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^10.0.1",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
|
||||||
"@typescript-eslint/parser": "^8.60.1",
|
|
||||||
"eslint": "^10.4.1",
|
|
||||||
"eslint-config-prettier": "^10.1.8",
|
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
|
||||||
"eslint-plugin-unused-imports": "^4.4.1",
|
|
||||||
"globals": "^17.6.0",
|
|
||||||
"prettier": "^3.8.3",
|
|
||||||
"raw-loader": "^4.0.2",
|
|
||||||
"terser-webpack-plugin": "^5.6.1",
|
|
||||||
"ts-loader": "^9.6.0",
|
|
||||||
"typedoc": "^0.28.19",
|
|
||||||
"typedoc-plugin-extras": "^4.0.1",
|
|
||||||
"typescript": "^6.0.3",
|
|
||||||
"webpack": "^5.107.2",
|
|
||||||
"webpack-cli": "^7.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
|
|
@ -6694,6 +6666,13 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/resize-observer-polyfill": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.12",
|
"version": "1.22.12",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||||
|
|
@ -6940,8 +6919,15 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sdf-2d": {
|
"node_modules/sdf-2d": {
|
||||||
"resolved": "../sdf-2d",
|
"version": "0.7.6",
|
||||||
"link": true
|
"resolved": "https://registry.npmjs.org/sdf-2d/-/sdf-2d-0.7.6.tgz",
|
||||||
|
"integrity": "sha512-aBJUjwYjWP+/fSvLHH6vhpFyWeSJNCSEHpE0XBX69s2bQlo/NOUr3nq/KuhRZ6A3FeXrGU4lzyurHJ1N8f4rFg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"gl-matrix": "^3.3.0",
|
||||||
|
"resize-observer-polyfill": "^1.5.1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/select-hose": {
|
"node_modules/select-hose": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
"resolve-url-loader": "^5.0.0",
|
"resolve-url-loader": "^5.0.0",
|
||||||
"sass": "^1.100.0",
|
"sass": "^1.100.0",
|
||||||
"sass-loader": "^17.0.0",
|
"sass-loader": "^17.0.0",
|
||||||
"sdf-2d": "^0.8.0",
|
"sdf-2d": "^0.7.6",
|
||||||
"source-map-loader": "^5.0.0",
|
"source-map-loader": "^5.0.0",
|
||||||
"ts-loader": "^9.6.0",
|
"ts-loader": "^9.6.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,31 @@
|
||||||
import { track } from '../analytics';
|
const baseUri = 'https://insights.decla.red';
|
||||||
|
const type = 'sdf-2d-demo';
|
||||||
|
|
||||||
export const handleInsights = async (initialData: any): Promise<(data: any) => void> => {
|
export const handleInsights = async (initialData: any): Promise<(data: any) => void> => {
|
||||||
track('Session Insights', { props: toStringProps(initialData) });
|
try {
|
||||||
|
const sessionId = await createSession(initialData);
|
||||||
let performanceReported = false;
|
return (data) => createFrame(sessionId, data);
|
||||||
return (data) => {
|
} catch {
|
||||||
if (performanceReported) {
|
return () => null;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
performanceReported = true;
|
|
||||||
track('Performance Insights', { props: toStringProps(data) });
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toStringProps = (data: Record<string, unknown>): Record<string, string> =>
|
const createSession = async (data: any): Promise<string> => {
|
||||||
Object.entries(data).reduce<Record<string, string>>((props, [key, value]) => {
|
const response = await sendPostRequest(`${baseUri}/${type}/sessions/`, data);
|
||||||
if (value !== undefined && value !== null) {
|
const { sessionId } = await response.json();
|
||||||
props[key] = String(value);
|
return sessionId;
|
||||||
}
|
};
|
||||||
return props;
|
|
||||||
}, {});
|
const createFrame = (sessionId: string, data: any): Promise<unknown> =>
|
||||||
|
sendPostRequest(`${baseUri}/${type}/sessions/${sessionId}`, data).catch();
|
||||||
|
|
||||||
|
const sendPostRequest = async (uri: string, data: any): Promise<Response> =>
|
||||||
|
await fetch(uri, {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
redirect: 'follow',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,11 @@ import { Random } from './helper/random';
|
||||||
import { removeUnnecessaryOutlines } from './helper/remove-unnecessary-outlines';
|
import { removeUnnecessaryOutlines } from './helper/remove-unnecessary-outlines';
|
||||||
import { BlobScene } from './scenes/blob/blob-scene';
|
import { BlobScene } from './scenes/blob/blob-scene';
|
||||||
import { MetaballScene } from './scenes/metaball/metaball-scene';
|
import { MetaballScene } from './scenes/metaball/metaball-scene';
|
||||||
import { OrbitScene } from './scenes/orbit/orbit-scene';
|
|
||||||
import { RainScene } from './scenes/rain/rain-scene';
|
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, BlobScene];
|
||||||
Random.seed = 2;
|
Random.seed = 2;
|
||||||
|
|
||||||
glMatrix.setMatrixArrayType(Array);
|
glMatrix.setMatrixArrayType(Array);
|
||||||
|
|
|
||||||
|
|
@ -74,21 +74,17 @@ export class MetaballScene implements Scene {
|
||||||
renderer.addDrawable(c.shape);
|
renderer.addDrawable(c.shape);
|
||||||
});
|
});
|
||||||
|
|
||||||
const sweep = Math.sin(currentTime / 1500) * 0.5;
|
|
||||||
const light1 = new Flashlight(
|
const light1 = new Flashlight(
|
||||||
vec2.fromValues(-0.05, -0.05),
|
vec2.fromValues(-0.05, -0.05),
|
||||||
rgb255(104, 171, 212),
|
rgb255(104, 171, 212),
|
||||||
0.02,
|
0.02,
|
||||||
vec2.fromValues(Math.cos(Math.PI / 4 + sweep), Math.sin(Math.PI / 4 + sweep))
|
vec2.fromValues(1, 1)
|
||||||
);
|
);
|
||||||
const light2 = new Flashlight(
|
const light2 = new Flashlight(
|
||||||
vec2.fromValues(viewAreaWidth + 0.05, -0.05),
|
vec2.fromValues(viewAreaWidth + 0.05, -0.05),
|
||||||
rgb255(226, 90, 102),
|
rgb255(226, 90, 102),
|
||||||
0.02,
|
0.02,
|
||||||
vec2.fromValues(
|
vec2.fromValues(-1, 1)
|
||||||
Math.cos((Math.PI * 3) / 4 - sweep),
|
|
||||||
Math.sin((Math.PI * 3) / 4 - sweep)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
renderer.addDrawable(light1);
|
renderer.addDrawable(light1);
|
||||||
|
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
import { mat2d, vec2 } from 'gl-matrix';
|
|
||||||
import { Drawable, DrawableDescriptor } from 'sdf-2d';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A rocky celestial body: a circle whose radius is modulated by a few angular
|
|
||||||
* harmonics so its outline looks bumpy. One descriptor renders every body in
|
|
||||||
* the scene (sun, planets, asteroids) — each instance carries its own palette
|
|
||||||
* 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
|
|
||||||
* asteroid).
|
|
||||||
*/
|
|
||||||
export class Body extends Drawable {
|
|
||||||
// The peak fraction by which the harmonics below can push the outline
|
|
||||||
// outward (0.06 + 0.04 + 0.02), used for a conservative culling bound.
|
|
||||||
private static readonly MAX_WOBBLE = 0.12;
|
|
||||||
|
|
||||||
public static descriptor: DrawableDescriptor = {
|
|
||||||
sdf: {
|
|
||||||
shader: `
|
|
||||||
uniform vec2 bodyCenters[BODY_COUNT];
|
|
||||||
uniform float bodyRadii[BODY_COUNT];
|
|
||||||
uniform float bodyColorIndices[BODY_COUNT];
|
|
||||||
uniform float bodySeeds[BODY_COUNT];
|
|
||||||
uniform float bodyRoughness[BODY_COUNT];
|
|
||||||
|
|
||||||
float bodyMinDistance(vec2 target, out vec4 color) {
|
|
||||||
color = readFromPalette(0);
|
|
||||||
float minDistance = 1000.0;
|
|
||||||
|
|
||||||
for (int i = 0; i < BODY_COUNT; i++) {
|
|
||||||
vec2 d = target - bodyCenters[i];
|
|
||||||
float angle = atan(d.y, d.x);
|
|
||||||
float seed = bodySeeds[i];
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
float dist =
|
|
||||||
length(d) - bodyRadii[i] * (1.0 + wobble * bodyRoughness[i]);
|
|
||||||
|
|
||||||
if (dist < minDistance) {
|
|
||||||
minDistance = dist;
|
|
||||||
color = readFromPalette(int(bodyColorIndices[i] + 0.5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return minDistance;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
distanceFunctionName: 'bodyMinDistance',
|
|
||||||
},
|
|
||||||
propertyUniformMapping: {
|
|
||||||
center: 'bodyCenters',
|
|
||||||
radius: 'bodyRadii',
|
|
||||||
colorIndex: 'bodyColorIndices',
|
|
||||||
seed: 'bodySeeds',
|
|
||||||
roughness: 'bodyRoughness',
|
|
||||||
},
|
|
||||||
uniformCountMacroName: 'BODY_COUNT',
|
|
||||||
shaderCombinationSteps: [0, 4, 8, 16, 32],
|
|
||||||
empty: new Body(vec2.create(), 0, 0, 0, 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public center: vec2,
|
|
||||||
public radius: number,
|
|
||||||
public colorIndex: number,
|
|
||||||
public seed: number,
|
|
||||||
public roughness: number
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public minDistance(target: vec2): number {
|
|
||||||
// Conservative: bound by the most outward the bumpy outline can reach so a
|
|
||||||
// body is never culled from a tile its surface might poke into.
|
|
||||||
return (
|
|
||||||
vec2.dist(this.center, target) -
|
|
||||||
this.radius * (1 + Body.MAX_WOBBLE * this.roughness)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {
|
|
||||||
return {
|
|
||||||
center: vec2.transformMat2d(vec2.create(), this.center, transform2d),
|
|
||||||
radius: this.radius * transform1d,
|
|
||||||
colorIndex: this.colorIndex,
|
|
||||||
seed: this.seed,
|
|
||||||
roughness: this.roughness,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
import { CircleLight, hsl, Renderer, rgb, runAnimation } from 'sdf-2d';
|
|
||||||
import { prettyPrint } from '../../helper/pretty-print';
|
|
||||||
import { Random } from '../../helper/random';
|
|
||||||
import { settings } from '../../settings';
|
|
||||||
import { Scene } from '../scene';
|
|
||||||
import { Body } from './body';
|
|
||||||
|
|
||||||
const PLANET_COUNT = 6;
|
|
||||||
const ASTEROID_COUNT = 18;
|
|
||||||
|
|
||||||
// Palette indices used for the bodies (see colorPalette below).
|
|
||||||
const SUN_COLOR = 5; // warm gold
|
|
||||||
const ROCK_COLOR = 7; // brown-grey rock
|
|
||||||
const PLANET_COLORS = [1, 2, 3, 4, 6]; // red, green, blue, purple, pink
|
|
||||||
|
|
||||||
interface OrbitingBody {
|
|
||||||
drawable: InstanceType<typeof Body>;
|
|
||||||
radiusRatio: number; // orbit radius as a fraction of the system radius
|
|
||||||
phase: number;
|
|
||||||
speed: number;
|
|
||||||
sizeRatio: number; // body radius as a fraction of the system radius
|
|
||||||
spin: number; // how fast the surface seed rotates
|
|
||||||
seedPhase: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OrbitScene implements Scene {
|
|
||||||
private overlay: HTMLDivElement;
|
|
||||||
public insights?: any;
|
|
||||||
|
|
||||||
// The sun is a bright solid body so it's actually visible: in this engine a
|
|
||||||
// light only shows where it reflects off a surface, so a bare light over the
|
|
||||||
// near-black "space" background would be invisible. The light sources sit at
|
|
||||||
// its centre and illuminate the orbiting bodies. Because the sun is small
|
|
||||||
// relative to the orbits, it only mildly occludes its own light.
|
|
||||||
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 sunGlow = new CircleLight(vec2.create(), rgb(1.0, 0.62, 0.26), 0.55);
|
|
||||||
|
|
||||||
private bodies: Array<OrbitingBody> = [];
|
|
||||||
|
|
||||||
public async run(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void> {
|
|
||||||
this.overlay = overlay;
|
|
||||||
|
|
||||||
for (let i = 0; i < PLANET_COUNT; i++) {
|
|
||||||
const radiusRatio = 0.26 + (0.64 / PLANET_COUNT) * i;
|
|
||||||
this.bodies.push({
|
|
||||||
drawable: new Body(
|
|
||||||
vec2.create(),
|
|
||||||
0,
|
|
||||||
PLANET_COLORS[i % PLANET_COLORS.length],
|
|
||||||
Random.getRandom() * 10,
|
|
||||||
1.0
|
|
||||||
),
|
|
||||||
radiusRatio,
|
|
||||||
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.
|
|
||||||
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(
|
|
||||||
canvas,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
...Body.descriptor,
|
|
||||||
shaderCombinationSteps: [0, 4, 8, 16, bodyCount],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...CircleLight.descriptor,
|
|
||||||
shaderCombinationSteps: [0, 2],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
this.drawNextFrame.bind(this),
|
|
||||||
{
|
|
||||||
enableHighDpiRendering: true,
|
|
||||||
motionBlur: 0.6,
|
|
||||||
ambientLight: rgb(0.17, 0.17, 0.23),
|
|
||||||
backgroundColor: rgb(0.02, 0.02, 0.05),
|
|
||||||
colorPalette: [
|
|
||||||
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 drawNextFrame(
|
|
||||||
renderer: Renderer,
|
|
||||||
currentTime: DOMHighResTimeStamp,
|
|
||||||
_: DOMHighResTimeStamp
|
|
||||||
): boolean {
|
|
||||||
this.insights = renderer.insights;
|
|
||||||
|
|
||||||
const width = renderer.canvasSize.x;
|
|
||||||
const height = renderer.canvasSize.y;
|
|
||||||
const maxSide = Math.max(width, height);
|
|
||||||
|
|
||||||
const viewAreaWidth = width / maxSide;
|
|
||||||
const viewAreaHeight = height / maxSide;
|
|
||||||
renderer.setViewArea(
|
|
||||||
vec2.fromValues(0, viewAreaHeight),
|
|
||||||
vec2.fromValues(viewAreaWidth, viewAreaHeight)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.overlay.innerText = prettyPrint(renderer.insights);
|
|
||||||
|
|
||||||
const center = vec2.fromValues(viewAreaWidth / 2, viewAreaHeight / 2);
|
|
||||||
const maxRadius = Math.min(viewAreaWidth, viewAreaHeight) / 2;
|
|
||||||
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.sunGlow.center, center);
|
|
||||||
this.sunGlow.intensity = 0.55 + 0.05 * Math.sin(time * 1.7);
|
|
||||||
|
|
||||||
this.bodies.forEach((body) => {
|
|
||||||
const angle = body.phase + time * body.speed;
|
|
||||||
vec2.set(
|
|
||||||
body.drawable.center,
|
|
||||||
center.x + Math.cos(angle) * body.radiusRatio * maxRadius,
|
|
||||||
center.y + Math.sin(angle) * body.radiusRatio * maxRadius
|
|
||||||
);
|
|
||||||
body.drawable.radius = body.sizeRatio * maxRadius;
|
|
||||||
body.drawable.seed = body.seedPhase + time * body.spin;
|
|
||||||
});
|
|
||||||
|
|
||||||
renderer.addDrawable(this.sun);
|
|
||||||
this.bodies.forEach((body) => renderer.addDrawable(body.drawable));
|
|
||||||
renderer.addDrawable(this.sunCore);
|
|
||||||
renderer.addDrawable(this.sunGlow);
|
|
||||||
|
|
||||||
return currentTime < settings.sceneTimeInMilliseconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,41 +6,34 @@ export const Droplet = DropletFactory(rgb255(122, 122, 255));
|
||||||
export class DropletWrapper {
|
export class DropletWrapper {
|
||||||
public readonly drawable: InstanceType<typeof Droplet>;
|
public readonly drawable: InstanceType<typeof Droplet>;
|
||||||
|
|
||||||
private speed = Random.getRandom() * 0.45 + 0.45;
|
private speed = Random.getRandom() * 0.2 + 0.2;
|
||||||
private position = vec2.fromValues(
|
private position = vec2.fromValues(
|
||||||
Random.getRandomInRange(0.1, 0.9),
|
Random.getRandomInRange(0.1, 0.9),
|
||||||
Random.getRandom()
|
Random.getRandom()
|
||||||
);
|
);
|
||||||
private length = Random.getRandom() * 14 + 8;
|
private length = Random.getRandom() * 20 + 4;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const size = Random.getRandom() * 1.2 + 1;
|
const size = Random.getRandom() * 2 + 2;
|
||||||
|
|
||||||
this.drawable = new Droplet(
|
this.drawable = new Droplet(
|
||||||
vec2.create(),
|
vec2.create(),
|
||||||
vec2.create(),
|
vec2.create(),
|
||||||
size + Random.getRandom() + 1,
|
size + Random.getRandom() * 2 + 2,
|
||||||
size
|
size
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public animate(currentTime: number, viewAreaSize: ReadonlyVec2, windSlant: number) {
|
public animate(currentTime: number, viewAreaSize: ReadonlyVec2) {
|
||||||
const heightOffset = 100;
|
const heightOffset = 100;
|
||||||
const fall = this.speed * currentTime;
|
|
||||||
|
|
||||||
vec2.set(
|
vec2.set(
|
||||||
this.drawable.from,
|
this.drawable.from,
|
||||||
(this.position.x * viewAreaSize.x + fall * windSlant) % viewAreaSize.x,
|
this.position.x * viewAreaSize.x,
|
||||||
viewAreaSize.y -
|
viewAreaSize.y -
|
||||||
((this.position.y * viewAreaSize.y + fall) % (viewAreaSize.y + heightOffset))
|
((this.position.y * viewAreaSize.y + this.speed * currentTime) %
|
||||||
|
(viewAreaSize.y + heightOffset))
|
||||||
);
|
);
|
||||||
|
|
||||||
// the tail points opposite to the direction of motion
|
vec2.add(this.drawable.to, this.drawable.from, vec2.fromValues(0, this.length));
|
||||||
const norm = this.length / Math.sqrt(1 + windSlant * windSlant);
|
|
||||||
vec2.add(
|
|
||||||
this.drawable.to,
|
|
||||||
this.drawable.from,
|
|
||||||
vec2.fromValues(-windSlant * norm, norm)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,17 @@ import { settings } from '../../settings';
|
||||||
import { Scene } from '../scene';
|
import { Scene } from '../scene';
|
||||||
import { Droplet, DropletWrapper } from './droplet';
|
import { Droplet, DropletWrapper } from './droplet';
|
||||||
|
|
||||||
const WIND_SLANT = 0.2;
|
|
||||||
|
|
||||||
export class RainScene implements Scene {
|
export class RainScene implements Scene {
|
||||||
private droplets: Array<DropletWrapper> = [];
|
private droplets: Array<DropletWrapper> = [];
|
||||||
private lights = [
|
private light1: CircleLight = new CircleLight(vec2.create(), rgb255(184, 41, 255), 1.5);
|
||||||
new CircleLight(vec2.create(), rgb255(184, 41, 255), 1.5),
|
private light2: CircleLight = new CircleLight(vec2.create(), rgb255(255, 31, 109), 1.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;
|
||||||
|
|
||||||
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 (
|
for (let i = 0; i < (canvas.getBoundingClientRect().width / 800) * 20; i++) {
|
||||||
let i = 0;
|
|
||||||
i < Math.max(100, (canvas.getBoundingClientRect().width / 800) * 100);
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
this.droplets.push(new DropletWrapper());
|
this.droplets.push(new DropletWrapper());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,24 +24,18 @@ export class RainScene implements Scene {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
...Droplet.descriptor,
|
...Droplet.descriptor,
|
||||||
// Tiles that contain more droplets than the largest step have no
|
shaderCombinationSteps: [0, 1, 2, 4, 8, 12, 16, 24],
|
||||||
// compiled shader to fall back on and flicker; together with the
|
|
||||||
// raised tileMultiplier, 48 keeps the worst-case tile comfortably
|
|
||||||
// covered.
|
|
||||||
shaderCombinationSteps: [0, 2, 4, 8, 16, 32, 48],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...CircleLight.descriptor,
|
...CircleLight.descriptor,
|
||||||
shaderCombinationSteps: [0, 3],
|
shaderCombinationSteps: [0, 2],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
this.drawNextFrame.bind(this),
|
this.drawNextFrame.bind(this),
|
||||||
{
|
{
|
||||||
backgroundColor: rgb(0.5, 0.5, 0.5),
|
backgroundColor: rgb(0.5, 0.5, 0.5),
|
||||||
ambientLight: rgb(0.2, 0.2, 0.23),
|
ambientLight: rgb(0.2, 0.2, 0.2),
|
||||||
enableHighDpiRendering: true,
|
enableHighDpiRendering: true,
|
||||||
// More, smaller tiles keep the droplet count per tile low.
|
|
||||||
tileMultiplier: 12,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -70,19 +55,12 @@ export class RainScene implements Scene {
|
||||||
|
|
||||||
const viewAreaSize = renderer.viewAreaSize;
|
const viewAreaSize = renderer.viewAreaSize;
|
||||||
|
|
||||||
// each light sweeps within its own third of the screen
|
vec2.set(this.light1.center, 0, viewAreaSize.y / 2);
|
||||||
this.lights.forEach((light, i) => {
|
vec2.set(this.light2.center, viewAreaSize.x, viewAreaSize.y / 2);
|
||||||
vec2.set(
|
|
||||||
light.center,
|
|
||||||
viewAreaSize.x *
|
|
||||||
((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));
|
||||||
|
|
||||||
[...this.droplets.map((d) => d.drawable), ...this.lights].forEach((d) =>
|
[...this.droplets.map((d) => d.drawable), this.light1, this.light2].forEach((d) =>
|
||||||
renderer.addDrawable(d)
|
renderer.addDrawable(d)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue