mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-08-26 14:27:44 +10:00
@@ -9,6 +9,7 @@
|
||||
"main": "./src/index.ts",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@kawarp/core": "*"
|
||||
"@kawarp/core": "*",
|
||||
"ogl": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,15 @@ export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", {
|
||||
backdropScale: 100,
|
||||
// Readability on bright covers <3
|
||||
backdropDarken: 80,
|
||||
// Oil's own controls
|
||||
backdropOilOpacity: 75,
|
||||
backdropOilWarp: 75,
|
||||
backdropOilSpeed: 175,
|
||||
backdropOilBrightness: 15,
|
||||
backdropOilContrast: 95,
|
||||
backdropOilSaturation: 125,
|
||||
backdropOilShimmerSpeed: 50,
|
||||
backdropOilFollowCursor: false,
|
||||
});
|
||||
|
||||
export const Settings = () => {
|
||||
@@ -113,6 +122,30 @@ export const Settings = () => {
|
||||
const [backdropDarken, setBackdropDarken] = React.useState(
|
||||
settings.backdropDarken,
|
||||
);
|
||||
const [backdropOilOpacity, setBackdropOilOpacity] = React.useState(
|
||||
settings.backdropOilOpacity,
|
||||
);
|
||||
const [backdropOilWarp, setBackdropOilWarp] = React.useState(
|
||||
settings.backdropOilWarp,
|
||||
);
|
||||
const [backdropOilSpeed, setBackdropOilSpeed] = React.useState(
|
||||
settings.backdropOilSpeed,
|
||||
);
|
||||
const [backdropOilBrightness, setBackdropOilBrightness] = React.useState(
|
||||
settings.backdropOilBrightness,
|
||||
);
|
||||
const [backdropOilContrast, setBackdropOilContrast] = React.useState(
|
||||
settings.backdropOilContrast,
|
||||
);
|
||||
const [backdropOilSaturation, setBackdropOilSaturation] = React.useState(
|
||||
settings.backdropOilSaturation,
|
||||
);
|
||||
const [backdropOilShimmerSpeed, setBackdropOilShimmerSpeed] = React.useState(
|
||||
settings.backdropOilShimmerSpeed,
|
||||
);
|
||||
const [backdropOilFollowCursor, setBackdropOilFollowCursor] = React.useState(
|
||||
settings.backdropOilFollowCursor,
|
||||
);
|
||||
const [floatingPlayerBar, setFloatingPlayerBar] = React.useState(
|
||||
settings.floatingPlayerBar,
|
||||
);
|
||||
@@ -198,6 +231,9 @@ export const Settings = () => {
|
||||
window.updateRadiantLyricsBackdrop?.();
|
||||
};
|
||||
|
||||
// Oil is a separate renderer (kawarp is exclusive)
|
||||
const isOil = backdropStyle === 2;
|
||||
|
||||
// Derive props and override onChange to accept a broader first param type
|
||||
type BaseSwitchProps = React.ComponentProps<typeof LunaSwitchSetting>;
|
||||
type AnySwitchProps = Omit<BaseSwitchProps, "onChange"> & {
|
||||
@@ -917,9 +953,9 @@ export const Settings = () => {
|
||||
<>
|
||||
<LunaNumberSetting
|
||||
title="Backdrop Style" // This feature is the result of a bug caused by my ADHD <3
|
||||
desc="0 = Fluid, 1 = Aurora"
|
||||
desc="Water | Oil | Aurora"
|
||||
min={0}
|
||||
max={1}
|
||||
max={3}
|
||||
step={1}
|
||||
value={backdropStyle}
|
||||
onNumber={(value: number) => {
|
||||
@@ -948,123 +984,258 @@ export const Settings = () => {
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Backdrop Opacity"
|
||||
desc="How strongly the backdrop shows through (0-100% default = 75)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropOpacity}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropOpacity = value;
|
||||
setBackdropOpacity(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Warp Intensity"
|
||||
desc="Strength of the fluidity effect (0-100% default = 100)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropWarp}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropWarp = value;
|
||||
setBackdropWarp(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Blur Passes"
|
||||
desc="Kawase blur passes, higher is softer but costs more GPU (1-40 default = 5)"
|
||||
min={1}
|
||||
max={40}
|
||||
step={1}
|
||||
value={backdropBlurPasses}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropBlurPasses = value;
|
||||
setBackdropBlurPasses(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Animation Speed"
|
||||
desc="How fast the backdrop flows (0-500% default = 175)"
|
||||
min={0}
|
||||
max={500}
|
||||
step={5}
|
||||
value={backdropSpeed}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropSpeed = value;
|
||||
setBackdropSpeed(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Contrast"
|
||||
desc="Contrast of the backdrop (0-300%, 100 = stock default = 125)"
|
||||
min={0}
|
||||
max={300}
|
||||
step={5}
|
||||
value={backdropContrast}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropContrast = value;
|
||||
setBackdropContrast(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Auto Darken Bright Covers"
|
||||
desc="Prevents bright covers from making text unreadable (0-100% 0 = Off default = 80)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropDarken}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropDarken = value;
|
||||
setBackdropDarken(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Saturation"
|
||||
desc="Colour intensity of the backdrop (0-400% default = 125)"
|
||||
min={0}
|
||||
max={400}
|
||||
step={5}
|
||||
value={backdropSaturation}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropSaturation = value;
|
||||
setBackdropSaturation(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Backdrop Scale"
|
||||
desc="Zoom level of the effect (10-400% default = 100)"
|
||||
min={10}
|
||||
max={400}
|
||||
step={5}
|
||||
value={backdropScale}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropScale = value;
|
||||
setBackdropScale(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Dithering"
|
||||
desc="Breaks up color banding in smooth gradients (0-100 default = 15)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropDithering}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropDithering = value;
|
||||
setBackdropDithering(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
{!isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Backdrop Opacity"
|
||||
desc="How strongly the backdrop shows through (0-100% default = 75)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropOpacity}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropOpacity = value;
|
||||
setBackdropOpacity(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Warp Intensity"
|
||||
desc="Strength of the fluidity effect (0-100% default = 100)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropWarp}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropWarp = value;
|
||||
setBackdropWarp(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Blur Passes"
|
||||
desc="Kawase blur passes, higher is softer but costs more GPU (1-40 default = 5)"
|
||||
min={1}
|
||||
max={40}
|
||||
step={1}
|
||||
value={backdropBlurPasses}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropBlurPasses = value;
|
||||
setBackdropBlurPasses(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Animation Speed"
|
||||
desc="How fast the backdrop flows (0-500% default = 175)"
|
||||
min={0}
|
||||
max={500}
|
||||
step={5}
|
||||
value={backdropSpeed}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropSpeed = value;
|
||||
setBackdropSpeed(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Contrast"
|
||||
desc="Contrast of the backdrop (0-300%, 100 = stock default = 125)"
|
||||
min={0}
|
||||
max={300}
|
||||
step={5}
|
||||
value={backdropContrast}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropContrast = value;
|
||||
setBackdropContrast(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Auto Darken Bright Covers"
|
||||
desc="Prevents bright covers from making text unreadable (0-100% 0 = Off default = 80)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropDarken}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropDarken = value;
|
||||
setBackdropDarken(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Backdrop Opacity"
|
||||
desc="How strongly the backdrop shows through (0-100% default = 75)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropOilOpacity}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropOilOpacity = value;
|
||||
setBackdropOilOpacity(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Warp Intensity"
|
||||
desc="Strength of the fluidity effect (0-100% default = 75)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropOilWarp}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropOilWarp = value;
|
||||
setBackdropOilWarp(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Animation Speed"
|
||||
desc="How fast the backdrop flows (0-500% default = 175)"
|
||||
min={0}
|
||||
max={500}
|
||||
step={5}
|
||||
value={backdropOilSpeed}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropOilSpeed = value;
|
||||
setBackdropOilSpeed(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Brightness"
|
||||
desc="How bright the waves render (0-100, default: 15)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropOilBrightness}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropOilBrightness = value;
|
||||
setBackdropOilBrightness(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Contrast"
|
||||
desc="Contrast of the backdrop (0-300%, 100 = unchanged)"
|
||||
min={0}
|
||||
max={300}
|
||||
step={5}
|
||||
value={backdropOilContrast}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropOilContrast = value;
|
||||
setBackdropOilContrast(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Saturation"
|
||||
desc="Colour intensity of the backdrop (0-400%)"
|
||||
min={0}
|
||||
max={400}
|
||||
step={5}
|
||||
value={backdropOilSaturation}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropOilSaturation = value;
|
||||
setBackdropOilSaturation(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOil && !backdropOilFollowCursor && (
|
||||
<LunaNumberSetting
|
||||
title="Shimmer Speed"
|
||||
desc="How fast the shimmer floats (0-100, default: 50)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropOilShimmerSpeed}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropOilShimmerSpeed = value;
|
||||
setBackdropOilShimmerSpeed(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOil && (
|
||||
<AnySwitch
|
||||
title="Shimmer Follows Cursor"
|
||||
desc="Point the shimmer at your cursor instead of floating"
|
||||
checked={backdropOilFollowCursor}
|
||||
onChange={(_: unknown, checked: boolean) => {
|
||||
settings.backdropOilFollowCursor = checked;
|
||||
setBackdropOilFollowCursor(checked);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Saturation"
|
||||
desc="Colour intensity of the backdrop (0-400% default = 125)"
|
||||
min={0}
|
||||
max={400}
|
||||
step={5}
|
||||
value={backdropSaturation}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropSaturation = value;
|
||||
setBackdropSaturation(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Backdrop Scale"
|
||||
desc="Zoom level of the effect (10-400% default = 100)"
|
||||
min={10}
|
||||
max={400}
|
||||
step={5}
|
||||
value={backdropScale}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropScale = value;
|
||||
setBackdropScale(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isOil && (
|
||||
<LunaNumberSetting
|
||||
title="Dithering"
|
||||
desc="Breaks up color banding in smooth gradients (0-100 default = 15)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropDithering}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropDithering = value;
|
||||
setBackdropDithering(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
// MARKER: Oil Backdrop
|
||||
// cover art -> palette -> procedural oil waves <3
|
||||
//
|
||||
// The shader below is ported verbatim from Terax's website:
|
||||
// https://github.com/crynta/Terax-website - components/line-waves.tsx
|
||||
// Copyright Crynta, Apache-2.0
|
||||
//
|
||||
// Fully procedural, art only feeds the colours.
|
||||
|
||||
import { Mesh, Program, Renderer, Triangle } from "ogl";
|
||||
import { type Rgb, samplePalette } from "./backdrop";
|
||||
import { settings } from "./Settings";
|
||||
|
||||
const getDpr = (): number =>
|
||||
settings.performanceMode ? 1 : Math.min(window.devicePixelRatio || 1, 2);
|
||||
|
||||
// Same coast as kawarp
|
||||
const RAMP_SECONDS = 1.8;
|
||||
// Values below are what terax.app actually renders with
|
||||
// (background-waves.tsx, not the component defaults)
|
||||
const REFERENCE_WAVE_SPEED = 0.35;
|
||||
const REFERENCE_WARP = 0.3;
|
||||
// Slider defaults land on the reference
|
||||
const REFERENCE_SPEED_AT_SETTING = 175;
|
||||
const REFERENCE_WARP_AT_SETTING = 100;
|
||||
// Brightness is its own setting (theirs assumes full opacity)
|
||||
// Shimmer has its own clock (sharing dragged the whole pattern)
|
||||
const DRIFT_SPEED_AT_SETTING = 100;
|
||||
// Glide, don't snap
|
||||
const POINTER_LERP = 0.04;
|
||||
|
||||
// Palette crossfade rate
|
||||
const COLOUR_LERP = 0.05;
|
||||
// Site's values for the rest
|
||||
const INNER_LINES = 40;
|
||||
const OUTER_LINES = 15;
|
||||
const ROTATION_DEG = -38;
|
||||
const EDGE_FADE_WIDTH = 0;
|
||||
// Site turns this off
|
||||
const COLOUR_CYCLE_SPEED = 0;
|
||||
const DRIFT_INFLUENCE = 1.6;
|
||||
|
||||
const vertexShader = `
|
||||
attribute vec2 uv;
|
||||
attribute vec2 position;
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 0, 1);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
precision highp float;
|
||||
|
||||
uniform float uTime;
|
||||
uniform vec3 uResolution;
|
||||
uniform float uSpeed;
|
||||
uniform float uInnerLines;
|
||||
uniform float uOuterLines;
|
||||
uniform float uWarpIntensity;
|
||||
uniform float uRotation;
|
||||
uniform float uEdgeFadeWidth;
|
||||
uniform float uColorCycleSpeed;
|
||||
uniform float uBrightness;
|
||||
uniform vec3 uColor1;
|
||||
uniform vec3 uColor2;
|
||||
uniform vec3 uColor3;
|
||||
uniform vec2 uMouse;
|
||||
uniform float uMouseInfluence;
|
||||
uniform bool uEnableMouse;
|
||||
|
||||
#define HALF_PI 1.5707963
|
||||
|
||||
float hashF(float n) {
|
||||
return fract(sin(n * 127.1) * 43758.5453123);
|
||||
}
|
||||
|
||||
float smoothNoise(float x) {
|
||||
float i = floor(x);
|
||||
float f = fract(x);
|
||||
float u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(hashF(i), hashF(i + 1.0), u);
|
||||
}
|
||||
|
||||
float displaceA(float coord, float t) {
|
||||
float result = sin(coord * 2.123) * 0.2;
|
||||
result += sin(coord * 3.234 + t * 4.345) * 0.1;
|
||||
result += sin(coord * 0.589 + t * 0.934) * 0.5;
|
||||
return result;
|
||||
}
|
||||
|
||||
float displaceB(float coord, float t) {
|
||||
float result = sin(coord * 1.345) * 0.3;
|
||||
result += sin(coord * 2.734 + t * 3.345) * 0.2;
|
||||
result += sin(coord * 0.189 + t * 0.934) * 0.3;
|
||||
return result;
|
||||
}
|
||||
|
||||
vec2 rotate2D(vec2 p, float angle) {
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
return vec2(p.x * c - p.y * s, p.x * s + p.y * c);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 coords = gl_FragCoord.xy / uResolution.xy;
|
||||
coords = coords * 2.0 - 1.0;
|
||||
coords = rotate2D(coords, uRotation);
|
||||
|
||||
float halfT = uTime * uSpeed * 0.5;
|
||||
float fullT = uTime * uSpeed;
|
||||
|
||||
float mouseWarp = 0.0;
|
||||
if (uEnableMouse) {
|
||||
vec2 mPos = rotate2D(uMouse * 2.0 - 1.0, uRotation);
|
||||
float mDist = length(coords - mPos);
|
||||
mouseWarp = uMouseInfluence * exp(-mDist * mDist * 4.0);
|
||||
}
|
||||
|
||||
float warpAx = coords.x + displaceA(coords.y, halfT) * uWarpIntensity + mouseWarp;
|
||||
float warpAy = coords.y - displaceA(coords.x * cos(fullT) * 1.235, halfT) * uWarpIntensity;
|
||||
float warpBx = coords.x + displaceB(coords.y, halfT) * uWarpIntensity + mouseWarp;
|
||||
float warpBy = coords.y - displaceB(coords.x * sin(fullT) * 1.235, halfT) * uWarpIntensity;
|
||||
|
||||
vec2 fieldA = vec2(warpAx, warpAy);
|
||||
vec2 fieldB = vec2(warpBx, warpBy);
|
||||
vec2 blended = mix(fieldA, fieldB, mix(fieldA, fieldB, 0.5));
|
||||
|
||||
float fadeTop = smoothstep(uEdgeFadeWidth, uEdgeFadeWidth + 0.4, blended.y);
|
||||
float fadeBottom = smoothstep(-uEdgeFadeWidth, -(uEdgeFadeWidth + 0.4), blended.y);
|
||||
float vMask = 1.0 - max(fadeTop, fadeBottom);
|
||||
|
||||
float tileCount = mix(uOuterLines, uInnerLines, vMask);
|
||||
float scaledY = blended.y * tileCount;
|
||||
float nY = smoothNoise(abs(scaledY));
|
||||
|
||||
float ridge = pow(
|
||||
step(abs(nY - blended.x) * 2.0, HALF_PI) * cos(2.0 * (nY - blended.x)),
|
||||
5.0
|
||||
);
|
||||
|
||||
float lines = 0.0;
|
||||
for (float i = 1.0; i < 3.0; i += 1.0) {
|
||||
lines += pow(max(fract(scaledY), fract(-scaledY)), i * 2.0);
|
||||
}
|
||||
|
||||
float pattern = vMask * lines;
|
||||
|
||||
float cycleT = fullT * uColorCycleSpeed;
|
||||
float lum = (pattern + lines * ridge) * (cos(blended.y + cycleT * 0.5) * 0.5 + 1.0);
|
||||
|
||||
vec3 baseColor = (uColor1 + uColor2 + uColor3) / 3.0;
|
||||
vec3 col = lum * baseColor * uBrightness;
|
||||
float alpha = clamp(length(col), 0.0, 1.0);
|
||||
|
||||
gl_FragColor = vec4(col, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
/** Drifting warp point (odd periods so it barely repeats) */
|
||||
const driftPoint = (t: number): [number, number] => [
|
||||
0.5 + 0.35 * Math.sin(t * 0.7) * Math.cos(t * 0.31),
|
||||
0.5 + 0.35 * Math.cos(t * 0.53) * Math.sin(t * 0.23),
|
||||
];
|
||||
|
||||
export class OilLayer {
|
||||
private host: HTMLElement;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private renderer: Renderer | null = null;
|
||||
private program: Program | null = null;
|
||||
private mesh: Mesh | null = null;
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private currentSrc: string | null = null;
|
||||
private loadToken = 0;
|
||||
private running = false;
|
||||
private failed = false;
|
||||
private playing = true;
|
||||
private rafId: number | null = null;
|
||||
private shaderTime = 0;
|
||||
private driftTime = 0;
|
||||
private lastFrame = 0;
|
||||
// Smoothed position + raw target
|
||||
private pointer: [number, number] = [0.5, 0.5];
|
||||
private cursorTarget: [number, number] = [0.5, 0.5];
|
||||
private cursorBound = false;
|
||||
// Eased 0-1
|
||||
private speedFactor = 1;
|
||||
private targetColours: [Rgb, Rgb, Rgb] = [
|
||||
[1, 1, 1],
|
||||
[1, 1, 1],
|
||||
[1, 1, 1],
|
||||
];
|
||||
|
||||
constructor(host: HTMLElement, id: string, zIndex: string) {
|
||||
this.host = host;
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.canvas.id = `rl-oil-${id}`;
|
||||
// kawarp's class, CSS already positions it
|
||||
this.canvas.className = "rl-kawarp-canvas";
|
||||
this.canvas.style.zIndex = zIndex;
|
||||
|
||||
this.canvas.addEventListener(
|
||||
"webglcontextlost",
|
||||
this.onContextLost as EventListener,
|
||||
false,
|
||||
);
|
||||
this.canvas.addEventListener(
|
||||
"webglcontextrestored",
|
||||
this.onContextRestored as EventListener,
|
||||
false,
|
||||
);
|
||||
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.syncSize();
|
||||
});
|
||||
this.resizeObserver.observe(host);
|
||||
}
|
||||
}
|
||||
|
||||
private onCursorMove = (event: MouseEvent): void => {
|
||||
const r = this.canvas.getBoundingClientRect();
|
||||
if (!r.width || !r.height) return;
|
||||
this.cursorTarget = [
|
||||
(event.clientX - r.left) / r.width,
|
||||
// Shader is y-up
|
||||
1 - (event.clientY - r.top) / r.height,
|
||||
];
|
||||
};
|
||||
|
||||
private bindCursor(want: boolean): void {
|
||||
if (want === this.cursorBound) return;
|
||||
this.cursorBound = want;
|
||||
// On window so it tracks behind content
|
||||
if (want) window.addEventListener("mousemove", this.onCursorMove);
|
||||
else window.removeEventListener("mousemove", this.onCursorMove);
|
||||
}
|
||||
|
||||
private onContextLost = (event: Event): void => {
|
||||
// Needed or no restore fires
|
||||
event.preventDefault();
|
||||
this.running = false;
|
||||
this.renderer = null;
|
||||
this.program = null;
|
||||
this.mesh = null;
|
||||
};
|
||||
|
||||
private onContextRestored = (): void => {
|
||||
this.renderer = null;
|
||||
this.program = null;
|
||||
this.mesh = null;
|
||||
this.failed = false;
|
||||
const src = this.currentSrc;
|
||||
this.currentSrc = null;
|
||||
this.apply(src);
|
||||
};
|
||||
|
||||
private ensureEngine(): Program | null {
|
||||
if (this.program || this.failed) return this.program;
|
||||
try {
|
||||
// Hand ogl our canvas (keeps CSS + cleanup the same)
|
||||
const renderer = new Renderer({
|
||||
canvas: this.canvas,
|
||||
alpha: true,
|
||||
premultipliedAlpha: false,
|
||||
});
|
||||
const gl = renderer.gl;
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex: vertexShader,
|
||||
fragment: fragmentShader,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uResolution: { value: [1, 1, 1] },
|
||||
uSpeed: { value: 1 },
|
||||
uInnerLines: { value: INNER_LINES },
|
||||
uOuterLines: { value: OUTER_LINES },
|
||||
uWarpIntensity: { value: 1 },
|
||||
uRotation: { value: (ROTATION_DEG * Math.PI) / 180 },
|
||||
uEdgeFadeWidth: { value: EDGE_FADE_WIDTH },
|
||||
uColorCycleSpeed: { value: COLOUR_CYCLE_SPEED },
|
||||
uBrightness: { value: 0.2 },
|
||||
uColor1: { value: [1, 1, 1] },
|
||||
uColor2: { value: [1, 1, 1] },
|
||||
uColor3: { value: [1, 1, 1] },
|
||||
uMouse: { value: new Float32Array([0.5, 0.5]) },
|
||||
uMouseInfluence: { value: DRIFT_INFLUENCE },
|
||||
// On, we just drive it ourselves
|
||||
uEnableMouse: { value: true },
|
||||
},
|
||||
});
|
||||
|
||||
this.renderer = renderer;
|
||||
this.program = program;
|
||||
this.mesh = new Mesh(gl, { geometry: new Triangle(gl), program });
|
||||
this.host.appendChild(this.canvas);
|
||||
this.syncSize();
|
||||
} catch (_err) {
|
||||
// No WebGL/context refused
|
||||
this.failed = true;
|
||||
this.renderer = null;
|
||||
this.program = null;
|
||||
}
|
||||
return this.program;
|
||||
}
|
||||
|
||||
private syncSize(): void {
|
||||
if (!this.renderer || !this.program) return;
|
||||
const dpr = getDpr();
|
||||
const rect = this.host.getBoundingClientRect();
|
||||
const width = Math.max(1, Math.round(rect.width));
|
||||
const height = Math.max(1, Math.round(rect.height));
|
||||
this.renderer.dpr = dpr;
|
||||
this.renderer.setSize(width, height);
|
||||
const gl = this.renderer.gl;
|
||||
this.program.uniforms.uResolution.value = [
|
||||
gl.canvas.width,
|
||||
gl.canvas.height,
|
||||
gl.canvas.width / gl.canvas.height,
|
||||
];
|
||||
}
|
||||
|
||||
/** False once Tidal has rebuilt the container */
|
||||
isMountedIn(container: HTMLElement): boolean {
|
||||
return this.canvas.parentElement === container;
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
if (!this.canvas.isConnected) return false;
|
||||
if (typeof this.canvas.checkVisibility === "function") {
|
||||
return this.canvas.checkVisibility({ checkVisibilityCSS: true });
|
||||
}
|
||||
return getComputedStyle(this.canvas).visibility !== "hidden";
|
||||
}
|
||||
|
||||
/** Push current settings + cover art into the shader. */
|
||||
apply(src: string | null): boolean {
|
||||
const program = this.ensureEngine();
|
||||
if (!program) return false;
|
||||
|
||||
const opacity = String(settings.backdropOilOpacity / 100);
|
||||
if (this.canvas.style.opacity !== opacity)
|
||||
this.canvas.style.opacity = opacity;
|
||||
|
||||
program.uniforms.uWarpIntensity.value =
|
||||
REFERENCE_WARP * (settings.backdropOilWarp / REFERENCE_WARP_AT_SETTING);
|
||||
program.uniforms.uSpeed.value =
|
||||
REFERENCE_WAVE_SPEED *
|
||||
(settings.backdropOilSpeed / REFERENCE_SPEED_AT_SETTING);
|
||||
// Own dial, no art here to wash out lyrics
|
||||
program.uniforms.uBrightness.value = settings.backdropOilBrightness / 100;
|
||||
|
||||
this.bindCursor(settings.backdropOilFollowCursor);
|
||||
|
||||
// No uniforms for these, so ride the canvas
|
||||
const grade: string[] = [];
|
||||
if (settings.backdropOilContrast !== 100)
|
||||
grade.push(`contrast(${settings.backdropOilContrast}%)`);
|
||||
if (settings.backdropOilSaturation !== 100)
|
||||
grade.push(`saturate(${settings.backdropOilSaturation}%)`);
|
||||
const filter = grade.join(" ");
|
||||
if (this.canvas.style.filter !== filter) this.canvas.style.filter = filter;
|
||||
|
||||
this.syncSize();
|
||||
|
||||
if (src && src !== this.currentSrc) {
|
||||
this.currentSrc = src;
|
||||
void this.loadPalette(src);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Tidal sends no CORS headers, so fetch + decode ourselves */
|
||||
private async loadPalette(src: string): Promise<void> {
|
||||
const token = ++this.loadToken;
|
||||
try {
|
||||
const res = await fetch(src);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const blob = await res.blob();
|
||||
if (token !== this.loadToken) return;
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
try {
|
||||
if (token !== this.loadToken) return;
|
||||
this.targetColours = samplePalette(bitmap);
|
||||
this.ensureLoop();
|
||||
} finally {
|
||||
bitmap.close();
|
||||
}
|
||||
} catch {
|
||||
// Keep the old palette, retry later
|
||||
if (token === this.loadToken) this.currentSrc = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Track play/pause (ramp render loop) */
|
||||
setPlaying(playing: boolean): void {
|
||||
if (this.playing === playing) return;
|
||||
this.playing = playing;
|
||||
this.ensureLoop();
|
||||
}
|
||||
|
||||
/** Where the speed multiplier is heading (1 = playing 0 = paused) */
|
||||
private get targetFactor(): number {
|
||||
if (!settings.backdropPlaybackReactive) return 1;
|
||||
return this.playing ? 1 : 0;
|
||||
}
|
||||
|
||||
private tick = (): void => {
|
||||
this.rafId = null;
|
||||
const program = this.program;
|
||||
const renderer = this.renderer;
|
||||
const mesh = this.mesh;
|
||||
if (!program || !renderer || !mesh || !this.running) return;
|
||||
|
||||
const now = performance.now();
|
||||
// freeze so backgrounded tab doesn't jump shader
|
||||
const dt = Math.min((now - this.lastFrame) / 1000, 0.1);
|
||||
this.lastFrame = now;
|
||||
|
||||
const target = this.targetFactor;
|
||||
const step = dt / RAMP_SECONDS;
|
||||
if (this.speedFactor < target) {
|
||||
this.speedFactor = Math.min(target, this.speedFactor + step);
|
||||
} else if (this.speedFactor > target) {
|
||||
this.speedFactor = Math.max(target, this.speedFactor - step);
|
||||
}
|
||||
|
||||
// Own clock in real seconds, speed lives in uSpeed
|
||||
this.shaderTime += dt * this.speedFactor;
|
||||
program.uniforms.uTime.value = this.shaderTime;
|
||||
|
||||
// Not the speed ramp's target
|
||||
let shimmer: [number, number];
|
||||
if (settings.backdropOilFollowCursor) {
|
||||
shimmer = this.cursorTarget;
|
||||
} else {
|
||||
this.driftTime +=
|
||||
dt *
|
||||
(settings.backdropOilShimmerSpeed / DRIFT_SPEED_AT_SETTING) *
|
||||
this.speedFactor;
|
||||
shimmer = driftPoint(this.driftTime);
|
||||
}
|
||||
// Glide when the source changes
|
||||
this.pointer[0] += (shimmer[0] - this.pointer[0]) * POINTER_LERP;
|
||||
this.pointer[1] += (shimmer[1] - this.pointer[1]) * POINTER_LERP;
|
||||
program.uniforms.uMouse.value[0] = this.pointer[0];
|
||||
program.uniforms.uMouse.value[1] = this.pointer[1];
|
||||
|
||||
// Ease into a new track's palette rather than cutting
|
||||
for (const [i, key] of (["uColor1", "uColor2", "uColor3"] as const).entries()) {
|
||||
const current = program.uniforms[key].value as number[];
|
||||
const wanted = this.targetColours[i];
|
||||
for (let c = 0; c < 3; c++) {
|
||||
current[c] += (wanted[c] - current[c]) * COLOUR_LERP;
|
||||
}
|
||||
}
|
||||
|
||||
renderer.render({ scene: mesh });
|
||||
|
||||
// Stopped, park the loop
|
||||
if (this.speedFactor === 0 && target === 0) return;
|
||||
this.rafId = requestAnimationFrame(this.tick);
|
||||
};
|
||||
|
||||
private ensureLoop(): void {
|
||||
if (this.rafId !== null || !this.running || !this.program) return;
|
||||
this.lastFrame = performance.now();
|
||||
this.rafId = requestAnimationFrame(this.tick);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.running || !this.program) return;
|
||||
this.running = true;
|
||||
this.ensureLoop();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (!this.running) return;
|
||||
this.running = false;
|
||||
if (this.rafId !== null) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
}
|
||||
|
||||
setPaused(paused: boolean): void {
|
||||
if (paused) this.stop();
|
||||
else this.start();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
this.bindCursor(false);
|
||||
this.resizeObserver?.disconnect();
|
||||
this.resizeObserver = null;
|
||||
this.canvas.removeEventListener(
|
||||
"webglcontextlost",
|
||||
this.onContextLost as EventListener,
|
||||
);
|
||||
this.canvas.removeEventListener(
|
||||
"webglcontextrestored",
|
||||
this.onContextRestored as EventListener,
|
||||
);
|
||||
// ogl has no disposer
|
||||
this.renderer?.gl.getExtension("WEBGL_lose_context")?.loseContext();
|
||||
this.renderer = null;
|
||||
this.program = null;
|
||||
this.mesh = null;
|
||||
this.canvas.remove();
|
||||
this.currentSrc = null;
|
||||
}
|
||||
}
|
||||
@@ -29,48 +29,39 @@ const getDpr = (): number =>
|
||||
|
||||
// Claude did all the auto darken stuff cause i'm not bothered to make this stuff a second time.. (so ignore comment bloat)
|
||||
|
||||
// Auto-darken samples the album art at this size to judge how bright it is.
|
||||
// 16x16 is 256 pixels and the GPU does the downscale.
|
||||
// Sample size for auto-darken (GPU does the downscale)
|
||||
const SAMPLE_SIZE = 16;
|
||||
// Never crush the backdrop to black, however blinding the cover art is
|
||||
// Never go fully black
|
||||
const MIN_BRIGHTNESS = 0.15;
|
||||
// Darken strength maps onto a luminance ceiling: how bright the backdrop is
|
||||
// allowed to read before it gets pulled down. Strength 1 barely touches
|
||||
// anything, 100 keeps even white covers well below the lyrics.
|
||||
// Darken strength -> luminance ceiling
|
||||
const CEILING_AT_MIN_STRENGTH = 0.9;
|
||||
const CEILING_AT_MAX_STRENGTH = 0.15;
|
||||
// Seconds to coast between full speed and a standstill. Linear, so the
|
||||
// deceleration is constant rather than dropping off a cliff and then crawling.
|
||||
// Seconds to coast to a stop (linear)
|
||||
const RAMP_SECONDS = 1.8;
|
||||
// kawarp's album-art crossfade defaults to 1000ms - keep rendering a little
|
||||
// past that so a track change still resolves while playback is paused
|
||||
// Render past the cover crossfade (1000ms)
|
||||
const CROSSFADE_RENDER_MS = 1400;
|
||||
|
||||
/**
|
||||
* Mean Rec. 709 luma of an image, 0-1.
|
||||
*
|
||||
* Measured on the source art rather than the rendered canvas. The shader output
|
||||
* is in constant motion, so sampling it gives a brightness that drifts with the
|
||||
* animation; the cover art is fixed, so one reading per track is exact and
|
||||
* stays put. A blur preserves mean luminance, so this is also a faithful
|
||||
* predictor of how bright the finished backdrop lands.
|
||||
*/
|
||||
/** Mean luma of the art (measure source, not the moving canvas) */
|
||||
let sampleCtx: CanvasRenderingContext2D | null = null;
|
||||
const measureLuminance = (source: CanvasImageSource): number | null => {
|
||||
const ensureSampler = (): CanvasRenderingContext2D | null => {
|
||||
if (!sampleCtx) {
|
||||
const sampler = document.createElement("canvas");
|
||||
sampler.width = SAMPLE_SIZE;
|
||||
sampler.height = SAMPLE_SIZE;
|
||||
sampleCtx = sampler.getContext("2d", { willReadFrequently: true });
|
||||
}
|
||||
if (!sampleCtx) return null;
|
||||
return sampleCtx;
|
||||
};
|
||||
|
||||
const measureLuminance = (source: CanvasImageSource): number | null => {
|
||||
if (!ensureSampler() || !sampleCtx) return null;
|
||||
try {
|
||||
sampleCtx.clearRect(0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
||||
sampleCtx.drawImage(source, 0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
||||
const { data } = sampleCtx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// Green dominates how bright a colour reads to the eye
|
||||
// Green reads brightest
|
||||
sum +=
|
||||
(0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]) / 255;
|
||||
}
|
||||
@@ -80,6 +71,72 @@ const measureLuminance = (source: CanvasImageSource): number | null => {
|
||||
}
|
||||
};
|
||||
|
||||
export type Rgb = [number, number, number];
|
||||
|
||||
const WHITE: Rgb = [1, 1, 1];
|
||||
// Scale colours up to this (keeps hue)
|
||||
const PALETTE_TARGET = 0.9;
|
||||
// Too dark to use
|
||||
const MIN_CHANNEL = 0.15;
|
||||
// Too grey to use
|
||||
const MIN_SATURATION = 0.08;
|
||||
|
||||
/** 3 dominant colours from the art (mono covers -> white) */
|
||||
export const samplePalette = (source: CanvasImageSource): [Rgb, Rgb, Rgb] => {
|
||||
if (!ensureSampler() || !sampleCtx) return [WHITE, WHITE, WHITE];
|
||||
try {
|
||||
sampleCtx.clearRect(0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
||||
sampleCtx.drawImage(source, 0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
||||
const { data } = sampleCtx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
||||
|
||||
// Coarse buckets so shades merge
|
||||
const buckets = new Map<number, { n: number; r: number; g: number; b: number }>();
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i] / 255;
|
||||
const g = data[i + 1] / 255;
|
||||
const b = data[i + 2] / 255;
|
||||
const max = Math.max(r, g, b);
|
||||
if (max < MIN_CHANNEL) continue;
|
||||
if (max - Math.min(r, g, b) < MIN_SATURATION) continue;
|
||||
const key =
|
||||
((r * 7) | 0) * 64 + ((g * 7) | 0) * 8 + ((b * 7) | 0);
|
||||
const hit = buckets.get(key);
|
||||
if (hit) {
|
||||
hit.n++;
|
||||
hit.r += r;
|
||||
hit.g += g;
|
||||
hit.b += b;
|
||||
} else {
|
||||
buckets.set(key, { n: 1, r, g, b });
|
||||
}
|
||||
}
|
||||
|
||||
const ranked = [...buckets.values()]
|
||||
.sort((a, b) => b.n - a.n)
|
||||
.slice(0, 3)
|
||||
// Average so it's not grid-snapped
|
||||
.map((c): Rgb => [c.r / c.n, c.g / c.n, c.b / c.n])
|
||||
// Dark covers give dark colours, so lift them
|
||||
.map((c): Rgb => {
|
||||
const max = Math.max(c[0], c[1], c[2]);
|
||||
if (max <= 0) return WHITE;
|
||||
const scale = PALETTE_TARGET / max;
|
||||
return [c[0] * scale, c[1] * scale, c[2] * scale];
|
||||
});
|
||||
|
||||
if (ranked.length === 0) return [WHITE, WHITE, WHITE];
|
||||
// Cycle what we found (white would desaturate)
|
||||
return [
|
||||
ranked[0],
|
||||
ranked[1] ?? ranked[0],
|
||||
ranked[2] ?? ranked[1] ?? ranked[0],
|
||||
];
|
||||
} catch {
|
||||
return [WHITE, WHITE, WHITE];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export class KawarpLayer {
|
||||
private host: HTMLElement;
|
||||
private canvas: HTMLCanvasElement;
|
||||
@@ -90,21 +147,21 @@ export class KawarpLayer {
|
||||
private appliedOptions = "";
|
||||
private running = false;
|
||||
private failed = false;
|
||||
// Luminance of the current cover art - measured once when it loads
|
||||
// Measured once per cover
|
||||
private coverLuminance: number | null = null;
|
||||
private brightness = 1;
|
||||
private playing = true;
|
||||
private rafId: number | null = null;
|
||||
private shaderTime = 0;
|
||||
private lastFrame = 0;
|
||||
// Current speed multiplier, eased between 0 and 1
|
||||
// Eased 0-1
|
||||
private speedFactor = 1;
|
||||
private transitionUntil = 0;
|
||||
|
||||
constructor(host: HTMLElement, id: string, zIndex: string) {
|
||||
this.host = host;
|
||||
this.canvas = document.createElement("canvas");
|
||||
// Same id shape BetterLyrics uses, so its own detection selector matches
|
||||
// detection selector
|
||||
this.canvas.id = `better-lyrics-kawarp-${id}`;
|
||||
this.canvas.className = "rl-kawarp-canvas";
|
||||
this.canvas.style.zIndex = zIndex;
|
||||
@@ -130,7 +187,7 @@ export class KawarpLayer {
|
||||
}
|
||||
|
||||
private onContextLost = (event: Event): void => {
|
||||
// Preventing default is what allows a restore to be delivered
|
||||
// Needed or no restore fires
|
||||
event.preventDefault();
|
||||
this.running = false;
|
||||
this.kawarp = null;
|
||||
@@ -166,7 +223,7 @@ export class KawarpLayer {
|
||||
const width = Math.max(1, Math.round(rect.width * dpr));
|
||||
// Aurora: render a single row of pixels and let CSS stretch it down the whole canvas (makes bands of color)
|
||||
const height =
|
||||
settings.backdropStyle === 1
|
||||
settings.backdropStyle === 3
|
||||
? 1
|
||||
: Math.max(1, Math.round(rect.height * dpr));
|
||||
if (this.canvas.width === width && this.canvas.height === height) return;
|
||||
@@ -180,9 +237,7 @@ export class KawarpLayer {
|
||||
return this.canvas.parentElement === container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this canvas is actually on screen.
|
||||
*/
|
||||
/** Actually on screen? */
|
||||
isVisible(): boolean {
|
||||
if (!this.canvas.isConnected) return false;
|
||||
if (typeof this.canvas.checkVisibility === "function") {
|
||||
@@ -228,7 +283,7 @@ export class KawarpLayer {
|
||||
const res = await fetch(src);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const blob = await res.blob();
|
||||
// A newer track won the race
|
||||
// Newer track won
|
||||
if (token !== this.loadToken || this.kawarp !== engine) return;
|
||||
// Decode once
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* Cover-art backdrop container styles
|
||||
*/
|
||||
/* Backdrop containers */
|
||||
|
||||
/* Stock backdrop projected app wide */
|
||||
.rl-stock-background-container {
|
||||
@@ -109,7 +108,7 @@
|
||||
z-index: -3;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
/* Dark base the shader composites over at its configured opacity */
|
||||
/* Dark base under the shader */
|
||||
background: #000;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
romanizeLyrics as romanizeLyricsApi,
|
||||
} from "./api";
|
||||
import { KawarpLayer } from "./backdrop";
|
||||
import { OilLayer } from "./backdrop-oil";
|
||||
import { Settings, settings } from "./Settings";
|
||||
|
||||
import backdropStylesCss from "file://backdrop-styles.css?minify";
|
||||
@@ -689,9 +690,12 @@ let globalBackgroundContainer: HTMLElement | null = null;
|
||||
let nowPlayingBackgroundContainer: HTMLElement | null = null;
|
||||
let currentCoverSrc: string | null = null;
|
||||
|
||||
/** Either renderer */
|
||||
type BackdropLayer = KawarpLayer | OilLayer;
|
||||
|
||||
const kawarpLayers: {
|
||||
global: KawarpLayer | null;
|
||||
nowPlaying: KawarpLayer | null;
|
||||
global: BackdropLayer | null;
|
||||
nowPlaying: BackdropLayer | null;
|
||||
} = { global: null, nowPlaying: null };
|
||||
|
||||
const disposeKawarpLayer = (slot: "global" | "nowPlaying"): void => {
|
||||
@@ -711,7 +715,7 @@ const pauseHiddenSince: { global: number; nowPlaying: number } = {
|
||||
|
||||
const setLayerRunning = (
|
||||
slot: "global" | "nowPlaying",
|
||||
layer: KawarpLayer | null,
|
||||
layer: BackdropLayer | null,
|
||||
shouldRun: boolean,
|
||||
now: number,
|
||||
): void => {
|
||||
@@ -767,7 +771,7 @@ const syncBackdropActivity = (): void => {
|
||||
|
||||
/** cover art @ resolution worth sampling */
|
||||
const getCoverArtSrc = (): string | null => {
|
||||
const targetRes = settings.performanceMode ? "640x640" : "1280x1280";
|
||||
const targetRes = "160x160";
|
||||
const img = document.querySelector(
|
||||
'[data-test="current-media-imagery"] img',
|
||||
) as HTMLImageElement | null;
|
||||
@@ -785,15 +789,19 @@ const ensureLayer = (
|
||||
slot: "global" | "nowPlaying",
|
||||
container: HTMLElement,
|
||||
zIndex: string,
|
||||
): KawarpLayer | null => {
|
||||
): BackdropLayer | null => {
|
||||
// Oil is a whole different renderer
|
||||
const wantsOil = settings.backdropStyle === 2;
|
||||
let layer = kawarpLayers[slot];
|
||||
if (layer && !layer.isMountedIn(container)) {
|
||||
// Tidal rebuilt the container
|
||||
if (layer && (!layer.isMountedIn(container) || wantsOil !== (layer instanceof OilLayer))) {
|
||||
// Container rebuilt or renderer changed
|
||||
disposeKawarpLayer(slot);
|
||||
layer = null;
|
||||
}
|
||||
if (!layer) {
|
||||
layer = new KawarpLayer(container, slot, zIndex);
|
||||
layer = wantsOil
|
||||
? new OilLayer(container, slot, zIndex)
|
||||
: new KawarpLayer(container, slot, zIndex);
|
||||
kawarpLayers[slot] = layer;
|
||||
}
|
||||
return layer;
|
||||
|
||||
Generated
+8
@@ -52,6 +52,9 @@ importers:
|
||||
'@kawarp/core':
|
||||
specifier: '*'
|
||||
version: 1.2.0
|
||||
ogl:
|
||||
specifier: '*'
|
||||
version: 1.0.11
|
||||
|
||||
packages:
|
||||
|
||||
@@ -542,6 +545,9 @@ packages:
|
||||
oby@15.1.2:
|
||||
resolution: {integrity: sha512-6QD9iEoPzV+pMDdcg3RtFWhgDX8pS5hZouVHvgXGDy3Q9RxFfnI3CYv9i62keeuX+qk6iN2z5E9FD3q3OckZ6A==}
|
||||
|
||||
ogl@1.0.11:
|
||||
resolution: {integrity: sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==}
|
||||
|
||||
opener@1.5.2:
|
||||
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
|
||||
hasBin: true
|
||||
@@ -1162,6 +1168,8 @@ snapshots:
|
||||
|
||||
oby@15.1.2: {}
|
||||
|
||||
ogl@1.0.11: {}
|
||||
|
||||
opener@1.5.2: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
Reference in New Issue
Block a user