Flares as loops of the drop's own liquid

This commit is contained in:
nvrl committed 2026-11-02 22:11:56 +01:00
1 parent c4424ebd4d
commit f2c8915e5a
3 files changed
+183 -127

No files matched your search

+2 -1
View File
@@ -45,7 +45,7 @@ const FAST_FOR = 10; // s of frames on time before it tries one step up again
/// Quality steps: icosphere subdivisions and resolution scale. /// Quality steps: icosphere subdivisions and resolution scale.
const QUALITY: [number, number][] = [[5, 1], [4, 1], [4, 0.6], [3, 0.5]]; const QUALITY: [number, number][] = [[5, 1], [4, 1], [4, 0.6], [3, 0.5]];
/// Solar flare loops at most, per quality step. /// Solar flare loops at most, per quality step.
const FLARE_LOOPS = [10, 8, 6, 4]; const FLARE_LOOPS = [8, 6, 5, 4];
/// Solar flares: "auto" shows them where the track's look asks for them (style.ts). /// Solar flares: "auto" shows them where the track's look asks for them (style.ts).
export type FlareSetting = "auto" | "on" | "off"; export type FlareSetting = "auto" | "on" | "off";
@@ -339,6 +339,7 @@ export class LiquidBlob {
scale: this.scaleNow, scale: this.scaleNow,
lift: this.liftNow, lift: this.liftNow,
accent: this.accent, accent: this.accent,
time: this.time,
toObject: (x, y, z) => this.spin.toObject(x, y, z), toObject: (x, y, z) => this.spin.toObject(x, y, z),
}); });
return this.morph < 1 || moving || spinning || showing || flaring || this.pointer[3] > 0.01 || this.pointerGoal[3] > 0; return this.morph < 1 || moving || spinning || showing || flaring || this.pointer[3] > 0.01 || this.pointerGoal[3] > 0;
+175 -118
View File
@@ -1,151 +1,186 @@
// Solar flares round the drop: coronal loops, the magnetic arcs that leave a star at // Solar flares round the drop: coronal loops, the arcs that leave a star at one
// one point and land at another, played by the music. // point and land at another, played by the music. They are pulled out of the drop
// itself, so they are made of its liquid: the same artwork colours, the same rim in
// the card's accent, the same gloss, and they spring like it does.
// //
// - Each kick raises a loop. Where it rises follows the beat, so a riddim, which // - Each kick pulls a loop out. Where it rises follows the beat, so a riddim, which
// puts the same kick on the same beat bar after bar, draws the same crown again // puts the same kick on the same beat bar after bar, draws the same crown again
// every bar (`Look.crown` positions, `Look.scatter` off them). // every bar (`Look.crown` positions, `Look.scatter` off them).
// - The bass line's envelope lifts and drops every loop: each "womb" swells them. // - The bass line's envelope lifts every loop: each "womb" swells them.
// - Bright knots of plasma run along each loop once per womb. // - Light runs along each loop once per womb.
// - The wobble's filter sets the heat: a dark "oo" glows deep red, a bright "wah" // - The wobble's filter sways the loops: a dark "oo" barely, a bright "wah" more.
// burns yellow to white, and twists the loops. // - A snare pinches the top off a loop and throws it out as a drop of its own.
// - A snare breaks a loop at its top and throws the top out, like an eruption. // - In a gap (the stop before a drop) the loops sink back in; the drop then pulls
// - In a gap (the stop before a drop) the loops sink down and plasma rains back // out a burst of them at once.
// to their feet; the drop then sets everything off at once.
// //
// The loops are drawn after the drop, in the same WebGL2 context, added onto it. // The loops are drawn after the drop, in the same WebGL2 context, with its depth, so
// They test against its depth, so the drop hides the parts behind it. // their feet sink into it and the drop hides what is behind it.
import type { Features } from "../features"; import type { Features } from "../features";
import type { Look } from "./style"; import type { Look } from "./style";
const SEGMENTS = 40; // along each loop const SEGMENTS = 32; // along each loop
const SIDES = 10; // round it
const FLOATS = 16; // per loop in the instance buffer const FLOATS = 16; // per loop in the instance buffer
const RISE = 0.18; // s for a new loop to stand up const KICK_MIN = 0.2; // weaker kicks pull out no loop
const FADE = 0.35; // s for an old one to go const SPAN = [0.2, 0.36]; // half the distance between a loop's feet, rad
const ERUPT = 0.7; // s from the break until the loop is gone const WIDTH = 0.024; // the loop's radius at its top, in radii of the drop
const KICK_MIN = 0.2; // weaker kicks raise no loop const ERUPT = 0.6; // s from the pinch until the thrown top is gone
const SPAN = [0.22, 0.42]; // half the distance between a loop's feet, rad const DROP_BURST = 0.55; // the drop rising through this pulls out a burst of loops
const WIDTH = 0.03; // across a loop, in radii // Each loop's height is a spring, a little looser than the drop's jump, so it
const HEAT_SMOOTH = 0.12; // s // overshoots once as it comes out and sinks back softly.
const DROP_BURST = 0.55; // the drop rising through this sets off a burst of loops const SPRING = 150;
const DAMPING = 13;
interface Loop { interface Loop {
/// Feet, unit vectors in the drop's own space, so they turn with it. /// Feet, unit vectors in the drop's own space, so they turn with it.
a: [number, number, number]; a: [number, number, number];
b: [number, number, number]; b: [number, number, number];
age: number; age: number;
/// Seconds it lives, from the tempo. /// Seconds before it sinks back, from the tempo.
life: number; life: number;
/// Seconds since it broke, or -1. /// Seconds since its top was pinched off, or -1.
erupt: number; erupt: number;
seed: number; seed: number;
/// Knots travelled, in loop lengths. /// Light travelled along it, in loop lengths.
phase: number; phase: number;
height: number; height: number;
speed: number;
/// How high it wants to stand.
goal: number;
} }
export const FLARE_VERTEX = /* glsl */ `#version 300 es export const FLARE_VERTEX = /* glsl */ `#version 300 es
precision highp float; precision highp float;
layout(location = 0) in vec2 aStrip; // along 0..1, across -1..1 layout(location = 0) in vec2 aTube; // along 0..1, round 0..1
layout(location = 1) in vec3 aA; layout(location = 1) in vec3 aA;
layout(location = 2) in vec3 aB; layout(location = 2) in vec3 aB;
layout(location = 3) in vec4 aP; // height, alpha, heat, seed layout(location = 3) in vec4 aP; // height, alpha, grown 0..1, seed
layout(location = 4) in vec4 aQ; // erupt 0..1, twist, width, phase layout(location = 4) in vec4 aQ; // erupt 0..1, sway, width, phase
uniform mat4 uProj; uniform mat4 uProj;
uniform mat3 uRot; uniform mat3 uRot;
uniform vec3 uScale; uniform vec3 uScale;
uniform float uLift, uDist, uTime; uniform float uLift, uDist, uTime;
out vec3 vPos; // view space
out vec3 vNormal; // view space
out vec3 vObj; // the drop's surface under this point, for the artwork
out float vT; out float vT;
out float vSide;
out float vAlpha; out float vAlpha;
out float vHeat;
out float vPhase; out float vPhase;
out float vErupt;
// The loop's centre line at t, in view space. const float PI = 3.14159265;
// The loop's centre line at t, in the drop's own space.
vec3 along(float t) { vec3 along(float t) {
t = clamp(t, 0.0, 1.0);
float om = acos(clamp(dot(aA, aB), -1.0, 1.0)); float om = acos(clamp(dot(aA, aB), -1.0, 1.0));
vec3 base = om < 0.001 ? aA : (sin((1.0 - t) * om) * aA + sin(t * om) * aB) / sin(om); vec3 base = om < 0.001 ? aA : (sin((1.0 - t) * om) * aA + sin(t * om) * aB) / sin(om);
float arch = sin(3.14159265 * t); float arch = sin(PI * t);
float e = aQ.x; float e = aQ.x;
// Broken, the legs sink and the top is thrown out. // The piece that is pinched off: thrown out while the legs sink back.
float top = smoothstep(0.7, 0.98, arch); float top = 1.0 - smoothstep(0.12, 0.2, abs(t - 0.5));
float h = aP.x * arch * (1.0 - 0.6 * e) + e * (0.25 + 0.5 * e) * top; float h = aP.x * arch * (1.0 - e * (1.0 - top)) + e * (0.15 + 0.45 * e) * top;
vec3 p = base * (1.0 + h); // The feet start a little inside, so they come out of the surface, not off it.
// A twist: the loop winds round its own line, more at the top. vec3 p = base * (0.93 + 0.07 * arch + h);
// A slow sway, flowing with the drop's own noise time.
vec3 side = normalize(cross(base, aB - aA + vec3(1e-4))); vec3 side = normalize(cross(base, aB - aA + vec3(1e-4)));
p += side * aQ.y * 0.06 * sin(t * 12.566 + uTime * 3.0 + aP.w * 6.283) * arch; p += side * aQ.y * 0.05 * sin(t * 6.283 + uTime * 2.0 + aP.w * 6.283) * arch;
return uRot * p * uScale + vec3(0.0, uLift, -uDist); return p;
} }
void main() { void main() {
float t = aStrip.x; float t = aTube.x;
vec3 v = along(t); vec3 c = along(t);
vec3 ahead = along(min(1.0, t + 0.01)) - along(max(0.0, t - 0.01)); vec3 ahead = normalize(along(t + 0.01) - along(t - 0.01) + vec3(1e-5));
vec3 across = normalize(cross(ahead, v) + vec3(1e-5)); vec3 n1 = normalize(cross(ahead, normalize(c)) + vec3(1e-5));
// Thicker legs, thin at the top, like the real ones look. vec3 n2 = cross(n1, ahead);
float arch = sin(3.14159265 * t); float a = aTube.y * 2.0 * PI;
v += across * aStrip.y * aQ.z * (1.0 - 0.35 * arch); vec3 d = cos(a) * n1 + sin(a) * n2;
// Thick feet that melt into the drop, thinner at the top; thin while it grows.
float arch = sin(PI * t);
float foot = 1.0 - arch;
float r = aQ.z * (1.0 + 2.2 * foot * foot * foot) * aP.z;
float e = aQ.x;
if (e > 0.0) {
// A neck on each side of the top that narrows until it parts.
float neck = abs(abs(t - 0.5) - 0.16);
r *= smoothstep(0.0, 0.01 + 0.09 * e, neck);
r *= 1.0 - 0.6 * e * (1.0 - smoothstep(0.12, 0.2, abs(t - 0.5)));
}
vec3 p = c + d * r;
vPos = uRot * p * uScale + vec3(0.0, uLift, -uDist);
vNormal = normalize(uRot * d / uScale);
vObj = normalize(c);
vT = t; vT = t;
vSide = aStrip.y;
vAlpha = aP.y; vAlpha = aP.y;
vHeat = aP.z;
vPhase = aQ.w; vPhase = aQ.w;
vErupt = aQ.x; gl_Position = uProj * vec4(vPos, 1.0);
gl_Position = uProj * vec4(v, 1.0);
} }
`; `;
export const FLARE_FRAGMENT = /* glsl */ `#version 300 es export const FLARE_FRAGMENT = /* glsl */ `#version 300 es
precision highp float; precision highp float;
in vec3 vPos;
in vec3 vNormal;
in vec3 vObj;
in float vT; in float vT;
in float vSide;
in float vAlpha; in float vAlpha;
in float vHeat;
in float vPhase; in float vPhase;
in float vErupt;
uniform sampler2D uArt;
uniform vec3 uAccent; uniform vec3 uAccent;
uniform float uTint; uniform float uTime, uHue, uIri, uMetal, uHeat;
uniform float uRain; // 0..1: plasma runs down to the feet instead of along uniform float uGlow; // the drop's rim brightness from the music
uniform float uFlash; // the feet light up on a drop uniform float uRun; // light running along: 0 none, 1 from the feet, -1 to them
out vec4 outColor; out vec4 outColor;
// Glowing plasma from cool to hot: deep red, orange, yellow, white. vec3 hueRotate(vec3 c, float a) {
vec3 heat(float h) { const vec3 k = vec3(0.57735);
vec3 c = mix(vec3(0.45, 0.02, 0.0), vec3(1.0, 0.3, 0.02), smoothstep(0.0, 0.35, h)); float ca = cos(a);
c = mix(c, vec3(1.0, 0.78, 0.25), smoothstep(0.35, 0.7, h)); return c * ca + cross(k, c) * sin(a) + k * dot(k, c) * (1.0 - ca);
return mix(c, vec3(1.0, 0.97, 0.9), smoothstep(0.7, 1.0, h));
} }
// The drop's own shading (shaders.ts), without the refraction: the loops are thin.
void main() { void main() {
float across = 1.0 - abs(vSide); vec3 n = normalize(vNormal);
float core = pow(across, 6.0); vec3 v = normalize(-vPos);
float glow = pow(across, 1.5); float nv = 1.0 - max(dot(n, v), 0.0);
// Knots: along the loop, or from its top down both legs while it rains. float fres = nv * nv * nv;
float s = mix(vT, 1.0 - abs(vT - 0.5) * 2.0, uRain); // The artwork where the loop comes out, blurred into its colours.
float k = fract(s * 2.0 - vPhase); vec3 art = textureLod(uArt, 0.5 + vec2(vObj.x, -vObj.y) * 0.48, 4.5).rgb;
float knot = smoothstep(0.0, 0.06, k) * (1.0 - smoothstep(0.06, 0.32, k)); vec3 rim = hueRotate(uAccent, uHue + uIri * (fres + 0.5 * sin(uTime * 0.3)));
float feet = 1.0 - smoothstep(0.0, 0.14, min(vT, 1.0 - vT)); rim = mix(rim, vec3(1.0, 0.35, 0.1), uHeat * 0.6);
vec3 hot = heat(vHeat); vec3 slick = 0.5 + 0.5 * cos(6.2831853 * (fres * 1.6 + vec3(0.0, 0.33, 0.67) + uTime * 0.05));
vec3 col = hot * (0.35 * glow + 0.8 * core); rim = mix(rim, rim * 0.4 + slick * length(rim) * 0.75, 0.25 + 0.5 * uIri);
col += heat(min(1.0, vHeat + 0.35)) * knot * (0.3 * glow + 1.1 * core);
col += heat(min(1.0, vHeat + 0.5)) * feet * glow * (0.25 + 1.2 * uFlash); vec3 col = art * mix(0.8, 0.4, fres);
// Leaning towards the card's colours, keeping the brightness. col += rim * fres * (0.65 + uGlow);
col = mix(col, uAccent * dot(col, vec3(0.6)), uTint); col += mix(rim, vec3(1.0), 0.5) * fres * fres * (0.3 + 0.8 * uGlow);
// A broken loop fades from the break outwards. vec3 light = normalize(vec3(-0.5, 0.7, 0.6));
col *= vAlpha * (1.0 - vErupt * smoothstep(0.0, 0.5, 1.0 - abs(vT - 0.5) * 2.0) * 0.5); col += pow(max(dot(n, normalize(light + v)), 0.0), mix(40.0, 140.0, uMetal)) * mix(0.35, 0.8, uMetal);
// Added onto what is there; premultiplied, so alpha is the brightest channel. vec3 r = reflect(-v, n);
outColor = vec4(col, max(col.r, max(col.g, col.b))); vec3 env = mix(vec3(0.015), uAccent * 0.5, pow(smoothstep(-0.4, 1.0, r.y), 2.0));
env += 0.5 * smoothstep(0.92, 0.985, dot(r, normalize(vec3(-0.5, 0.7, 0.5))));
col += env * (0.1 + 0.5 * fres);
// Light in the accent runs up both legs, or back down them in a gap.
float s = 1.0 - abs(vT - 0.5) * 2.0;
float k = fract(s - vPhase * sign(uRun));
float run = smoothstep(0.0, 0.08, k) * (1.0 - smoothstep(0.08, 0.4, k)) * abs(uRun);
col += rim * run * (0.25 + 0.6 * fres);
// Glassy: the middle lets the page through, the rim holds the colour.
float a = mix(0.8, 1.0, smoothstep(0.0, 0.5, nv)) * (1.0 - 0.9 * smoothstep(0.8, 1.0, fres)) * vAlpha;
outColor = vec4(col * a, a);
} }
`; `;
const FLARE_UNIFORMS = ["uProj", "uRot", "uScale", "uLift", "uDist", "uTime", "uAccent", "uTint", "uRain", "uFlash"] as const; const FLARE_UNIFORMS = [
"uProj", "uRot", "uScale", "uLift", "uDist", "uTime", "uArt", "uAccent", "uHue", "uIri", "uMetal", "uHeat", "uGlow", "uRun",
] as const;
/// What the drop shares with its loops each frame. /// What the drop shares with its loops each frame.
export interface FlareFrame { export interface FlareFrame {
@@ -155,6 +190,8 @@ export interface FlareFrame {
scale: [number, number, number]; scale: [number, number, number];
lift: number; lift: number;
accent: [number, number, number]; accent: [number, number, number];
/// The drop's flow time, so the loops sway with its surface.
time: number;
/// View direction to the drop's own space (spin.ts). /// View direction to the drop's own space (spin.ts).
toObject: (x: number, y: number, z: number) => [number, number, number]; toObject: (x: number, y: number, z: number) => [number, number, number];
} }
@@ -162,13 +199,11 @@ export interface FlareFrame {
export class Flares { export class Flares {
private prog!: WebGLProgram; private prog!: WebGLProgram;
private vao!: WebGLVertexArrayObject; private vao!: WebGLVertexArrayObject;
private strip!: WebGLBuffer;
private inst!: WebGLBuffer; private inst!: WebGLBuffer;
private count = 0;
private u = {} as Record<(typeof FLARE_UNIFORMS)[number], WebGLUniformLocation | null>; private u = {} as Record<(typeof FLARE_UNIFORMS)[number], WebGLUniformLocation | null>;
private data: Float32Array; private data: Float32Array;
private loops: Loop[] = []; private loops: Loop[] = [];
private heat = 0.3;
private time = 0;
private lastDrop = 0; private lastDrop = 0;
/// Where on the crown the next loop goes, for tracks with scatter. /// Where on the crown the next loop goes, for tracks with scatter.
private turn = Math.random(); private turn = Math.random();
@@ -196,16 +231,34 @@ export class Flares {
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog) || "flare link failed"); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog) || "flare link failed");
this.prog = prog; this.prog = prog;
for (const name of FLARE_UNIFORMS) this.u[name] = gl.getUniformLocation(prog, name); for (const name of FLARE_UNIFORMS) this.u[name] = gl.getUniformLocation(prog, name);
gl.useProgram(prog);
gl.uniform1i(this.u.uArt, 0);
// A tube: rings of SIDES + 1 points (the seam twice) at SEGMENTS + 1 steps.
const ring = SIDES + 1;
const tube = new Float32Array((SEGMENTS + 1) * ring * 2);
for (let i = 0; i <= SEGMENTS; i++) for (let j = 0; j < ring; j++) tube.set([i / SEGMENTS, j / SIDES], (i * ring + j) * 2);
const idx = new Uint16Array(SEGMENTS * SIDES * 6);
let o = 0;
for (let i = 0; i < SEGMENTS; i++) {
for (let j = 0; j < SIDES; j++) {
const p = i * ring + j, q = p + ring;
idx.set([p, q, p + 1, p + 1, q, q + 1], o);
o += 6;
}
}
this.count = idx.length;
this.vao = gl.createVertexArray(); this.vao = gl.createVertexArray();
gl.bindVertexArray(this.vao); gl.bindVertexArray(this.vao);
const strip = new Float32Array((SEGMENTS + 1) * 4); const pos = gl.createBuffer();
for (let i = 0; i <= SEGMENTS; i++) strip.set([i / SEGMENTS, -1, i / SEGMENTS, 1], i * 4); gl.bindBuffer(gl.ARRAY_BUFFER, pos);
this.strip = gl.createBuffer(); gl.bufferData(gl.ARRAY_BUFFER, tube, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, this.strip);
gl.bufferData(gl.ARRAY_BUFFER, strip, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0); gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
const ib = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ib);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, idx, gl.STATIC_DRAW);
this.inst = gl.createBuffer(); this.inst = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.inst); gl.bindBuffer(gl.ARRAY_BUFFER, this.inst);
gl.bufferData(gl.ARRAY_BUFFER, this.data.byteLength, gl.DYNAMIC_DRAW); gl.bufferData(gl.ARRAY_BUFFER, this.data.byteLength, gl.DYNAMIC_DRAW);
@@ -240,12 +293,10 @@ export class Flares {
return this.loops.length > 0; return this.loops.length > 0;
} }
/// Step the loops by `dt` and draw them. `strength` 0 lets them die out. /// Step the loops by `dt` and draw them. `strength` 0 lets them sink back.
/// True while there are loops left. /// True while there are loops left.
frame(f: Features, dt: number, look: Look, playing: boolean, strength: number, period: number, view: FlareFrame): boolean { frame(f: Features, dt: number, look: Look, playing: boolean, strength: number, period: number, view: FlareFrame): boolean {
const on = playing && strength > 0; const on = playing && strength > 0;
this.time += dt;
this.heat += (Math.min(1, 0.3 + 0.55 * f.growl * Math.max(0.4, f.bass) + 0.35 * f.drop) - this.heat) * (1 - Math.exp(-dt / HEAT_SMOOTH));
if (on && f.kickHit >= KICK_MIN && f.gap < 0.5) this.spawn(f, look, period, view); if (on && f.kickHit >= KICK_MIN && f.gap < 0.5) this.spawn(f, look, period, view);
// The drop: a burst all round. // The drop: a burst all round.
if (on && f.drop > DROP_BURST && this.lastDrop <= DROP_BURST) { if (on && f.drop > DROP_BURST && this.lastDrop <= DROP_BURST) {
@@ -253,37 +304,43 @@ export class Flares {
} }
this.lastDrop = f.drop; this.lastDrop = f.drop;
if (on && f.snareHit >= look.erupt) { if (on && f.snareHit >= look.erupt) {
const victim = this.loops.filter((l) => l.erupt < 0 && l.age > RISE).sort((x, y) => y.age - x.age)[0]; const victim = this.loops.filter((l) => l.erupt < 0 && l.height > 0.5 * l.goal * look.arch).sort((x, y) => y.age - x.age)[0];
if (victim) victim.erupt = 0; if (victim) victim.erupt = 0;
} }
// Knots run once along a loop per womb, or with the beat while there is no wobble. // Light runs once along a loop per womb, or with the beat while there is no wobble.
const rate = f.wobbleRate > 0.3 ? f.wobbleRate : 0.5 / period; const rate = f.wobbleRate > 0.3 ? f.wobbleRate : 0.5 / period;
const want = look.arch * (0.3 + 0.55 * f.bass + 0.3 * f.drop) * (1 - 0.8 * f.gap); const lift = look.arch * (0.35 + 0.5 * f.bass + 0.25 * f.drop) * (1 - 0.85 * f.gap);
const follow = 1 - Math.exp(-dt / 0.06);
this.loops = this.loops.filter((l) => { this.loops = this.loops.filter((l) => {
l.age += dt; l.age += dt;
l.phase += dt * rate * (1 - 0.5 * f.gap); l.phase += dt * rate;
l.height += (want * (0.85 + 0.3 * l.seed) - l.height) * follow;
if (l.erupt >= 0) l.erupt += dt; if (l.erupt >= 0) l.erupt += dt;
// Without music they go, quicker than they would. // Without music, or past its life, it sinks back in.
if (!on) l.life = Math.min(l.life, l.age + FADE); const sinking = !on || l.age > l.life;
return l.erupt < ERUPT && l.age < l.life; const goal = sinking ? 0 : lift * l.goal;
for (let left = Math.min(dt, 0.1); left > 0; left -= 1 / 240) {
const h = Math.min(left, 1 / 240);
l.speed += (-SPRING * (l.height - goal) - DAMPING * l.speed) * h;
l.height += l.speed * h;
}
if (l.erupt >= ERUPT) return false;
return !(sinking && l.height < 0.01 && l.speed <= 0);
}); });
if (!this.loops.length) return false; if (!this.loops.length) return false;
const d = this.data; const d = this.data;
const n = Math.min(this.loops.length, this.max); const n = Math.min(this.loops.length, this.max);
// As thick as it is out: a loop grows out of the surface and sinks back thin.
const full = Math.max(0.05, 0.6 * look.arch);
const sway = 0.3 + 1.2 * f.growl * f.bass + 0.4 * f.wobble;
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const l = this.loops[i]; const l = this.loops[i];
const rise = Math.min(1, l.age / RISE); const grown = Math.min(1, Math.max(0, l.height) / full);
const fade = Math.min(1, (l.life - l.age) / FADE);
const alpha = rise * rise * (3 - 2 * rise) * Math.max(0, fade) * Math.max(strength, on ? 0 : 1);
const e = l.erupt < 0 ? 0 : l.erupt / ERUPT; const e = l.erupt < 0 ? 0 : l.erupt / ERUPT;
d.set([ d.set([
...l.a, ...l.b, ...l.a, ...l.b,
l.height * rise, alpha * (1 - e * e), this.heat, l.seed, Math.max(0, l.height), Math.min(1, strength + (on ? 0 : 1)), Math.sqrt(grown), l.seed,
e, 0.2 + 1.2 * f.growl * f.bass + 0.3 * f.wobble, WIDTH * (1 + 0.6 * f.drop), l.phase, e, sway, WIDTH * (1 + 0.3 * f.drop + 0.2 * f.bass), l.phase,
0, 0, 0, 0,
], i * FLOATS); ], i * FLOATS);
} }
@@ -296,20 +353,18 @@ export class Flares {
gl.uniform3f(u.uScale, ...view.scale); gl.uniform3f(u.uScale, ...view.scale);
gl.uniform1f(u.uLift, view.lift); gl.uniform1f(u.uLift, view.lift);
gl.uniform1f(u.uDist, view.dist); gl.uniform1f(u.uDist, view.dist);
gl.uniform1f(u.uTime, this.time); gl.uniform1f(u.uTime, view.time);
gl.uniform3fv(u.uAccent, view.accent); gl.uniform3fv(u.uAccent, view.accent);
gl.uniform1f(u.uTint, look.tint); gl.uniform1f(u.uHue, look.hue);
gl.uniform1f(u.uRain, f.gap); gl.uniform1f(u.uIri, look.iri);
gl.uniform1f(u.uFlash, f.drop); gl.uniform1f(u.uMetal, look.metal);
gl.uniform1f(u.uHeat, look.heat);
gl.uniform1f(u.uGlow, 0.9 * f.kick + 0.5 * f.drop + 0.5 * f.bass * look.throb);
gl.uniform1f(u.uRun, f.gap > 0.5 ? -1 : Math.min(1, 0.3 + f.bass + f.wobble));
gl.bindVertexArray(this.vao); gl.bindVertexArray(this.vao);
gl.bindBuffer(gl.ARRAY_BUFFER, this.inst); gl.bindBuffer(gl.ARRAY_BUFFER, this.inst);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, d, 0, n * FLOATS); gl.bufferSubData(gl.ARRAY_BUFFER, 0, d, 0, n * FLOATS);
// Added on, tested against the drop but not hiding each other. gl.drawElementsInstanced(gl.TRIANGLES, this.count, gl.UNSIGNED_SHORT, 0, n);
gl.depthMask(false);
gl.blendFunc(gl.ONE, gl.ONE);
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, (SEGMENTS + 1) * 2, n);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.depthMask(true);
gl.bindVertexArray(null); gl.bindVertexArray(null);
return true; return true;
} }
@@ -318,9 +373,9 @@ export class Flares {
/// it, so it arcs out past the edge. /// it, so it arcs out past the edge.
private spawn(f: Features, look: Look, period: number, view: FlareFrame, anywhere = false): void { private spawn(f: Features, look: Look, period: number, view: FlareFrame, anywhere = false): void {
if (this.loops.length >= this.max) { if (this.loops.length >= this.max) {
// The oldest that is not breaking makes room. // The oldest that is not breaking sinks back to make room.
const oldest = this.loops.reduce((o, l) => (l.erupt < 0 && l.age > o.age ? l : o), this.loops[0]); const oldest = this.loops.reduce((o, l) => (l.erupt < 0 && l.age > o.age ? l : o), this.loops[0]);
oldest.life = Math.min(oldest.life, oldest.age + FADE); oldest.life = Math.min(oldest.life, oldest.age);
if (this.loops.length >= this.max + 2) return; if (this.loops.length >= this.max + 2) return;
} }
const crown = Math.max(1, Math.round(look.crown)); const crown = Math.max(1, Math.round(look.crown));
@@ -333,7 +388,7 @@ export class Flares {
} }
// Upright crowns read best: the first slot sits at the top. // Upright crowns read best: the first slot sits at the top.
const angle = Math.PI / 2 - (slot / crown) * Math.PI * 2; const angle = Math.PI / 2 - (slot / crown) * Math.PI * 2;
const z = -0.1 + Math.random() * 0.4; const z = -0.1 + Math.random() * 0.35;
const r = Math.sqrt(1 - z * z); const r = Math.sqrt(1 - z * z);
const c: [number, number, number] = [Math.cos(angle) * r, Math.sin(angle) * r, z]; const c: [number, number, number] = [Math.cos(angle) * r, Math.sin(angle) * r, z];
const tx = -Math.sin(angle), ty = Math.cos(angle); const tx = -Math.sin(angle), ty = Math.cos(angle);
@@ -352,6 +407,8 @@ export class Flares {
seed: Math.random(), seed: Math.random(),
phase: Math.random(), phase: Math.random(),
height: 0, height: 0,
speed: 0,
goal: 0.85 + 0.3 * Math.random(),
}); });
} }
} }
+6 -8
View File
@@ -41,33 +41,31 @@ export interface Look {
arch: number; arch: number;
/// Beats a loop lives. /// Beats a loop lives.
life: number; life: number;
/// Snares weaker than this break no loop. /// Snares weaker than this pinch no loop.
erupt: number; erupt: number;
/// How far the loops' colour leans from the sun's towards the card's accent.
tint: number;
} }
const BASE: Look = { const BASE: Look = {
flow: 1, gain: 1, spike: 0.5, step: 0, throb: 1, bounce: 1, stiff: 90, twist: 0.15, ripple: 1, shock: 1, iri: 0.1, metal: 0.3, heat: 0, hue: 0, flow: 1, gain: 1, spike: 0.5, step: 0, throb: 1, bounce: 1, stiff: 90, twist: 0.15, ripple: 1, shock: 1, iri: 0.1, metal: 0.3, heat: 0, hue: 0,
flares: 0, crown: 6, scatter: 0.4, arch: 0.5, life: 4, erupt: 0.45, tint: 0.15, flares: 0, crown: 6, scatter: 0.4, arch: 0.32, life: 4, erupt: 0.45,
}; };
const TAGS: Record<string, (l: Look) => void> = { const TAGS: Record<string, (l: Look) => void> = {
// Riddim: the loops snap to the beat and the crown repeats every bar. // Riddim: the loops snap to the beat and the crown repeats every bar.
riddim: (l) => Object.assign(l, { riddim: (l) => Object.assign(l, {
step: 0.5, spike: 0.6, throb: 1.4, bounce: 1.3, stiff: 120, metal: 0.9, shock: 1.4, ripple: 1.2, step: 0.5, spike: 0.6, throb: 1.4, bounce: 1.3, stiff: 120, metal: 0.9, shock: 1.4, ripple: 1.2,
flares: 1, crown: 4, scatter: 0, arch: 0.55, life: 4, erupt: 0.4, flares: 1, crown: 4, scatter: 0, arch: 0.36, life: 4, erupt: 0.4,
}), }),
tearout: (l) => Object.assign(l, { spike: 1.3, bounce: 1.4, gain: l.gain * 1.15, heat: 0.7, flares: 1, crown: 6, scatter: 0.5, erupt: 0.25 }), tearout: (l) => Object.assign(l, { spike: 1.3, bounce: 1.4, gain: l.gain * 1.15, heat: 0.7, flares: 1, crown: 6, scatter: 0.5, erupt: 0.25 }),
deathstep: (l) => Object.assign(l, { spike: 1.4, bounce: 1.4, gain: l.gain * 1.15, heat: 0.9, metal: 0.7, flares: 1, crown: 6, scatter: 0.6, erupt: 0.2 }), deathstep: (l) => Object.assign(l, { spike: 1.4, bounce: 1.4, gain: l.gain * 1.15, heat: 0.9, metal: 0.7, flares: 1, crown: 6, scatter: 0.6, erupt: 0.2 }),
melodic: (l) => Object.assign(l, { melodic: (l) => Object.assign(l, {
iri: 0.8, spike: 0.15, throb: 0.7, bounce: 0.6, stiff: 55, gain: l.gain * 0.9, flow: l.flow * 0.8, metal: 0.1, iri: 0.8, spike: 0.15, throb: 0.7, bounce: 0.6, stiff: 55, gain: l.gain * 0.9, flow: l.flow * 0.8, metal: 0.1,
flares: 0, crown: 5, scatter: 0.3, arch: 0.8, life: 8, erupt: 0.8, tint: 0.5, flares: 0, crown: 5, scatter: 0.3, arch: 0.45, life: 8, erupt: 0.8,
}), }),
colourbass: (l) => Object.assign(l, { iri: 1, spike: 0.25, bounce: 0.8, stiff: 70, metal: 0.15, tint: 0.6 }), colourbass: (l) => Object.assign(l, { iri: 1, spike: 0.25, bounce: 0.8, stiff: 70, metal: 0.15 }),
dnb: (l) => Object.assign(l, { ripple: 1.6 }), dnb: (l) => Object.assign(l, { ripple: 1.6 }),
"170": (l) => Object.assign(l, { ripple: 1.5 }), "170": (l) => Object.assign(l, { ripple: 1.5 }),
halftime: (l) => Object.assign(l, { flow: l.flow * 0.7, gain: l.gain * 1.15, stiff: 70, crown: 2, arch: 0.7 }), halftime: (l) => Object.assign(l, { flow: l.flow * 0.7, gain: l.gain * 1.15, stiff: 70, crown: 2, arch: 0.45 }),
wonk: (l) => Object.assign(l, { twist: 0.9 }), wonk: (l) => Object.assign(l, { twist: 0.9 }),
}; };