mirror of
https://github.com/narrrl/pl8list-rewind.git
synced 2026-11-03 16:41:26 +01:00
Flares as loops of the drop's own liquid
This commit is contained in:
1 parent
c4424ebd4d
commit
f2c8915e5a
3 files changed
+183
-127
No files matched your search
@@ -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
@@ -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(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user