diff --git a/Dockerfile b/Dockerfile
index 47753a7..7ae674d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,10 @@
# syntax=docker/dockerfile:1
-FROM node:24-alpine AS web
+FROM oven/bun:1.4.4-alpine AS web
WORKDIR /src/web
-COPY web/package.json web/package-lock.json ./
-RUN npm ci
+COPY web/package.json web/bun.lock ./
+RUN bun install --frozen-lockfile
COPY web ./
-RUN npm run build
+RUN bun run build
FROM rust:1-alpine AS build
RUN apk add --no-cache musl-dev cmake make perl clang ca-certificates
diff --git a/README.md b/README.md
index 7ee0894..ffe87cd 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,8 @@ Not affiliated with pl8list.
## Screenshots
+The [monochrome redesign previews](docs/redesign/README.md) show the current visual treatment using a synthetic track. The live-playback captures below predate that pass.
+

@@ -62,17 +64,17 @@ Not affiliated with pl8list.
docker compose up -d --build # http://127.0.0.1:8790
```
-or without Docker (Rust 1.85+, Node 22+):
+or without Docker (Rust 1.85+, Bun 1.4.4+):
```sh
-cd web && npm ci && npm run build # front end into web/dist
+cd web && bun install --frozen-lockfile && bun run build # front end into web/dist
cd ../backend && cargo run --release # embeds web/dist at build time
cargo test # unit tests + API tests against a fake pl8list
```
-For front-end work run the server, then `npm run dev` in `web/`: Vite serves the page with
+For front-end work run the server, then `bun run dev` in `web/`: Vite serves the page with
hot reload on http://127.0.0.1:5173/static/ and passes API calls to the server (`BACKEND=` to point it
-elsewhere). Debug builds of the server read `web/dist` from disk, so `npm run build` is enough
+elsewhere). Debug builds of the server read `web/dist` from disk, so `bun run build` is enough
to see changes on :8790 too.
The server is the Rust crate in `backend/`, the front end is TypeScript in `web/` (Vite, no
diff --git a/backend/build.rs b/backend/build.rs
index 71a8ed8..f2d54f7 100644
--- a/backend/build.rs
+++ b/backend/build.rs
@@ -1,4 +1,4 @@
-// The front end is built separately (`cd web && npm ci && npm run build`) and
+// The front end is built separately (`cd web && bun install --frozen-lockfile && bun run build`) and
// embedded from web/dist. Create the folder if it is missing so the backend still
// compiles (and its tests run) without Node; the pages are then empty.
fn main() {
@@ -6,6 +6,6 @@ fn main() {
println!("cargo:rerun-if-changed=../web/dist");
if !dist.join("index.html").exists() {
std::fs::create_dir_all(dist).expect("create ../web/dist");
- println!("cargo:warning=web/dist has no index.html; run `npm run build` in web/ to include the front end");
+ println!("cargo:warning=web/dist has no index.html; run `bun run build` in web/ to include the front end");
}
}
diff --git a/backend/src/http/mod.rs b/backend/src/http/mod.rs
index cdbd7bf..5b7377a 100644
--- a/backend/src/http/mod.rs
+++ b/backend/src/http/mod.rs
@@ -286,7 +286,7 @@ pub type Peer = ConnectInfo;
// Static files ------------------------------------------------------------
-/// The built front end (`web/`, `npm run build`). Hashed asset names, so they
+/// The built front end (`web/`, `bun run build`). Hashed asset names, so they
/// can be cached for good; index.html and fixed-name files are revalidated.
#[derive(Embed)]
#[folder = "../web/dist/"]
diff --git a/docs/redesign/README.md b/docs/redesign/README.md
new file mode 100644
index 0000000..ba5b963
--- /dev/null
+++ b/docs/redesign/README.md
@@ -0,0 +1,9 @@
+# Monochrome visual pass
+
+These screenshots use a synthetic track and deterministic audio features in Chromium, not live pl8list playback. They demonstrate desktop (1440×1000), mobile (390×844), short mobile (390×667), settings and login styling.
+
+Validated: Bun frozen-lockfile installation, TypeScript check, production build, local font loading, WebGL shader creation/rendering, opening and dismissing settings, and a sustained synthetic drop whose one-shot envelope decays without retriggering. Chromium reported no page errors.
+
+Still requires real-device validation: real-track onset timing, touch gestures, GPU context restoration, reduced-motion transitions, audio fallback on iOS, and comparative frame-time/battery measurements. Docker's Bun build stage was updated but the full container image was not built.
+
+Run the frontend with `cd web && bun install --frozen-lockfile && bun run dev`. Build with `bun run build`.
diff --git a/docs/redesign/desktop.png b/docs/redesign/desktop.png
new file mode 100644
index 0000000..be9c954
Binary files /dev/null and b/docs/redesign/desktop.png differ
diff --git a/docs/redesign/login.png b/docs/redesign/login.png
new file mode 100644
index 0000000..b605bab
Binary files /dev/null and b/docs/redesign/login.png differ
diff --git a/docs/redesign/mobile.png b/docs/redesign/mobile.png
new file mode 100644
index 0000000..1c3b162
Binary files /dev/null and b/docs/redesign/mobile.png differ
diff --git a/docs/redesign/settings.png b/docs/redesign/settings.png
new file mode 100644
index 0000000..d1d51d6
Binary files /dev/null and b/docs/redesign/settings.png differ
diff --git a/docs/redesign/short-mobile.png b/docs/redesign/short-mobile.png
new file mode 100644
index 0000000..61af483
Binary files /dev/null and b/docs/redesign/short-mobile.png differ
diff --git a/web/bun.lock b/web/bun.lock
new file mode 100644
index 0000000..63a9a42
--- /dev/null
+++ b/web/bun.lock
@@ -0,0 +1,141 @@
+{
+ "lockfileVersion": 2,
+ "configVersion": 0,
+ "workspaces": {
+ "": {
+ "name": "pl8inf-web",
+ "dependencies": {
+ "hls.js": "1.7.4",
+ },
+ "devDependencies": {
+ "typescript": "7.0.2",
+ "vite": "8.3.5",
+ },
+ },
+ },
+ "packages": {
+ "@oxc-project/types": ["@oxc-project/types@0.156.0", "", {}, "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw=="],
+
+ "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.16", "", { "os": "android", "cpu": "arm" }, "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA=="],
+
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.16", "", { "os": "android", "cpu": "arm64" }, "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg=="],
+
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ=="],
+
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ=="],
+
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q=="],
+
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.16", "", { "os": "linux", "cpu": "arm" }, "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA=="],
+
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.16", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w=="],
+
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.16", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg=="],
+
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.16", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA=="],
+
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.16", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg=="],
+
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.16", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w=="],
+
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.16", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA=="],
+
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.16", "", { "os": "none", "cpu": "arm64" }, "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ=="],
+
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw=="],
+
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.16", "", { "os": "win32", "cpu": "x64" }, "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
+
+ "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
+
+ "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
+
+ "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
+
+ "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
+
+ "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
+
+ "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
+
+ "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
+
+ "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
+
+ "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
+
+ "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
+
+ "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
+
+ "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
+
+ "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
+
+ "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
+
+ "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
+
+ "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
+
+ "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
+
+ "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
+
+ "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
+
+ "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin", "hasInstallScript": true }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "hls.js": ["hls.js@1.7.4", "", {}, "sha512-3GlxGdPvT2YHvQYOt+yOzT/DEior/qJVNIylHA77kgeoBTdQQqi70GwefNEUmHQeNGyjC8U62+bruqW/Tpm+tg=="],
+
+ "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
+
+ "nanoid": ["nanoid@3.3.20", "", { "bin": "bin/nanoid.cjs" }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
+
+ "postcss": ["postcss@8.5.32", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g=="],
+
+ "rolldown": ["rolldown@1.2.16", "", { "dependencies": { "@oxc-project/types": "=0.156.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.16", "@rolldown/binding-android-arm64": "1.2.16", "@rolldown/binding-darwin-arm64": "1.2.16", "@rolldown/binding-darwin-x64": "1.2.16", "@rolldown/binding-freebsd-x64": "1.2.16", "@rolldown/binding-linux-arm-gnueabihf": "1.2.16", "@rolldown/binding-linux-arm64-gnu": "1.2.16", "@rolldown/binding-linux-arm64-musl": "1.2.16", "@rolldown/binding-linux-ppc64-gnu": "1.2.16", "@rolldown/binding-linux-s390x-gnu": "1.2.16", "@rolldown/binding-linux-x64-gnu": "1.2.16", "@rolldown/binding-linux-x64-musl": "1.2.16", "@rolldown/binding-openharmony-arm64": "1.2.16", "@rolldown/binding-win32-arm64-msvc": "1.2.16", "@rolldown/binding-win32-x64-msvc": "1.2.16" }, "bin": "bin/cli.mjs" }, "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw=="],
+
+ "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="],
+
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+
+ "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
+
+ "vite": ["vite@8.3.5", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.30", "rolldown": "~1.2.14", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ=="],
+ }
+}
diff --git a/web/index.html b/web/index.html
index 8768dc9..a0ffdea 100644
--- a/web/index.html
+++ b/web/index.html
@@ -5,7 +5,7 @@
pl8list rewind
-
+
diff --git a/web/package-lock.json b/web/package-lock.json
deleted file mode 100644
index 710ff40..0000000
--- a/web/package-lock.json
+++ /dev/null
@@ -1,1211 +0,0 @@
-{
- "name": "pl8inf-web",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "pl8inf-web",
- "dependencies": {
- "hls.js": "1.7.4"
- },
- "devDependencies": {
- "typescript": "7.0.2",
- "vite": "8.3.5"
- }
- },
- "node_modules/@oxc-project/types": {
- "version": "0.156.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz",
- "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/oxc-project"
- }
- },
- "node_modules/@rolldown/binding-android-arm-eabi": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz",
- "integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz",
- "integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz",
- "integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz",
- "integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz",
- "integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz",
- "integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz",
- "integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz",
- "integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz",
- "integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz",
- "integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz",
- "integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz",
- "integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz",
- "integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz",
- "integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz",
- "integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
- "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@typescript/typescript-aix-ppc64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
- "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-darwin-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
- "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-darwin-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
- "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-freebsd-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
- "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-freebsd-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
- "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-arm": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
- "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
- "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-loong64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
- "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-mips64el": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
- "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-ppc64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
- "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-riscv64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
- "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-s390x": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
- "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
- "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-netbsd-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
- "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-netbsd-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
- "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-openbsd-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
- "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-openbsd-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
- "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-sunos-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
- "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-win32-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
- "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-win32-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
- "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/hls.js": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.4.tgz",
- "integrity": "sha512-3GlxGdPvT2YHvQYOt+yOzT/DEior/qJVNIylHA77kgeoBTdQQqi70GwefNEUmHQeNGyjC8U62+bruqW/Tpm+tg==",
- "license": "Apache-2.0"
- },
- "node_modules/lightningcss": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
- "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
- "dev": true,
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.33.0",
- "lightningcss-darwin-arm64": "1.33.0",
- "lightningcss-darwin-x64": "1.33.0",
- "lightningcss-freebsd-x64": "1.33.0",
- "lightningcss-linux-arm-gnueabihf": "1.33.0",
- "lightningcss-linux-arm64-gnu": "1.33.0",
- "lightningcss-linux-arm64-musl": "1.33.0",
- "lightningcss-linux-x64-gnu": "1.33.0",
- "lightningcss-linux-x64-musl": "1.33.0",
- "lightningcss-win32-arm64-msvc": "1.33.0",
- "lightningcss-win32-x64-msvc": "1.33.0"
- }
- },
- "node_modules/lightningcss-android-arm64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
- "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
- "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
- "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
- "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
- "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
- "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
- "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
- "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
- "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
- "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
- "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/nanoid": {
- "version": "3.3.20",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
- "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
- "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.32",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
- "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.20",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.2"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/rolldown": {
- "version": "1.2.16",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz",
- "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@oxc-project/types": "=0.156.0",
- "@rolldown/pluginutils": "^1.0.0"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm-eabi": "1.2.16",
- "@rolldown/binding-android-arm64": "1.2.16",
- "@rolldown/binding-darwin-arm64": "1.2.16",
- "@rolldown/binding-darwin-x64": "1.2.16",
- "@rolldown/binding-freebsd-x64": "1.2.16",
- "@rolldown/binding-linux-arm-gnueabihf": "1.2.16",
- "@rolldown/binding-linux-arm64-gnu": "1.2.16",
- "@rolldown/binding-linux-arm64-musl": "1.2.16",
- "@rolldown/binding-linux-ppc64-gnu": "1.2.16",
- "@rolldown/binding-linux-s390x-gnu": "1.2.16",
- "@rolldown/binding-linux-x64-gnu": "1.2.16",
- "@rolldown/binding-linux-x64-musl": "1.2.16",
- "@rolldown/binding-openharmony-arm64": "1.2.16",
- "@rolldown/binding-win32-arm64-msvc": "1.2.16",
- "@rolldown/binding-win32-x64-msvc": "1.2.16"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
- "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/tinyglobby": {
- "version": "0.2.17",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/typescript": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
- "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc"
- },
- "engines": {
- "node": ">=16.20.0"
- },
- "optionalDependencies": {
- "@typescript/typescript-aix-ppc64": "7.0.2",
- "@typescript/typescript-darwin-arm64": "7.0.2",
- "@typescript/typescript-darwin-x64": "7.0.2",
- "@typescript/typescript-freebsd-arm64": "7.0.2",
- "@typescript/typescript-freebsd-x64": "7.0.2",
- "@typescript/typescript-linux-arm": "7.0.2",
- "@typescript/typescript-linux-arm64": "7.0.2",
- "@typescript/typescript-linux-loong64": "7.0.2",
- "@typescript/typescript-linux-mips64el": "7.0.2",
- "@typescript/typescript-linux-ppc64": "7.0.2",
- "@typescript/typescript-linux-riscv64": "7.0.2",
- "@typescript/typescript-linux-s390x": "7.0.2",
- "@typescript/typescript-linux-x64": "7.0.2",
- "@typescript/typescript-netbsd-arm64": "7.0.2",
- "@typescript/typescript-netbsd-x64": "7.0.2",
- "@typescript/typescript-openbsd-arm64": "7.0.2",
- "@typescript/typescript-openbsd-x64": "7.0.2",
- "@typescript/typescript-sunos-x64": "7.0.2",
- "@typescript/typescript-win32-arm64": "7.0.2",
- "@typescript/typescript-win32-x64": "7.0.2"
- }
- },
- "node_modules/vite": {
- "version": "8.3.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
- "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "lightningcss": "^1.33.0",
- "picomatch": "^4.0.7",
- "postcss": "^8.5.30",
- "rolldown": "~1.2.14",
- "tinyglobby": "^0.2.17"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.7.1 || ^0.8.0",
- "esbuild": "^0.27.0 || ^0.28.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "@vitejs/devtools": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- }
- }
-}
diff --git a/web/package.json b/web/package.json
index c9937d9..19858bd 100644
--- a/web/package.json
+++ b/web/package.json
@@ -3,9 +3,9 @@
"private": true,
"type": "module",
"scripts": {
- "dev": "vite",
- "build": "tsc --noEmit && vite build",
- "check": "tsc --noEmit"
+ "dev": "bun --bun vite",
+ "build": "bun run check && bun --bun vite build",
+ "check": "bun --bun tsc --noEmit"
},
"dependencies": {
"hls.js": "1.7.4"
@@ -13,5 +13,6 @@
"devDependencies": {
"typescript": "7.0.2",
"vite": "8.3.5"
- }
+ },
+ "packageManager": "bun@1.4.4"
}
diff --git a/web/public/fonts/Barlow-Regular.ttf b/web/public/fonts/Barlow-Regular.ttf
new file mode 100644
index 0000000..bb3faec
Binary files /dev/null and b/web/public/fonts/Barlow-Regular.ttf differ
diff --git a/web/public/fonts/BarlowCondensed-Bold.ttf b/web/public/fonts/BarlowCondensed-Bold.ttf
new file mode 100644
index 0000000..9e480f4
Binary files /dev/null and b/web/public/fonts/BarlowCondensed-Bold.ttf differ
diff --git a/web/public/fonts/OFL-Barlow.txt b/web/public/fonts/OFL-Barlow.txt
new file mode 100644
index 0000000..2f22ba6
--- /dev/null
+++ b/web/public/fonts/OFL-Barlow.txt
@@ -0,0 +1,93 @@
+Copyright 2017 The Barlow Project Authors (https://github.com/jpt/barlow)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/web/public/fonts/OFL-BarlowCondensed.txt b/web/public/fonts/OFL-BarlowCondensed.txt
new file mode 100644
index 0000000..2f22ba6
--- /dev/null
+++ b/web/public/fonts/OFL-BarlowCondensed.txt
@@ -0,0 +1,93 @@
+Copyright 2017 The Barlow Project Authors (https://github.com/jpt/barlow)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/web/public/icons/apple-touch-icon.png b/web/public/icons/apple-touch-icon.png
index 674be3c..c228f05 100644
Binary files a/web/public/icons/apple-touch-icon.png and b/web/public/icons/apple-touch-icon.png differ
diff --git a/web/public/icons/icon-192.png b/web/public/icons/icon-192.png
index d54949a..008963c 100644
Binary files a/web/public/icons/icon-192.png and b/web/public/icons/icon-192.png differ
diff --git a/web/public/icons/icon-512.png b/web/public/icons/icon-512.png
index ecd952a..8d62c7a 100644
Binary files a/web/public/icons/icon-512.png and b/web/public/icons/icon-512.png differ
diff --git a/web/public/icons/icon-maskable-512.png b/web/public/icons/icon-maskable-512.png
index fbdc2a5..b01ab98 100644
Binary files a/web/public/icons/icon-maskable-512.png and b/web/public/icons/icon-maskable-512.png differ
diff --git a/web/public/icons/icon.svg b/web/public/icons/icon.svg
index b8bd70b..fa38492 100644
--- a/web/public/icons/icon.svg
+++ b/web/public/icons/icon.svg
@@ -1,7 +1,7 @@
diff --git a/web/public/legal.css b/web/public/legal.css
index 52d9a35..85e4a14 100644
--- a/web/public/legal.css
+++ b/web/public/legal.css
@@ -1,6 +1,6 @@
/* Impressum and privacy pages. Plain CSS outside the app bundle, so the server's
templates can link it by a fixed name. */
-:root { color-scheme: dark; --fg: #f2f2f2; --muted: #9a9a9a; --accent: #ff4d6d; }
+:root { color-scheme: dark; --fg: #f2f2f2; --muted: #9a9a9a; --accent: #e9e8e3; }
* { box-sizing: border-box; }
body {
margin: 0;
diff --git a/web/public/manifest.webmanifest b/web/public/manifest.webmanifest
index 11d84d5..b7c0bfb 100644
--- a/web/public/manifest.webmanifest
+++ b/web/public/manifest.webmanifest
@@ -7,8 +7,8 @@
"scope": "/",
"display": "standalone",
"orientation": "portrait",
- "background_color": "#000000",
- "theme_color": "#000000",
+ "background_color": "#090a0b",
+ "theme_color": "#090a0b",
"icons": [
{ "src": "/static/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/static/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
diff --git a/web/src/blob/blob.ts b/web/src/blob/blob.ts
index 1733054..01a834a 100644
--- a/web/src/blob/blob.ts
+++ b/web/src/blob/blob.ts
@@ -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);
diff --git a/web/src/blob/caustics.ts b/web/src/blob/caustics.ts
index 2ceca9b..700e069 100644
--- a/web/src/blob/caustics.ts
+++ b/web/src/blob/caustics.ts
@@ -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);
diff --git a/web/src/blob/ferro.ts b/web/src/blob/ferro.ts
index 511d3cc..36b776b 100644
--- a/web/src/blob/ferro.ts
+++ b/web/src/blob/ferro.ts
@@ -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]);
}
diff --git a/web/src/blob/shaders.ts b/web/src/blob/shaders.ts
index 4186a3c..a08c1c0 100644
--- a/web/src/blob/shaders.ts
+++ b/web/src/blob/shaders.ts
@@ -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
diff --git a/web/src/blob/style.ts b/web/src/blob/style.ts
index 02c5d21..a4f9bc6 100644
--- a/web/src/blob/style.ts
+++ b/web/src/blob/style.ts
@@ -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 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;
}
diff --git a/web/src/cards.ts b/web/src/cards.ts
index ebb5dd7..60d562e 100644
--- a/web/src/cards.ts
+++ b/web/src/cards.ts
@@ -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);
diff --git a/web/src/color.ts b/web/src/color.ts
index 0ee48a3..579ac05 100644
--- a/web/src/color.ts
+++ b/web/src/color.ts
@@ -50,8 +50,8 @@ async function compute(src: string): Promise {
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.
diff --git a/web/src/main.ts b/web/src/main.ts
index e8cd898..ef1ff9a 100644
--- a/web/src/main.ts
+++ b/web/src/main.ts
@@ -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;
diff --git a/web/src/style.css b/web/src/style.css
index ff71824..3996e43 100644
--- a/web/src/style.css
+++ b/web/src/style.css
@@ -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; }
+}