Compare commits
4 commits
414149293a
...
d8cbffcfd5
| Author | SHA1 | Date | |
|---|---|---|---|
| d8cbffcfd5 | |||
| 3ceae8677e | |||
| 133b0d9198 | |||
| 491d5817c2 |
12 changed files with 369 additions and 92 deletions
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"projects": {
|
||||
"default": "schmelczer-dev"
|
||||
}
|
||||
}
|
||||
|
|
@ -28,10 +28,7 @@ jobs:
|
|||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
# webpack 4 needs the legacy OpenSSL provider on Node 17+
|
||||
run: npm run build
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
|
||||
- name: Copy build to host pages mount (sdf2d)
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"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",
|
||||
"sass": "^1.100.0",
|
||||
"sass-loader": "^17.0.0",
|
||||
"sdf-2d": "^0.7.6",
|
||||
"sdf-2d": "^0.8.0",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"ts-loader": "^9.6.0",
|
||||
"typescript": "^6.0.3",
|
||||
|
|
@ -39,6 +39,34 @@
|
|||
"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": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
|
|
@ -6666,13 +6694,6 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.22.12",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||
|
|
@ -6919,15 +6940,8 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sdf-2d": {
|
||||
"version": "0.7.6",
|
||||
"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"
|
||||
}
|
||||
"resolved": "../sdf-2d",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/select-hose": {
|
||||
"version": "2.0.0",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
"resolve-url-loader": "^5.0.0",
|
||||
"sass": "^1.100.0",
|
||||
"sass-loader": "^17.0.0",
|
||||
"sdf-2d": "^0.7.6",
|
||||
"sdf-2d": "^0.8.0",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"ts-loader": "^9.6.0",
|
||||
"typescript": "^6.0.3",
|
||||
|
|
|
|||
|
|
@ -1,31 +1,22 @@
|
|||
const baseUri = 'https://insights.decla.red';
|
||||
const type = 'sdf-2d-demo';
|
||||
import { track } from '../analytics';
|
||||
|
||||
export const handleInsights = async (initialData: any): Promise<(data: any) => void> => {
|
||||
try {
|
||||
const sessionId = await createSession(initialData);
|
||||
return (data) => createFrame(sessionId, data);
|
||||
} catch {
|
||||
return () => null;
|
||||
track('Session Insights', { props: toStringProps(initialData) });
|
||||
|
||||
let performanceReported = false;
|
||||
return (data) => {
|
||||
if (performanceReported) {
|
||||
return;
|
||||
}
|
||||
performanceReported = true;
|
||||
track('Performance Insights', { props: toStringProps(data) });
|
||||
};
|
||||
};
|
||||
|
||||
const createSession = async (data: any): Promise<string> => {
|
||||
const response = await sendPostRequest(`${baseUri}/${type}/sessions/`, data);
|
||||
const { sessionId } = await response.json();
|
||||
return sessionId;
|
||||
};
|
||||
|
||||
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),
|
||||
});
|
||||
const toStringProps = (data: Record<string, unknown>): Record<string, string> =>
|
||||
Object.entries(data).reduce<Record<string, string>>((props, [key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
props[key] = String(value);
|
||||
}
|
||||
return props;
|
||||
}, {});
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@ import { Random } from './helper/random';
|
|||
import { removeUnnecessaryOutlines } from './helper/remove-unnecessary-outlines';
|
||||
import { BlobScene } from './scenes/blob/blob-scene';
|
||||
import { MetaballScene } from './scenes/metaball/metaball-scene';
|
||||
import { OrbitScene } from './scenes/orbit/orbit-scene';
|
||||
import { RainScene } from './scenes/rain/rain-scene';
|
||||
import { TunnelScene } from './scenes/tunnel-scene';
|
||||
import './styles/index.scss';
|
||||
|
||||
const scenes = [TunnelScene, MetaballScene, RainScene, BlobScene];
|
||||
const scenes = [TunnelScene, MetaballScene, OrbitScene, RainScene, BlobScene];
|
||||
Random.seed = 2;
|
||||
|
||||
glMatrix.setMatrixArrayType(Array);
|
||||
|
|
|
|||
|
|
@ -74,17 +74,21 @@ export class MetaballScene implements Scene {
|
|||
renderer.addDrawable(c.shape);
|
||||
});
|
||||
|
||||
const sweep = Math.sin(currentTime / 1500) * 0.5;
|
||||
const light1 = new Flashlight(
|
||||
vec2.fromValues(-0.05, -0.05),
|
||||
rgb255(104, 171, 212),
|
||||
0.02,
|
||||
vec2.fromValues(1, 1)
|
||||
vec2.fromValues(Math.cos(Math.PI / 4 + sweep), Math.sin(Math.PI / 4 + sweep))
|
||||
);
|
||||
const light2 = new Flashlight(
|
||||
vec2.fromValues(viewAreaWidth + 0.05, -0.05),
|
||||
rgb255(226, 90, 102),
|
||||
0.02,
|
||||
vec2.fromValues(-1, 1)
|
||||
vec2.fromValues(
|
||||
Math.cos((Math.PI * 3) / 4 - sweep),
|
||||
Math.sin((Math.PI * 3) / 4 - sweep)
|
||||
)
|
||||
);
|
||||
|
||||
renderer.addDrawable(light1);
|
||||
|
|
|
|||
94
src/scenes/orbit/body.ts
Normal file
94
src/scenes/orbit/body.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
169
src/scenes/orbit/orbit-scene.ts
Normal file
169
src/scenes/orbit/orbit-scene.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
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,34 +6,41 @@ export const Droplet = DropletFactory(rgb255(122, 122, 255));
|
|||
export class DropletWrapper {
|
||||
public readonly drawable: InstanceType<typeof Droplet>;
|
||||
|
||||
private speed = Random.getRandom() * 0.2 + 0.2;
|
||||
private speed = Random.getRandom() * 0.45 + 0.45;
|
||||
private position = vec2.fromValues(
|
||||
Random.getRandomInRange(0.1, 0.9),
|
||||
Random.getRandom()
|
||||
);
|
||||
private length = Random.getRandom() * 20 + 4;
|
||||
private length = Random.getRandom() * 14 + 8;
|
||||
|
||||
constructor() {
|
||||
const size = Random.getRandom() * 2 + 2;
|
||||
const size = Random.getRandom() * 1.2 + 1;
|
||||
|
||||
this.drawable = new Droplet(
|
||||
vec2.create(),
|
||||
vec2.create(),
|
||||
size + Random.getRandom() * 2 + 2,
|
||||
size + Random.getRandom() + 1,
|
||||
size
|
||||
);
|
||||
}
|
||||
|
||||
public animate(currentTime: number, viewAreaSize: ReadonlyVec2) {
|
||||
public animate(currentTime: number, viewAreaSize: ReadonlyVec2, windSlant: number) {
|
||||
const heightOffset = 100;
|
||||
const fall = this.speed * currentTime;
|
||||
|
||||
vec2.set(
|
||||
this.drawable.from,
|
||||
this.position.x * viewAreaSize.x,
|
||||
(this.position.x * viewAreaSize.x + fall * windSlant) % viewAreaSize.x,
|
||||
viewAreaSize.y -
|
||||
((this.position.y * viewAreaSize.y + this.speed * currentTime) %
|
||||
(viewAreaSize.y + heightOffset))
|
||||
((this.position.y * viewAreaSize.y + fall) % (viewAreaSize.y + heightOffset))
|
||||
);
|
||||
|
||||
vec2.add(this.drawable.to, this.drawable.from, vec2.fromValues(0, this.length));
|
||||
// the tail points opposite to the direction of motion
|
||||
const norm = this.length / Math.sqrt(1 + windSlant * windSlant);
|
||||
vec2.add(
|
||||
this.drawable.to,
|
||||
this.drawable.from,
|
||||
vec2.fromValues(-windSlant * norm, norm)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,26 @@ import { settings } from '../../settings';
|
|||
import { Scene } from '../scene';
|
||||
import { Droplet, DropletWrapper } from './droplet';
|
||||
|
||||
const WIND_SLANT = 0.2;
|
||||
|
||||
export class RainScene implements Scene {
|
||||
private droplets: Array<DropletWrapper> = [];
|
||||
private light1: CircleLight = new CircleLight(vec2.create(), rgb255(184, 41, 255), 1.5);
|
||||
private light2: CircleLight = new CircleLight(vec2.create(), rgb255(255, 31, 109), 1.5);
|
||||
private lights = [
|
||||
new CircleLight(vec2.create(), rgb255(184, 41, 255), 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;
|
||||
public insights?: any;
|
||||
|
||||
public async run(canvas: HTMLCanvasElement, overlay: HTMLDivElement): Promise<void> {
|
||||
this.overlay = overlay;
|
||||
for (let i = 0; i < (canvas.getBoundingClientRect().width / 800) * 20; i++) {
|
||||
for (
|
||||
let i = 0;
|
||||
i < Math.max(100, (canvas.getBoundingClientRect().width / 800) * 100);
|
||||
i++
|
||||
) {
|
||||
this.droplets.push(new DropletWrapper());
|
||||
}
|
||||
|
||||
|
|
@ -24,18 +33,24 @@ export class RainScene implements Scene {
|
|||
[
|
||||
{
|
||||
...Droplet.descriptor,
|
||||
shaderCombinationSteps: [0, 1, 2, 4, 8, 12, 16, 24],
|
||||
// Tiles that contain more droplets than the largest step have no
|
||||
// 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,
|
||||
shaderCombinationSteps: [0, 2],
|
||||
shaderCombinationSteps: [0, 3],
|
||||
},
|
||||
],
|
||||
this.drawNextFrame.bind(this),
|
||||
{
|
||||
backgroundColor: rgb(0.5, 0.5, 0.5),
|
||||
ambientLight: rgb(0.2, 0.2, 0.2),
|
||||
ambientLight: rgb(0.2, 0.2, 0.23),
|
||||
enableHighDpiRendering: true,
|
||||
// More, smaller tiles keep the droplet count per tile low.
|
||||
tileMultiplier: 12,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -55,12 +70,19 @@ export class RainScene implements Scene {
|
|||
|
||||
const viewAreaSize = renderer.viewAreaSize;
|
||||
|
||||
vec2.set(this.light1.center, 0, viewAreaSize.y / 2);
|
||||
vec2.set(this.light2.center, viewAreaSize.x, viewAreaSize.y / 2);
|
||||
// each light sweeps within its own third of the screen
|
||||
this.lights.forEach((light, i) => {
|
||||
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));
|
||||
this.droplets.forEach((d) => d.animate(currentTime, viewAreaSize, WIND_SLANT));
|
||||
|
||||
[...this.droplets.map((d) => d.drawable), this.light1, this.light2].forEach((d) =>
|
||||
[...this.droplets.map((d) => d.drawable), ...this.lights].forEach((d) =>
|
||||
renderer.addDrawable(d)
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue