mirror of
https://github.com/narrrl/pl8list-rewind.git
synced 2026-11-04 16:47:00 +01:00
Draw a clean play/pause on the drop and make the app installable
The raised icon was cut into a mesh too coarse for its edges and did not line up with the icon painted over it, so it came out doubled and jagged, and melting one shape into the other went through blobby in-betweens. The icon is now only painted: crisp and white on a dark disc like the page's round buttons, still turning with the drop, and pause and play fade into each other as the new one grows in. Some browsers only offer to install an app with a service worker. A minimal one at /sw.js (served from the root so it covers the whole app) caches nothing and only shows a short offline note instead of the browser's error page. The manifest gets an id.
This commit is contained in:
1 parent
ca23cf5b7c
commit
065c9980dc
6 files changed
+59
-23
No files matched your search
@@ -294,6 +294,11 @@ async fn static_route(Path(path): Path<String>) -> Response {
|
|||||||
static_file(&path)
|
static_file(&path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The service worker, at the root so its scope covers the whole app.
|
||||||
|
async fn service_worker() -> Response {
|
||||||
|
static_file("sw.js")
|
||||||
|
}
|
||||||
|
|
||||||
async fn healthz() -> Response {
|
async fn healthz() -> Response {
|
||||||
(no_store(), Json(json!({ "ok": true }))).into_response()
|
(no_store(), Json(json!({ "ok": true }))).into_response()
|
||||||
}
|
}
|
||||||
@@ -305,6 +310,7 @@ pub fn router(app: Arc<App>) -> Router {
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.route("/static/{*path}", get(static_route))
|
.route("/static/{*path}", get(static_route))
|
||||||
|
.route("/sw.js", get(service_worker))
|
||||||
.route("/healthz", get(healthz))
|
.route("/healthz", get(healthz))
|
||||||
.route("/impressum", get(legal::impressum))
|
.route("/impressum", get(legal::impressum))
|
||||||
.route("/datenschutz", get(legal::datenschutz))
|
.route("/datenschutz", get(legal::datenschutz))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "pl8list ∞",
|
"name": "pl8list ∞",
|
||||||
"short_name": "pl8 ∞",
|
"short_name": "pl8 ∞",
|
||||||
"description": "Endless scroll through pl8list, tuned to what you like.",
|
"description": "Endless scroll through pl8list, tuned to what you like.",
|
||||||
|
"id": "/",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Makes the app installable from the browser (some browsers want a service worker
|
||||||
|
// for that) and shows a short note instead of the browser's error page when the
|
||||||
|
// phone is offline. Nothing is cached: audio, artwork and the feed always come
|
||||||
|
// from the network, and every other request passes straight through.
|
||||||
|
|
||||||
|
self.addEventListener("install", () => self.skipWaiting());
|
||||||
|
self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));
|
||||||
|
|
||||||
|
const OFFLINE = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"><title>pl8list ∞</title></head>
|
||||||
|
<body bgcolor="#000000" text="#f2f2f2"><p>You are offline. pl8list ∞ needs a connection to play.</p></body></html>`;
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (e) => {
|
||||||
|
if (e.request.mode !== "navigate") return;
|
||||||
|
e.respondWith(
|
||||||
|
fetch(e.request).catch(() => new Response(OFFLINE, { headers: { "Content-Type": "text/html; charset=utf-8" } })),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -26,8 +26,8 @@ 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 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
|
const SNARE_ATTACK = 0.03; // s; spikes grow in rather than pop
|
||||||
const SNARE_RELEASE = 0.15; // s
|
const SNARE_RELEASE = 0.15; // s
|
||||||
// The play/pause icon: pausing pops up the bars, which melt into the play triangle;
|
// The play/pause icon: pausing shows the bars, which turn into the play triangle;
|
||||||
// playing melts the triangle back into the bars as they fade.
|
// playing turns the triangle back into the bars as they fade.
|
||||||
const ICON_MELT = 0.35; // s the bars show before turning into the triangle
|
const ICON_MELT = 0.35; // s the bars show before turning into the triangle
|
||||||
const ICON_FADE = 0.3; // s the bars show after playing starts
|
const ICON_FADE = 0.3; // s the bars show after playing starts
|
||||||
const TEXTURE_SIZE = 512;
|
const TEXTURE_SIZE = 512;
|
||||||
|
|||||||
+27
-21
@@ -54,18 +54,18 @@ float snoise(vec3 v) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// The play/pause icon on the front of the drop, as a distance in units of the radius
|
// The play/pause icon on the front of the drop, as distances in units of the radius
|
||||||
// (negative inside). `shape` 0 is the pause bars, 1 the play triangle; between them
|
// (negative inside), with rounded corners like the page's icons.
|
||||||
// one melts into the other.
|
|
||||||
const ICON = /* glsl */ `
|
const ICON = /* glsl */ `
|
||||||
float sdBox(vec2 p, vec2 b) {
|
float sdBox(vec2 p, vec2 b) {
|
||||||
vec2 d = abs(p) - b;
|
vec2 d = abs(p) - b;
|
||||||
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
|
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inigo Quilez's equilateral triangle, MIT licence; turned to point right.
|
// Inigo Quilez's equilateral triangle, MIT licence; turned to point right and
|
||||||
|
// moved right a little, so it looks centred.
|
||||||
float sdPlay(vec2 p, float r) {
|
float sdPlay(vec2 p, float r) {
|
||||||
p = vec2(-p.y, p.x + 0.08);
|
p = vec2(-p.y, p.x - 0.035);
|
||||||
const float k = sqrt(3.0);
|
const float k = sqrt(3.0);
|
||||||
p.x = abs(p.x) - r;
|
p.x = abs(p.x) - r;
|
||||||
p.y = p.y + r / k;
|
p.y = p.y + r / k;
|
||||||
@@ -74,9 +74,12 @@ float sdPlay(vec2 p, float r) {
|
|||||||
return -length(p) * sign(p.y);
|
return -length(p) * sign(p.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
float icon(vec2 p, float shape) {
|
float sdPause(vec2 p) {
|
||||||
float bars = sdBox(vec2(abs(p.x) - 0.15, p.y), vec2(0.07, 0.24));
|
return sdBox(vec2(abs(p.x) - 0.1, p.y), vec2(0.035, 0.15)) - 0.025;
|
||||||
return mix(bars, sdPlay(p, 0.24), shape) - 0.03;
|
}
|
||||||
|
|
||||||
|
float sdPlayIcon(vec2 p) {
|
||||||
|
return sdPlay(p, 0.15) - 0.03;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -100,15 +103,13 @@ uniform float uJelly; // extra wobble while a jump rings out, 0..1
|
|||||||
uniform vec4 uPointer; // direction on the sphere, strength
|
uniform vec4 uPointer; // direction on the sphere, strength
|
||||||
uniform mat3 uRot; // turned by hand, object to view space
|
uniform mat3 uRot; // turned by hand, object to view space
|
||||||
uniform vec3 uFront; // towards the viewer, in object space
|
uniform vec3 uFront; // towards the viewer, in object space
|
||||||
uniform float uIcon; // play/pause, see ICON
|
uniform float uIconA; // how much the play/pause icon shows, see ICON
|
||||||
uniform float uIconA; // how much it shows
|
|
||||||
|
|
||||||
out vec3 vPos; // view space
|
out vec3 vPos; // view space
|
||||||
out vec3 vNormal; // view space
|
out vec3 vNormal; // view space
|
||||||
out vec3 vObj; // the resting sphere; its front projection carries the artwork
|
out vec3 vObj; // the resting sphere; its front projection carries the artwork
|
||||||
|
|
||||||
${NOISE}
|
${NOISE}
|
||||||
${ICON}
|
|
||||||
|
|
||||||
// Every layer keeps a floor, so the drop never goes still and nothing snaps in from
|
// Every layer keeps a floor, so the drop never goes still and nothing snaps in from
|
||||||
// nothing; the music only turns each layer up and down.
|
// nothing; the music only turns each layer up and down.
|
||||||
@@ -132,9 +133,8 @@ float disp(vec3 p) {
|
|||||||
float angle = acos(clamp(dot(p, uFront), -1.0, 1.0));
|
float angle = acos(clamp(dot(p, uFront), -1.0, 1.0));
|
||||||
float wave = exp(-pow((angle - uKickAge * 5.0) * 1.8, 2.0)) * exp(-uKickAge * 2.8) * 0.09 * uShock;
|
float wave = exp(-pow((angle - uKickAge * 5.0) * 1.8, 2.0)) * exp(-uKickAge * 2.8) * 0.09 * uShock;
|
||||||
float dent = -0.18 * uPointer.w * exp(-pow(distance(p, uPointer.xyz) * 2.2, 2.0));
|
float dent = -0.18 * uPointer.w * exp(-pow(distance(p, uPointer.xyz) * 2.2, 2.0));
|
||||||
// The icon stands out of the front like a stamp, and turns with the drop.
|
// Calmer while the icon shows, so it stays readable.
|
||||||
float stamp = (1.0 - smoothstep(-0.02, 0.05, icon(p.xy, uIcon))) * smoothstep(0.3, 0.6, p.z) * uIconA;
|
return clamp((lobes + sway + ripples + spikes + wave) * uGain + dent, -0.3, 0.4) * (1.0 - 0.6 * uIconA);
|
||||||
return clamp((lobes + sway + ripples + spikes + wave) * uGain + dent, -0.3, 0.4) * (1.0 - 0.6 * uIconA) + 0.09 * stamp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vec3 shape(vec3 p) {
|
vec3 shape(vec3 p) {
|
||||||
@@ -173,7 +173,8 @@ uniform float uHue; // rim hue offset, radians
|
|||||||
uniform float uIri; // how much the rim hue drifts
|
uniform float uIri; // how much the rim hue drifts
|
||||||
uniform float uMetal; // sharper, brighter highlights
|
uniform float uMetal; // sharper, brighter highlights
|
||||||
uniform float uHeat; // pulls the rim towards orange
|
uniform float uHeat; // pulls the rim towards orange
|
||||||
uniform float uIcon, uIconA;
|
uniform float uIcon; // 0 pause, 1 play; between them one fades into the other
|
||||||
|
uniform float uIconA;
|
||||||
|
|
||||||
out vec4 outColor;
|
out vec4 outColor;
|
||||||
|
|
||||||
@@ -214,12 +215,17 @@ void main() {
|
|||||||
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), 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))));
|
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) * uMorph;
|
col += env * (0.1 + 0.5 * fres) * uMorph;
|
||||||
// The icon, crisp per pixel on top of the stamp, with a soft shadow round it.
|
// The icon sits on a dark glass disc, like the page's round buttons, painted onto
|
||||||
float d = icon(vObj.xy, uIcon);
|
// the surface so it turns with the drop. Each shape grows in as it fades in.
|
||||||
float aa = fwidth(d);
|
vec2 ip = vObj.xy;
|
||||||
float front = smoothstep(0.3, 0.6, vObj.z) * uIconA;
|
float front = smoothstep(0.35, 0.6, vObj.z) * uIconA;
|
||||||
col *= 1.0 - 0.5 * front * (1.0 - smoothstep(0.0, 0.12, d));
|
float disc = length(ip) - 0.36;
|
||||||
col = mix(col, vec3(0.96) + 0.4 * spec, front * (1.0 - smoothstep(-aa, aa, d)));
|
col = mix(col, col * 0.3, front * (1.0 - smoothstep(-0.01, 0.01, disc)));
|
||||||
|
float playIn = smoothstep(0.0, 1.0, uIcon);
|
||||||
|
float dPause = sdPause(ip / mix(1.0, 0.7, playIn)), dPlay = sdPlayIcon(ip / mix(0.7, 1.0, playIn));
|
||||||
|
float ink = max((1.0 - smoothstep(-fwidth(dPause), fwidth(dPause), dPause)) * (1.0 - playIn),
|
||||||
|
(1.0 - smoothstep(-fwidth(dPlay), fwidth(dPlay), dPlay)) * playIn);
|
||||||
|
col = mix(col, vec3(0.96), front * ink);
|
||||||
// The very edge fades out, so the drop sits in the page instead of on it.
|
// The very edge fades out, so the drop sits in the page instead of on it.
|
||||||
float a = 1.0 - 0.9 * smoothstep(0.8, 1.0, fres) * uMorph;
|
float a = 1.0 - 0.9 * smoothstep(0.8, 1.0, fres) * uMorph;
|
||||||
outColor = vec4(col * a, a);
|
outColor = vec4(col * a, a);
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ const radioEnd = $("radio-end");
|
|||||||
|
|
||||||
const player = new Player();
|
const player = new Player();
|
||||||
const visual = new Visualizer(player, isTouch);
|
const visual = new Visualizer(player, isTouch);
|
||||||
|
|
||||||
|
// Installable as an app; see public/sw.js. The dev server has no /sw.js.
|
||||||
|
if ("serviceWorker" in navigator && import.meta.env.PROD) {
|
||||||
|
navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch((err) => console.warn("no service worker:", err));
|
||||||
|
}
|
||||||
const cards: Card[] = [];
|
const cards: Card[] = [];
|
||||||
let active = -1;
|
let active = -1;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|||||||
Reference in new issue
Block a user