From 2c384fa0cbc1b7d693bd8743e941195c55e0c1a4 Mon Sep 17 00:00:00 2001 From: meowarex Date: Fri, 14 Aug 2026 23:03:04 +1000 Subject: [PATCH] Remove Oil Shader <3 --- plugins/radiant-lyrics-luna/package.json | 3 +- plugins/radiant-lyrics-luna/src/Settings.tsx | 409 ++++---------- .../radiant-lyrics-luna/src/backdrop-oil.ts | 514 ------------------ plugins/radiant-lyrics-luna/src/backdrop.ts | 76 +-- plugins/radiant-lyrics-luna/src/index.ts | 22 +- pnpm-lock.yaml | 8 - 6 files changed, 130 insertions(+), 902 deletions(-) delete mode 100644 plugins/radiant-lyrics-luna/src/backdrop-oil.ts diff --git a/plugins/radiant-lyrics-luna/package.json b/plugins/radiant-lyrics-luna/package.json index ac53930..b465460 100644 --- a/plugins/radiant-lyrics-luna/package.json +++ b/plugins/radiant-lyrics-luna/package.json @@ -9,7 +9,6 @@ "main": "./src/index.ts", "type": "module", "dependencies": { - "@kawarp/core": "*", - "ogl": "*" + "@kawarp/core": "*" } } diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index 95dfa87..a2422c2 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -61,15 +61,6 @@ 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 = () => { @@ -122,30 +113,6 @@ 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, ); @@ -231,9 +198,6 @@ 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; type AnySwitchProps = Omit & { @@ -953,9 +917,9 @@ export const Settings = () => { <> { @@ -984,258 +948,123 @@ export const Settings = () => { refreshBackdrop(); }} /> - {!isOil && ( - { - settings.backdropOpacity = value; - setBackdropOpacity(value); - refreshBackdrop(); - }} - /> - )} - {!isOil && ( - { - settings.backdropWarp = value; - setBackdropWarp(value); - refreshBackdrop(); - }} - /> - )} - {!isOil && ( - { - settings.backdropBlurPasses = value; - setBackdropBlurPasses(value); - refreshBackdrop(); - }} - /> - )} - {!isOil && ( - { - settings.backdropSpeed = value; - setBackdropSpeed(value); - refreshBackdrop(); - }} - /> - )} - {!isOil && ( - { - settings.backdropContrast = value; - setBackdropContrast(value); - refreshBackdrop(); - }} - /> - )} - {!isOil && ( - { - settings.backdropDarken = value; - setBackdropDarken(value); - refreshBackdrop(); - }} - /> - )} - {isOil && ( - { - settings.backdropOilOpacity = value; - setBackdropOilOpacity(value); - refreshBackdrop(); - }} - /> - )} - {isOil && ( - { - settings.backdropOilWarp = value; - setBackdropOilWarp(value); - refreshBackdrop(); - }} - /> - )} - {isOil && ( - { - settings.backdropOilSpeed = value; - setBackdropOilSpeed(value); - refreshBackdrop(); - }} - /> - )} - {isOil && ( - { - settings.backdropOilBrightness = value; - setBackdropOilBrightness(value); - refreshBackdrop(); - }} - /> - )} - {isOil && ( - { - settings.backdropOilContrast = value; - setBackdropOilContrast(value); - refreshBackdrop(); - }} - /> - )} - {isOil && ( - { - settings.backdropOilSaturation = value; - setBackdropOilSaturation(value); - refreshBackdrop(); - }} - /> - )} - {isOil && !backdropOilFollowCursor && ( - { - settings.backdropOilShimmerSpeed = value; - setBackdropOilShimmerSpeed(value); - refreshBackdrop(); - }} - /> - )} - {isOil && ( - { - settings.backdropOilFollowCursor = checked; - setBackdropOilFollowCursor(checked); - refreshBackdrop(); - }} - /> - )} - {!isOil && ( - { - settings.backdropSaturation = value; - setBackdropSaturation(value); - refreshBackdrop(); - }} - /> - )} - {!isOil && ( - { - settings.backdropScale = value; - setBackdropScale(value); - refreshBackdrop(); - }} - /> - )} - {!isOil && ( - { - settings.backdropDithering = value; - setBackdropDithering(value); - refreshBackdrop(); - }} - /> - )} + { + settings.backdropOpacity = value; + setBackdropOpacity(value); + refreshBackdrop(); + }} + /> + { + settings.backdropWarp = value; + setBackdropWarp(value); + refreshBackdrop(); + }} + /> + { + settings.backdropBlurPasses = value; + setBackdropBlurPasses(value); + refreshBackdrop(); + }} + /> + { + settings.backdropSpeed = value; + setBackdropSpeed(value); + refreshBackdrop(); + }} + /> + { + settings.backdropContrast = value; + setBackdropContrast(value); + refreshBackdrop(); + }} + /> + { + settings.backdropDarken = value; + setBackdropDarken(value); + refreshBackdrop(); + }} + /> + { + settings.backdropSaturation = value; + setBackdropSaturation(value); + refreshBackdrop(); + }} + /> + { + settings.backdropScale = value; + setBackdropScale(value); + refreshBackdrop(); + }} + /> + { + settings.backdropDithering = value; + setBackdropDithering(value); + refreshBackdrop(); + }} + /> )} diff --git a/plugins/radiant-lyrics-luna/src/backdrop-oil.ts b/plugins/radiant-lyrics-luna/src/backdrop-oil.ts deleted file mode 100644 index 6eb7b9c..0000000 --- a/plugins/radiant-lyrics-luna/src/backdrop-oil.ts +++ /dev/null @@ -1,514 +0,0 @@ -// 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 { - 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; - } -} diff --git a/plugins/radiant-lyrics-luna/src/backdrop.ts b/plugins/radiant-lyrics-luna/src/backdrop.ts index 95c7f7e..97394fd 100644 --- a/plugins/radiant-lyrics-luna/src/backdrop.ts +++ b/plugins/radiant-lyrics-luna/src/backdrop.ts @@ -43,18 +43,14 @@ const CROSSFADE_RENDER_MS = 1400; /** Mean luma of the art (measure source, not the moving canvas) */ let sampleCtx: CanvasRenderingContext2D | null = null; -const ensureSampler = (): CanvasRenderingContext2D | null => { +const measureLuminance = (source: CanvasImageSource): number | null => { if (!sampleCtx) { const sampler = document.createElement("canvas"); sampler.width = SAMPLE_SIZE; sampler.height = SAMPLE_SIZE; sampleCtx = sampler.getContext("2d", { willReadFrequently: true }); } - return sampleCtx; -}; - -const measureLuminance = (source: CanvasImageSource): number | null => { - if (!ensureSampler() || !sampleCtx) return null; + if (!sampleCtx) return null; try { sampleCtx.clearRect(0, 0, SAMPLE_SIZE, SAMPLE_SIZE); sampleCtx.drawImage(source, 0, 0, SAMPLE_SIZE, SAMPLE_SIZE); @@ -71,72 +67,6 @@ 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(); - 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; @@ -223,7 +153,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 === 3 + settings.backdropStyle === 1 ? 1 : Math.max(1, Math.round(rect.height * dpr)); if (this.canvas.width === width && this.canvas.height === height) return; diff --git a/plugins/radiant-lyrics-luna/src/index.ts b/plugins/radiant-lyrics-luna/src/index.ts index 4c59f7a..e48971a 100644 --- a/plugins/radiant-lyrics-luna/src/index.ts +++ b/plugins/radiant-lyrics-luna/src/index.ts @@ -20,7 +20,6 @@ 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"; @@ -690,12 +689,9 @@ let globalBackgroundContainer: HTMLElement | null = null; let nowPlayingBackgroundContainer: HTMLElement | null = null; let currentCoverSrc: string | null = null; -/** Either renderer */ -type BackdropLayer = KawarpLayer | OilLayer; - const kawarpLayers: { - global: BackdropLayer | null; - nowPlaying: BackdropLayer | null; + global: KawarpLayer | null; + nowPlaying: KawarpLayer | null; } = { global: null, nowPlaying: null }; const disposeKawarpLayer = (slot: "global" | "nowPlaying"): void => { @@ -715,7 +711,7 @@ const pauseHiddenSince: { global: number; nowPlaying: number } = { const setLayerRunning = ( slot: "global" | "nowPlaying", - layer: BackdropLayer | null, + layer: KawarpLayer | null, shouldRun: boolean, now: number, ): void => { @@ -789,19 +785,15 @@ const ensureLayer = ( slot: "global" | "nowPlaying", container: HTMLElement, zIndex: string, -): BackdropLayer | null => { - // Oil is a whole different renderer - const wantsOil = settings.backdropStyle === 2; +): KawarpLayer | null => { let layer = kawarpLayers[slot]; - if (layer && (!layer.isMountedIn(container) || wantsOil !== (layer instanceof OilLayer))) { - // Container rebuilt or renderer changed + if (layer && !layer.isMountedIn(container)) { + // Tidal rebuilt the container disposeKawarpLayer(slot); layer = null; } if (!layer) { - layer = wantsOil - ? new OilLayer(container, slot, zIndex) - : new KawarpLayer(container, slot, zIndex); + layer = new KawarpLayer(container, slot, zIndex); kawarpLayers[slot] = layer; } return layer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d992f1..9a35446 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,9 +52,6 @@ importers: '@kawarp/core': specifier: '*' version: 1.2.0 - ogl: - specifier: '*' - version: 1.0.11 packages: @@ -545,9 +542,6 @@ 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 @@ -1168,8 +1162,6 @@ snapshots: oby@15.1.2: {} - ogl@1.0.11: {} - opener@1.5.2: {} package-json-from-dist@1.0.1: {}