mirror of
https://github.com/narrrl/pl8list-rewind.git
synced 2026-11-13 17:17:03 +01:00
redesign(frontend)
This commit is contained in:
1 parent
7fe01b5950
commit
0a3b43a37a
34 files changed
+473
-1292
No files matched your search
+19
-9
@@ -17,13 +17,13 @@ const DIST = SPILL / Math.tan(FOV / 2);
|
||||
const MORPH_IN = 1.4; // s from flat artwork to liquid
|
||||
// The jump: a spring that each prominent kick or bass hit kicks upwards. Hits are
|
||||
// weighed by prominence squared, so only the strongest really make it jump.
|
||||
const JUMP = 1.9; // upward speed of a full-strength hit, in radii per second
|
||||
const JUMP_DAMPING = 7; // 1/s; low enough that it jiggles a few times as it lands
|
||||
const MAX_LIFT = 0.16; // radii
|
||||
const JUMP = 0.5; // upward speed of a full-strength hit, in radii per second
|
||||
const JUMP_DAMPING = 22; // 1/s; low enough that it jiggles a few times as it lands
|
||||
const MAX_LIFT = 0.035; // radii
|
||||
const STRETCH = 0.08; // stretch per unit of speed: long while flying, squashed on landing
|
||||
const PUMP = 0.7; // size kick of a snare hit
|
||||
const PUMP_STIFF = 160;
|
||||
const PUMP_DAMPING = 12;
|
||||
const PUMP_DAMPING = 24;
|
||||
const THROB = 0.09; // size swing with the bass line's envelope
|
||||
const WHIRL = 15; // rad/s of hand spin at which it bulges and sloshes the most
|
||||
const SNARE_ATTACK = 0.03; // s; spikes grow in rather than pop
|
||||
@@ -117,6 +117,8 @@ export class LiquidBlob {
|
||||
private jump = [0, 0];
|
||||
private pump = [0, 0];
|
||||
private snare = 0;
|
||||
private dropArmed = true;
|
||||
private dropImpulse = 0;
|
||||
/// The icon: its shape (0 pause, 1 play), how much it shows, and since when the
|
||||
/// playing state is what it is. `iconFor` is null until the first frame of a track.
|
||||
private iconShape = 1;
|
||||
@@ -241,6 +243,8 @@ export class LiquidBlob {
|
||||
this.slow = this.fast = 0;
|
||||
this.spin.reset();
|
||||
this.ferro.reset();
|
||||
this.dropArmed = true;
|
||||
this.dropImpulse = 0;
|
||||
this.light = 0;
|
||||
this.iconFor = null;
|
||||
const source = await artwork(art, initial, tint).catch(() => artwork(null, initial, tint));
|
||||
@@ -295,11 +299,17 @@ export class LiquidBlob {
|
||||
this.morph = Math.min(1, this.morph + dt / MORPH_IN);
|
||||
const morph = this.morph * this.morph * (3 - 2 * this.morph);
|
||||
// The liquid flows with the tempo, faster when the music moves more.
|
||||
this.time += dt * look.flow * (playing ? 0.6 + 0.8 * f.wobble + 0.6 * f.drop : 0.25);
|
||||
this.time += dt * look.flow * (playing ? (0.25 + 0.8 * f.wobble + 0.3 * this.dropImpulse) * (1 - 0.9 * f.gap) : 0.25);
|
||||
const k = 1 - Math.exp(-dt / 0.15);
|
||||
for (let i = 0; i < 4; i++) this.pointer[i] += (this.pointerGoal[i] - this.pointer[i]) * k;
|
||||
const spinning = this.spin.step(dt);
|
||||
const showing = this.showIcon(s, dt);
|
||||
if (f.drop < 0.2) this.dropArmed = true;
|
||||
if (playing && this.dropArmed && f.drop > 0.55) {
|
||||
this.dropImpulse = 1;
|
||||
this.dropArmed = false;
|
||||
}
|
||||
this.dropImpulse *= Math.exp(-dt / 0.22);
|
||||
const moving = this.bounce(f, dt, look);
|
||||
|
||||
const u = this.u;
|
||||
@@ -310,12 +320,12 @@ export class LiquidBlob {
|
||||
gl.uniform1f(u.uWobble, f.wobble);
|
||||
gl.uniform1f(u.uKick, f.kick);
|
||||
gl.uniform1f(u.uKickAge, f.kickAge);
|
||||
gl.uniform1f(u.uDrop, f.drop);
|
||||
gl.uniform1f(u.uDrop, this.dropImpulse);
|
||||
gl.uniform1f(u.uBass, f.bass * look.throb);
|
||||
gl.uniform1f(u.uGrowl, f.growl);
|
||||
this.snare += (f.snare - this.snare) * ease(dt, f.snare > this.snare ? SNARE_ATTACK : SNARE_RELEASE);
|
||||
gl.uniform1f(u.uSnare, this.snare);
|
||||
gl.uniform1f(u.uGain, look.gain);
|
||||
gl.uniform1f(u.uGain, look.gain * (1 - 0.85 * f.gap));
|
||||
gl.uniform1f(u.uSpike, look.spike);
|
||||
gl.uniform1f(u.uStep, look.step);
|
||||
gl.uniform1f(u.uTwist, look.twist);
|
||||
@@ -368,7 +378,7 @@ export class LiquidBlob {
|
||||
if (f.kickHit) jump[1] += JUMP * look.bounce * f.kickHit ** 2;
|
||||
if (f.snareHit) pump[1] += PUMP * look.bounce * f.snareHit ** 2;
|
||||
// The size also follows the bass and the drop, through the same spring.
|
||||
const pumpRest = 0.04 * f.low + 0.05 * f.drop;
|
||||
const pumpRest = (0.025 * f.low + 0.065 * this.dropImpulse - 0.045 * f.kick) * (1 - f.gap);
|
||||
// Small fixed steps keep the springs stable on slow frames.
|
||||
for (let left = Math.min(dt, 0.1); left > 0; left -= 1 / 240) {
|
||||
const h = Math.min(left, 1 / 240);
|
||||
@@ -381,7 +391,7 @@ export class LiquidBlob {
|
||||
// Long while it flies up, squashed as it comes down and lands.
|
||||
const stretch = 1 + Math.min(0.18, Math.max(-0.15, jump[1] * STRETCH + Math.min(0, jump[0]) * 1.2));
|
||||
// Each "womb" of the bass line swells the whole drop.
|
||||
const size = 1 + pump[0] + THROB * look.throb * f.bass;
|
||||
const size = Math.max(0.88, Math.min(1.18, 1 + pump[0] + THROB * look.throb * f.bass * (1 - f.gap)));
|
||||
// Spun fast it flattens out a little, like dough, and sloshes.
|
||||
const whirl = Math.min(1, this.spin.speed / WHIRL);
|
||||
const side = (size / Math.sqrt(stretch)) * (1 + 0.08 * whirl);
|
||||
|
||||
@@ -141,7 +141,7 @@ export class Caustics {
|
||||
gl.uniform1f(u.uHue, look.hue);
|
||||
gl.uniform1f(u.uIri, look.iri);
|
||||
gl.uniform1f(u.uHeat, look.heat);
|
||||
gl.uniform1f(u.uAmount, c.amount * look.caustics);
|
||||
gl.uniform1f(u.uAmount, c.amount * look.caustics * 0.22);
|
||||
gl.disable(gl.DEPTH_TEST);
|
||||
gl.depthMask(false);
|
||||
gl.bindVertexArray(this.vao);
|
||||
|
||||
@@ -35,7 +35,7 @@ export class Ferro {
|
||||
private charge = new Float32Array(SPIKES);
|
||||
private seed = new Float32Array(SPIKES);
|
||||
private clock = 0;
|
||||
private lastDrop = 0;
|
||||
private dropArmed = true;
|
||||
/// Where on the crown the next kick lands, for tracks with scatter.
|
||||
private turn = Math.random();
|
||||
/// The tallest spike, so the shader can skip them all when there are none.
|
||||
@@ -58,7 +58,7 @@ export class Ferro {
|
||||
this.height.fill(0);
|
||||
this.speed.fill(0);
|
||||
this.charge.fill(0);
|
||||
this.lastDrop = 0;
|
||||
this.dropArmed = true;
|
||||
this.tallest = 0;
|
||||
for (let i = 0; i < SPIKES; i++) this.data[i * 4 + 3] = 0;
|
||||
}
|
||||
@@ -70,13 +70,15 @@ export class Ferro {
|
||||
const n = SPIKES;
|
||||
this.clock += dt;
|
||||
if (on && f.kickHit >= KICK_MIN && f.gap < 0.5) this.kick(f, look, reach, toObject);
|
||||
if (on && f.drop > DROP_BURST && this.lastDrop <= DROP_BURST) {
|
||||
if (f.drop < 0.2) this.dropArmed = true;
|
||||
if (on && f.drop > DROP_BURST && this.dropArmed) {
|
||||
this.dropArmed = false;
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.charge[i] = 1;
|
||||
this.speed[i] += 2.5 * reach;
|
||||
}
|
||||
}
|
||||
this.lastDrop = f.drop;
|
||||
|
||||
if (on && f.snareHit >= look.jolt) {
|
||||
for (let i = 0; i < n; i++) this.speed[i] += f.snareHit * reach * (0.5 + 3 * this.charge[i]);
|
||||
}
|
||||
|
||||
+10
-7
@@ -265,22 +265,25 @@ void main() {
|
||||
// Looking through the surface bends the artwork like water does.
|
||||
vec2 bend = n.xy * (0.22 + 0.1 * uDrop) * uMorph;
|
||||
vec2 uv = 0.5 + vec2(vObj.x, -vObj.y) * 0.48 - vec2(bend.x, -bend.y) * 0.5;
|
||||
vec2 split = n.xy * (0.008 + 0.025 * uKick + 0.015 * uSnare + 0.01 * uGrowl * uBass) * uMorph;
|
||||
vec2 split = n.xy * (0.001 + 0.003 * uKick + 0.002 * uSnare) * uMorph;
|
||||
// Turned sideways the projection smears; blur it into flowing colour instead.
|
||||
float blur = 4.0 * pow(1.0 - abs(vObj.z), 3.0) * uMorph;
|
||||
vec3 art = vec3(texture(uArt, uv + split, blur).r, texture(uArt, uv, blur).g, texture(uArt, uv - split, blur).b);
|
||||
art = mix(art, smoothstep(0.0, 1.0, art), 0.6 * uDrop);
|
||||
float luminance = dot(art, vec3(0.2126, 0.7152, 0.0722));
|
||||
vec3 trace = mix(vec3(luminance), art, 0.22);
|
||||
// Preserve the flat artwork during entry; blackened metal takes over as it morphs.
|
||||
art = mix(art, vec3(0.035, 0.038, 0.042) + trace * 0.22, uMorph);
|
||||
|
||||
vec3 light = normalize(vec3(-0.5, 0.7, 0.6));
|
||||
float spec = pow(max(dot(n, normalize(light + v)), 0.0), mix(40.0, 140.0, uMetal)) * mix(0.35, 0.8, uMetal);
|
||||
// The hue sways around the accent but never leaves it, so the drop stays in the
|
||||
// card's colours.
|
||||
vec3 rim = hueRotate(uAccent, uHue + uIri * (fres + 0.5 * sin(uTime * 0.3)));
|
||||
vec3 rim = hueRotate(mix(vec3(0.72), uAccent, 0.16), uHue + uIri * (fres + 0.5 * sin(uTime * 0.3)));
|
||||
rim = mix(rim, vec3(1.0, 0.35, 0.1), uHeat * 0.6);
|
||||
// Oil-slick fringes: each channel cycles at its own phase across the edge, worn
|
||||
// over the accent, more for iridescent tracks.
|
||||
vec3 slick = 0.5 + 0.5 * cos(6.2831853 * (fres * 1.6 + vec3(0.0, 0.33, 0.67) + uTime * 0.05));
|
||||
rim = mix(rim, rim * 0.4 + slick * length(rim) * 0.75, 0.25 + 0.5 * uIri);
|
||||
rim = mix(rim, rim * 0.4 + slick * length(rim) * 0.75, 0.035 + 0.08 * uIri);
|
||||
// The fresnel per channel, red reaching in further than blue: a lens fringe.
|
||||
float nv = 1.0 - max(dot(n, v), 0.0);
|
||||
vec3 fresC = vec3(nv * nv * nv * mix(1.0, nv, -0.35), fres, nv * nv * nv * mix(1.0, nv, 0.35));
|
||||
@@ -290,13 +293,13 @@ void main() {
|
||||
// Out on a spike the liquid turns to ink, like ferrofluid: the artwork darkens and
|
||||
// the gloss takes over.
|
||||
vec3 col = art * mix(1.0, 0.45, fres * uMorph) * (1.0 - 0.6 * vSpike);
|
||||
col += rim * fresC * (0.65 + 0.9 * uKick + 0.5 * uDrop + 0.5 * uBass) * uMorph;
|
||||
col += mix(rim, vec3(1.0), 0.5) * edge * (0.3 + 1.2 * uKick + 0.8 * uDrop) * uMorph;
|
||||
col += rim * fresC * (0.3 + 0.35 * uKick + 0.15 * uDrop + 0.12 * uBass) * uMorph;
|
||||
col += mix(rim, vec3(1.0), 0.5) * edge * (0.15 + 0.45 * uKick + 0.25 * uDrop) * uMorph;
|
||||
col += spec * (1.0 + 0.8 * vSpike) * uMorph;
|
||||
// Gloss: a dark room lit by the accent from above, and one soft window. They stay
|
||||
// put while the surface turns under them, which is what makes a spin read.
|
||||
vec3 r = reflect(-v, n);
|
||||
vec3 env = mix(vec3(0.015), uAccent * 0.5, pow(smoothstep(-0.4, 1.0, r.y), 2.0));
|
||||
vec3 env = mix(vec3(0.015), mix(vec3(0.5), uAccent * 0.5, 0.12), 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 + 0.5 * vSpike) * uMorph;
|
||||
// The icon sits on a disc of frosted glass, painted onto the surface so it turns
|
||||
|
||||
@@ -48,14 +48,14 @@ export interface 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: 0.35, stiff: 160, twist: 0.15, ripple: 1, shock: 1, iri: 0.1, metal: 0.3, heat: 0, hue: 0,
|
||||
ferro: 0, crown: 6, scatter: 0.4, reach: 0.22, hold: 2, jolt: 0.45, caustics: 1,
|
||||
};
|
||||
|
||||
const TAGS: Record<string, (l: Look) => void> = {
|
||||
// Riddim: the spikes stand in step and the crown repeats every bar.
|
||||
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.7, spike: 0.6, throb: 1.4, bounce: 0.4, stiff: 200, metal: 0.9, shock: 1.4, ripple: 1.2,
|
||||
ferro: 1, crown: 4, scatter: 0, reach: 0.26, hold: 2, jolt: 0.4,
|
||||
}),
|
||||
tearout: (l) => Object.assign(l, { spike: 1.3, bounce: 1.4, gain: l.gain * 1.15, heat: 0.7, ferro: 1, crown: 6, scatter: 0.5, reach: 0.3, jolt: 0.25 }),
|
||||
@@ -78,6 +78,7 @@ export function lookFor(t: Track): Look {
|
||||
// Untagged dubstep tempo counts too: most riddim carries no riddim tag.
|
||||
if (tags.includes("140")) l.ferro = 1;
|
||||
for (const tag of tags) TAGS[tag]?.(l);
|
||||
l.bounce = Math.min(l.bounce, 0.45);
|
||||
l.hue = keyHue(t.key);
|
||||
return l;
|
||||
}
|
||||
|
||||
+1
-1
@@ -283,7 +283,7 @@ export class Card {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
const data = this.track.waveform || [];
|
||||
const max = Math.max(1, ...data);
|
||||
const accent = getComputedStyle(canvas).getPropertyValue("--card-accent").trim() || "#ff4d6d";
|
||||
const accent = getComputedStyle(canvas).getPropertyValue("--accent").trim() || "#e9e8e3";
|
||||
const base = Math.round(h * (1 - MIRROR)) - 1;
|
||||
const below = h - base - 2;
|
||||
const level = (i: number) => (data.length ? data[Math.floor((i / bars) * data.length)] / max : 0.15);
|
||||
|
||||
+2
-2
@@ -50,8 +50,8 @@ async function compute(src: string): Promise<string | null> {
|
||||
|
||||
const BACKDROP = 64; // px square; it is shown blurred, so this is plenty
|
||||
const BLUR = 2; // px each side, per box blur pass at that size
|
||||
const DIM = 0.45;
|
||||
const SATURATE = 1.3;
|
||||
const DIM = 0.16;
|
||||
const SATURATE = 0.15;
|
||||
|
||||
/// The card's background from the artist image, as a data URL: cropped square,
|
||||
/// blurred and darkened here, once, as `.bg`'s CSS filter would do on every frame.
|
||||
|
||||
+1
-1
@@ -1319,7 +1319,7 @@ function signedIn(user: User, pl8list?: Pl8Status): void {
|
||||
startEl.classList.remove("welcome");
|
||||
accountBtn.textContent = guest ? "sign in" : (user.name || user.handle)[0]?.toUpperCase() ?? "?";
|
||||
accountBtn.classList.toggle("guest", guest);
|
||||
accountBtn.style.backgroundColor = guest ? "var(--glass-strong)" : accentForName(user.handle);
|
||||
accountBtn.style.backgroundColor = guest ? "var(--glass-strong)" : "var(--accent)";
|
||||
accountBtn.title = guest ? "listening as a guest: sign in, or forget me" : `@${user.handle}`;
|
||||
$("account-handle").textContent = guest ? "listening as a guest" : `@${user.handle}`;
|
||||
$("delete-handle").textContent = user.handle;
|
||||
|
||||
+69
-32
@@ -1,24 +1,28 @@
|
||||
@font-face { font-family: Barlow; src: url('/fonts/Barlow-Regular.ttf') format('truetype'); font-weight: 400; font-display: swap; }
|
||||
@font-face { font-family: 'Barlow Condensed'; src: url('/fonts/BarlowCondensed-Bold.ttf') format('truetype'); font-weight: 700 900; font-display: swap; }
|
||||
|
||||
/* Design tokens ---------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #000;
|
||||
--bg: #090a0b;
|
||||
--fg: #f2f2f2;
|
||||
--muted: #b5b5b8;
|
||||
--faint: #96969a;
|
||||
--accent: #ff4d6d;
|
||||
--panel: rgba(18, 18, 20, 0.94);
|
||||
--accent: #e9e8e3;
|
||||
--danger: #ff8b8b;
|
||||
--panel: #141517;
|
||||
--line: #ffffff1f;
|
||||
--line-strong: #333;
|
||||
--scrim: #000b;
|
||||
--glass: rgba(255, 255, 255, 0.1);
|
||||
--glass-strong: rgba(255, 255, 255, 0.2);
|
||||
--radius-s: 8px;
|
||||
--radius-m: 14px;
|
||||
--radius-l: 18px;
|
||||
--radius-s: 4px;
|
||||
--radius-m: 6px;
|
||||
--radius-l: 6px;
|
||||
--shadow: 0 16px 40px #000a;
|
||||
--font: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
--font-display: "Arial Narrow", "Roboto Condensed", "Liberation Sans Narrow", sans-serif;
|
||||
--font: Barlow, system-ui, sans-serif;
|
||||
--font-display: "Barlow Condensed", "Arial Narrow", sans-serif;
|
||||
--ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
--fast: 0.15s;
|
||||
}
|
||||
@@ -46,7 +50,7 @@ button {
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
padding: 0.35em 0.9em;
|
||||
}
|
||||
|
||||
@@ -90,7 +94,7 @@ body.zen :is(#discovery-options, #active-filters) { display: none; }
|
||||
display: flex;
|
||||
gap: 0.2rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
@@ -147,7 +151,7 @@ body.zen :is(#discovery-options, #active-filters) { display: none; }
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: linear-gradient(90deg, var(--fg) var(--v), #ffffff30 var(--v)) center / 100% 4px no-repeat;
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#volume-slider::-webkit-slider-thumb {
|
||||
@@ -247,7 +251,7 @@ body.zen :is(#discovery-options, #active-filters) { display: none; }
|
||||
flex: 1 1 10rem;
|
||||
min-width: 0;
|
||||
padding: 0.55em 0.9em;
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--line-strong);
|
||||
background: #111;
|
||||
}
|
||||
@@ -258,7 +262,7 @@ body.zen :is(#discovery-options, #active-filters) { display: none; }
|
||||
.modes input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.modes label:has(input) {
|
||||
padding: 0.3em 0.9em;
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ffffff40;
|
||||
font-size: 0.85rem;
|
||||
transition: background var(--fast);
|
||||
@@ -274,7 +278,7 @@ body.zen :is(#discovery-options, #active-filters) { display: none; }
|
||||
width: 2.6rem;
|
||||
height: 1.5rem;
|
||||
margin: 0;
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
background: #ffffff26;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
@@ -435,7 +439,7 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
|
||||
position: absolute;
|
||||
inset: -40px;
|
||||
background: #111 center / cover no-repeat;
|
||||
filter: blur(40px) brightness(0.45) saturate(1.3);
|
||||
filter: blur(40px) brightness(0.16) saturate(0.15);
|
||||
}
|
||||
/* Already blurred and darkened (color.ts): a CSS blur over the whole screen would
|
||||
be worked out again whenever anything on top of it moves. */
|
||||
@@ -446,7 +450,7 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(55% 40% at 50% 40%, color-mix(in oklab, var(--card-accent) 55%, transparent), transparent 70%);
|
||||
opacity: calc(0.18 + var(--energy) * 0.55);
|
||||
opacity: calc(0.03 + var(--energy) * 0.07);
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
@@ -502,11 +506,11 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
|
||||
.card.liquid .art::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -65%;
|
||||
inset: -25%;
|
||||
background: radial-gradient(closest-side, color-mix(in oklab, var(--card-accent) 83%, transparent), transparent);
|
||||
/* Pulses through transform and opacity only, so it never has to be painted again. */
|
||||
transform: scale(calc(0.87 + var(--energy) * 0.13));
|
||||
opacity: calc(0.34 + var(--energy) * 0.66);
|
||||
opacity: calc(0.06 + var(--energy) * 0.12);
|
||||
will-change: transform, opacity;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -607,27 +611,27 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
|
||||
gap: 0.3em;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2em 0.75em 0.2em 0.55em;
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ffffff40;
|
||||
background: transparent;
|
||||
transition: background var(--fast), border-color var(--fast);
|
||||
}
|
||||
.pill svg { width: 0.95em; height: 0.95em; }
|
||||
.pill:hover { background: var(--glass); }
|
||||
.follow.on { background: color-mix(in oklab, var(--card-accent) 25%, transparent); border-color: transparent; }
|
||||
.follow.on { background: var(--accent); color: #111; border-color: transparent; }
|
||||
|
||||
.chips { display: flex; gap: 0.4rem; flex-wrap: wrap; justify-content: center; }
|
||||
.chips span {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.1em 0.6em;
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.chips .free { background: #1f7a4d; }
|
||||
.chips .new { background: var(--fg); color: #000; font-weight: 700; }
|
||||
.chips span:first-child { background: color-mix(in oklab, var(--card-accent) 35%, transparent); }
|
||||
.chips span:first-child { background: #252729; }
|
||||
.chips .preview { background: #6a4b16; }
|
||||
.chips .tag { background: transparent; box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--card-accent) 60%, transparent); }
|
||||
.chips .tag { background: transparent; box-shadow: inset 0 0 0 1px var(--line); }
|
||||
|
||||
.desc {
|
||||
margin: 0;
|
||||
@@ -787,7 +791,7 @@ body.no-wave-comments :is(.wave-comments, .wc-bubble), body.zen :is(.wave-commen
|
||||
margin-right: auto;
|
||||
padding: 0.05em 0.6em;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
background: none;
|
||||
color: var(--fg);
|
||||
font-size: inherit;
|
||||
@@ -885,7 +889,7 @@ body.no-wave-comments :is(.wave-comments, .wc-bubble), body.zen :is(.wave-commen
|
||||
bottom: calc(max(1rem, env(safe-area-inset-bottom)) + 3.4rem);
|
||||
transform: translate(-50%, 1rem);
|
||||
padding: 0.6em 1.1em;
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
background: #222e;
|
||||
border: 1px solid #ffffff22;
|
||||
font-size: 0.9rem;
|
||||
@@ -1006,7 +1010,7 @@ body.zen #up-next { display: none; }
|
||||
}
|
||||
.start-welcome a { color: inherit; }
|
||||
/* A guest's account button says what it is for. */
|
||||
.avatar-btn.guest { width: auto; padding: 0 0.9rem; border-radius: 999px; font-weight: 600; font-size: 0.85rem; color: var(--fg); }
|
||||
.avatar-btn.guest { width: auto; padding: 0 0.9rem; border-radius: 5px; font-weight: 600; font-size: 0.85rem; color: var(--fg); }
|
||||
|
||||
/* Radio sheet: a bottom sheet on phones, a centred box elsewhere. */
|
||||
#station {
|
||||
@@ -1140,10 +1144,7 @@ button.primary:disabled { opacity: 0.35; cursor: default; }
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 1rem;
|
||||
background:
|
||||
radial-gradient(60rem 40rem at 20% 10%, #ff4d6d33, transparent 60%),
|
||||
radial-gradient(50rem 40rem at 90% 90%, #4d6dff2e, transparent 60%),
|
||||
#050507;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
@@ -1224,7 +1225,7 @@ button.primary:disabled { opacity: 0.35; cursor: default; }
|
||||
width: 2.6rem;
|
||||
height: 0.3rem;
|
||||
margin: 0.55rem auto 0;
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
background: #fff5;
|
||||
}
|
||||
.content { width: calc(100vw - 2rem); margin-bottom: 6.5rem; gap: 0.6rem; }
|
||||
@@ -1268,7 +1269,7 @@ body.zen .content { gap: 0.3rem; }
|
||||
.bar-end {
|
||||
gap: 0.15rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
@@ -1331,3 +1332,39 @@ body.zen .content { gap: 0.3rem; }
|
||||
.wc-in { animation: none; }
|
||||
.wc-bubble::before { display: none; }
|
||||
}
|
||||
|
||||
/* Monochrome soundsystem surfaces. Keep the controls still around the sculpture. */
|
||||
.logo, .start-logo, .login-box h1 { font-family: var(--font-display); font-weight: 700; letter-spacing: 0.025em; }
|
||||
.title { font-family: var(--font-display); font-weight: 700; line-height: 1.05; letter-spacing: 0.015em; }
|
||||
.time, .counts, .chips, .station-kicker, .up-next-label { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; }
|
||||
#mode-tabs, .bar-end, #up-next { background: var(--panel); border: 1px solid var(--line); backdrop-filter: none; }
|
||||
button { min-height: 2.75rem; }
|
||||
button:hover { border-color: #737578; }
|
||||
button:active { filter: brightness(0.85); }
|
||||
button:disabled { cursor: default; opacity: 0.45; }
|
||||
.icon-btn { min-width: 2.75rem; min-height: 2.75rem; }
|
||||
.chips span { border-radius: 3px; background: #202225; color: var(--fg); border: 1px solid var(--line); }
|
||||
.chips span.free, .chips span.new { background: #252729; color: var(--fg); }
|
||||
.act .ring { background: #191b1d; border: 1px solid var(--line); backdrop-filter: none; }
|
||||
.art { box-shadow: 0 12px 35px #0009, 0 0 0 1px #ffffff24; scale: 1; }
|
||||
/* Fade the canvas spill before the title or navigation, including the caustics. */
|
||||
.liquid-canvas { mask-image: linear-gradient(transparent 2%, #000 18% 72%, transparent 88%); }
|
||||
.card.liquid .art { margin-bottom: 2.8rem; }
|
||||
.sheet, .station-box { box-shadow: 0 20px 80px #000b; }
|
||||
.track-row { padding-block: 0.7rem; }
|
||||
.login-box button[type=submit], .help-box button { color: #111; }
|
||||
.link.danger, #login-error { color: var(--danger); }
|
||||
button.danger:not(.link) { background: var(--danger); color: #111; }
|
||||
#login input { border-radius: var(--radius-s); }
|
||||
@media (max-width: 640px) {
|
||||
#mode-tabs button { padding-inline: 0.7em; }
|
||||
.content { margin-top: 3rem; }
|
||||
.art { width: min(14rem, 29dvh, 58vw); }
|
||||
.card.liquid .art { margin-bottom: 1.8rem; }
|
||||
.actions { gap: 0.5rem; }
|
||||
}
|
||||
@media (max-width: 640px) and (max-height: 700px) {
|
||||
.art { width: min(10rem, 22dvh, 48vw); }
|
||||
.card.liquid .art { margin-bottom: 1.2rem; }
|
||||
.content { gap: 0.25rem; }
|
||||
}
|
||||
Reference in new issue
Block a user