mini player + dark glass: eight shapes run by the hidden main as the one brain, dissolving covers and accents, drag from anywhere, and the space between the panes turned to real acrylic - with the webview flags that keep rest frames honest
This commit is contained in:
1 parent
9b84b30aef
commit
dc7ea16b22
33 files changed
+2432
-5
No files matched your search
@@ -0,0 +1,150 @@
|
||||
<script>
|
||||
// The playbar's scrubber lane, miniaturised: same waveLevels shaping,
|
||||
// same BAR_PITCH/BAR_W geometry, same colors (unplayed --text-muted,
|
||||
// played the accent, boundary bar cross-faded by its fractional fill).
|
||||
// The playbar's hover marker and loading motions stay where they are —
|
||||
// this lane is for a window a tenth the size.
|
||||
import { BAR_PITCH, BAR_W, waveLevels } from "../../lib/waveshape.js";
|
||||
import { mini, seekTo, fmt } from "../mini.svelte.js";
|
||||
|
||||
let { slim = false, showTimes = true } = $props();
|
||||
|
||||
let canvas = $state(null);
|
||||
let wrap = $state(null);
|
||||
let levels = [];
|
||||
let levelsFor = { wf: null, count: 0 };
|
||||
|
||||
const UNPLAYED = "#7a7a7a"; // --text-muted, the lanes' shared grey
|
||||
const MUTED_RGB = [122, 122, 122];
|
||||
|
||||
function accentRgb() {
|
||||
// --accent is a registered @property (<color>) in global.css, so the
|
||||
// COMPUTED value comes back as "rgb(r, g, b)" — not the authored hex.
|
||||
// Parse both forms; fall back to the muted grey, never to black.
|
||||
const v = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim();
|
||||
if (v[0] === "#") {
|
||||
const n = parseInt(v.slice(1), 16);
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||||
}
|
||||
const m = v.match(/[\d.]+/g);
|
||||
return m && m.length >= 3 ? [+m[0], +m[1], +m[2]] : MUTED_RGB;
|
||||
}
|
||||
|
||||
export function draw() {
|
||||
if (!canvas || !wrap) return;
|
||||
const w = wrap.clientWidth;
|
||||
const h = wrap.clientHeight;
|
||||
if (!w) return;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
if (canvas.width !== Math.round(w * dpr)) {
|
||||
canvas.width = Math.round(w * dpr);
|
||||
canvas.height = Math.round(h * dpr);
|
||||
}
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
const count = Math.max(8, Math.floor((w + (BAR_PITCH - BAR_W)) / BAR_PITCH));
|
||||
if (levelsFor.wf !== mini.waveform || levelsFor.count !== count) {
|
||||
levels = waveLevels(mini.waveform, count);
|
||||
levelsFor = { wf: mini.waveform, count };
|
||||
}
|
||||
const mid = h / 2;
|
||||
const dur = mini.duration || 1;
|
||||
const exact = (mini.position / dur) * count;
|
||||
const full = Math.floor(exact);
|
||||
const acc = accentRgb();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const amp = levels.length ? levels[i] : 0.09; // resting dots
|
||||
const bh = Math.max(2, amp * h);
|
||||
let color;
|
||||
if (i < full) color = `rgb(${acc.join(",")})`;
|
||||
else if (i === full) {
|
||||
const f = exact - full;
|
||||
const m = MUTED_RGB.map((c, k) => Math.round(c + (acc[k] - c) * f));
|
||||
color = `rgb(${m.join(",")})`;
|
||||
} else color = UNPLAYED;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(i * BAR_PITCH, mid - bh / 2, BAR_W, bh, 1);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// Redraw on every mirrored change + rAF for the smoothed position.
|
||||
$effect(() => {
|
||||
void mini.waveform;
|
||||
void mini.duration;
|
||||
let alive = true;
|
||||
const loop = () => {
|
||||
if (!alive) return;
|
||||
draw();
|
||||
requestAnimationFrame(loop);
|
||||
};
|
||||
loop();
|
||||
return () => (alive = false);
|
||||
});
|
||||
|
||||
function seekAt(e) {
|
||||
const r = wrap.getBoundingClientRect();
|
||||
const f = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
|
||||
const t = f * (mini.duration || 0);
|
||||
mini.position = t; // optimistic, the tick confirms
|
||||
seekTo(t);
|
||||
}
|
||||
function onDown(e) {
|
||||
seekAt(e);
|
||||
const mv = (ev) => seekAt(ev);
|
||||
const up = () => {
|
||||
window.removeEventListener("pointermove", mv);
|
||||
window.removeEventListener("pointerup", up);
|
||||
};
|
||||
window.addEventListener("pointermove", mv);
|
||||
window.addEventListener("pointerup", up);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="lane" class:slim>
|
||||
{#if showTimes}<span class="time">{fmt(mini.position)}</span>{/if}
|
||||
<div class="wave-wrap" bind:this={wrap} onpointerdown={onDown}>
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
</div>
|
||||
{#if showTimes}<span class="time">{fmt(mini.duration)}</span>{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lane {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
/* The playbar's own numbers: 11px secondary, min-width 32, centred. */
|
||||
.time {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
min-width: 32px;
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
flex: none;
|
||||
}
|
||||
.wave-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.lane.slim .wave-wrap {
|
||||
height: 16px;
|
||||
}
|
||||
.lane.slim .time {
|
||||
font-size: 9.5px;
|
||||
min-width: 26px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user