diff --git a/plugins/audio-visualizer-luna/src/Settings.tsx b/plugins/audio-visualizer-luna/src/Settings.tsx index cea0ebb..d7e8297 100644 --- a/plugins/audio-visualizer-luna/src/Settings.tsx +++ b/plugins/audio-visualizer-luna/src/Settings.tsx @@ -52,6 +52,8 @@ export const settings = await ReactiveStore.getPluginStorage( opacityFalloff: 0.5, lissajous: false, scrollingOscilloscope: false, + groupedSlots: false, + transparentContainers: false, miniSlots: [] as string[], customColors: [] as string[], }, @@ -86,6 +88,11 @@ export const Settings = () => { const [scrollingOscilloscope, setScrollingOscilloscope] = React.useState(settings.scrollingOscilloscope); + const [groupedSlots, setGroupedSlots] = React.useState(settings.groupedSlots); + const [transparentContainers, setTransparentContainers] = React.useState( + settings.transparentContainers, + ); + const [showColorPicker, setShowColorPicker] = React.useState(false); const [isColorAnimIn, setIsColorAnimIn] = React.useState(false); const [shouldRenderColor, setShouldRenderColor] = React.useState(false); @@ -286,6 +293,26 @@ export const Settings = () => { + { + setGroupedSlots(checked); + settings.groupedSlots = checked; + }} + /> + + { + setTransparentContainers(checked); + settings.transparentContainers = checked; + }} + /> + {/* Color picker modal */} {shouldRenderColor && ( <> diff --git a/plugins/audio-visualizer-luna/src/index.ts b/plugins/audio-visualizer-luna/src/index.ts index 67dc004..0a80177 100644 --- a/plugins/audio-visualizer-luna/src/index.ts +++ b/plugins/audio-visualizer-luna/src/index.ts @@ -132,10 +132,16 @@ const syncGroupHeights = (group: SlotGroup): void => { }; const updateGroupVisibility = (group: SlotGroup): void => { - const allNone = group.slots.every(s => s.currentType === "none"); + const activeCount = group.slots.filter(s => s.currentType !== "none").length; + const allNone = activeCount === 0; group.groupContainer.style.display = allNone ? "none" : "flex"; if (!allNone) syncGroupHeights(group); + group.groupContainer.classList.toggle( + "av-grouped", + settings.groupedSlots && activeCount >= 2, + ); + if (group === groups.get("topNav-left") && navArrowsEl) { navArrowsEl.style.marginRight = allNone ? "" : "0"; } @@ -397,6 +403,14 @@ const generateIdleData = (): AudioData => { let animationId: number | null = null; const lastSlotTypes = new Map(); const lastMiniState = new Map(); +let lastGrouped = settings.groupedSlots; +let lastChromeless = settings.transparentContainers; + +const syncChromelessClass = (): void => { + document.body.classList.toggle("av-chromeless", !!settings.transparentContainers); +}; + +syncChromelessClass(); for (const key of ALL_SLOT_KEYS) { lastSlotTypes.set(key, getSlot(key)); @@ -427,6 +441,18 @@ const animate = (): void => { if (changed) updateGroupVisibility(group); } + const grouped = settings.groupedSlots; + if (grouped !== lastGrouped) { + for (const group of groups.values()) updateGroupVisibility(group); + lastGrouped = grouped; + } + + const chromeless = !!settings.transparentContainers; + if (chromeless !== lastChromeless) { + syncChromelessClass(); + lastChromeless = chromeless; + } + const currentReactivity = settings.reactivity ?? 30; if (currentReactivity !== lastReactivity) { audio.setSmoothing(reactivityToSmoothing(currentReactivity)); @@ -480,6 +506,8 @@ unloads.add(() => { log("Plugin unloading"); clearRetry(); + document.body.classList.remove("av-chromeless"); + if (navArrowsEl) { navArrowsEl.style.marginRight = ""; navArrowsEl = null; diff --git a/plugins/audio-visualizer-luna/src/styles.css b/plugins/audio-visualizer-luna/src/styles.css index 1ac97e4..e45a9c9 100644 --- a/plugins/audio-visualizer-luna/src/styles.css +++ b/plugins/audio-visualizer-luna/src/styles.css @@ -68,3 +68,66 @@ margin-right: 8px; } +/* Grouped slots: merge active containers into one shared box */ +.av-slot-group.av-grouped { + gap: 0; + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 105, 180, 0.15); + animation: av-fadeIn 0.5s ease-out; + transition: all 0.3s ease-in-out; +} +.av-slot-group.av-grouped:hover { + transform: scale(1.02); + box-shadow: 0 4px 12px rgba(255, 105, 180, 0.2); + border-color: rgba(255, 105, 180, 0.3); +} +.av-slot-group.av-grouped > .audio-visualizer-container { + background: none; + border: none; + border-radius: 0; + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: none; + animation: none; +} +.av-slot-group.av-grouped > .audio-visualizer-container:hover { + transform: none; + box-shadow: none; +} + +/* Chromeless: no fill, blur, or shadow; border kept */ +body.av-chromeless .audio-visualizer-container { + background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: none; + border: 1px solid rgba(255, 105, 180, 0.15); + animation: none; + transition: border-color 0.3s ease-in-out; +} +body.av-chromeless .audio-visualizer-container:hover { + transform: none; + box-shadow: none; + border-color: rgba(255, 105, 180, 0.3); +} +body.av-chromeless .audio-visualizer-container.active { + box-shadow: none; +} +body.av-chromeless .av-slot-group.av-grouped { + background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; + box-shadow: none; + border: 1px solid rgba(255, 105, 180, 0.15); + animation: none; + transition: border-color 0.3s ease-in-out; +} +body.av-chromeless .av-slot-group.av-grouped:hover { + transform: none; + box-shadow: none; + border-color: rgba(255, 105, 180, 0.3); +} diff --git a/plugins/audio-visualizer-luna/src/visualizers/types.ts b/plugins/audio-visualizer-luna/src/visualizers/types.ts index 4081906..a0abcf8 100644 --- a/plugins/audio-visualizer-luna/src/visualizers/types.ts +++ b/plugins/audio-visualizer-luna/src/visualizers/types.ts @@ -26,7 +26,7 @@ export const VISUALIZER_DIMENSIONS: Record "spectrum-line": { width: 200, height: 40 }, "spectrum-bars": { width: 200, height: 40 }, oscilloscope: { width: 200, height: 40 }, - vectorscope: { width: 60, height: 60 }, + vectorscope: { width: 100, height: 40 }, "loudness-meter": { width: 160, height: 40 }, none: { width: 0, height: 0 }, }; @@ -80,8 +80,9 @@ export const POSITION_LABELS: Record = { right: "Right", }; -export const MINI_SUPPORTED = new Set(["oscilloscope"]); +export const MINI_SUPPORTED = new Set(["oscilloscope", "vectorscope"]); export const MINI_DIMENSIONS: Partial> = { oscilloscope: { width: 80, height: 60 }, + vectorscope: { width: 72, height: 40 }, }; diff --git a/plugins/audio-visualizer-luna/src/visualizers/vectorscope.ts b/plugins/audio-visualizer-luna/src/visualizers/vectorscope.ts index b111341..b9e877e 100644 --- a/plugins/audio-visualizer-luna/src/visualizers/vectorscope.ts +++ b/plugins/audio-visualizer-luna/src/visualizers/vectorscope.ts @@ -5,8 +5,6 @@ import { settings } from "../Settings"; export const createVectorscope = (): Visualizer => { let ctx: CanvasRenderingContext2D | null = null; let canvas: HTMLCanvasElement | null = null; - let trailCanvas: HTMLCanvasElement | null = null; - let trailCtx: CanvasRenderingContext2D | null = null; let w = 0, h = 0; let lastX = 0, lastY = 0; let hasLast = false; @@ -18,22 +16,19 @@ export const createVectorscope = (): Visualizer => { init(cvs, _color) { canvas = cvs; - ctx = cvs.getContext("2d")!; + const c = cvs.getContext("2d"); + if (!c) return; + ctx = c; w = cvs.width; h = cvs.height; hasLast = false; - trailCanvas = document.createElement("canvas"); - trailCanvas.width = w; - trailCanvas.height = h; - trailCtx = trailCanvas.getContext("2d")!; - lastLissajous = !!settings.lissajous; cvs.style.transform = lastLissajous ? "rotate(45deg) scale(0.707)" : ""; }, render(data: AudioData, color: string) { - if (!ctx || !trailCtx || !trailCanvas || !canvas) return; + if (!ctx || !canvas) return; const wantLissajous = !!settings.lissajous; if (wantLissajous !== lastLissajous) { @@ -41,64 +36,50 @@ export const createVectorscope = (): Visualizer => { canvas.style.transform = wantLissajous ? "rotate(45deg) scale(0.707)" : ""; } - // Fade the trail buffer by drawing it at reduced opacity onto itself - trailCtx.save(); - trailCtx.globalCompositeOperation = "destination-in"; - trailCtx.fillStyle = "rgba(0, 0, 0, 0.82)"; - trailCtx.fillRect(0, 0, w, h); - trailCtx.restore(); + ctx.clearRect(0, 0, w, h); const left = data.leftTimeDomain; const right = data.rightTimeDomain; const len = Math.min(left.length, right.length); const lineWidth = Math.max(0.5, (settings.lineThickness ?? 1.0) * 0.5); - const scale = 2.25; + const inset = lineWidth; + const halfW = Math.max(1, w / 2 - inset); + const halfH = Math.max(1, h / 2 - inset); - trailCtx.strokeStyle = color; - trailCtx.lineWidth = lineWidth; - trailCtx.lineJoin = "round"; - trailCtx.lineCap = "round"; - trailCtx.globalAlpha = 0.9; + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.lineJoin = "round"; + ctx.lineCap = "round"; - trailCtx.beginPath(); + hasLast = false; + ctx.beginPath(); for (let i = 0; i < len; i++) { - const x = left[i] * (w / scale) + w / 2; - const y = right[i] * (h / scale) + h / 2; + const x = left[i] * halfW + w / 2; + const y = right[i] * halfH + h / 2; if (!hasLast) { - trailCtx.moveTo(x, y); + ctx.moveTo(x, y); hasLast = true; } else { - trailCtx.moveTo(lastX, lastY); - trailCtx.lineTo(x, y); + ctx.moveTo(lastX, lastY); + ctx.lineTo(x, y); } lastX = x; lastY = y; } - trailCtx.stroke(); - trailCtx.globalAlpha = 1.0; - - // Composite trail onto visible canvas (fully transparent background) - ctx.clearRect(0, 0, w, h); - ctx.drawImage(trailCanvas, 0, 0); + ctx.stroke(); }, resize(width, height) { w = width; h = height; hasLast = false; - if (trailCanvas && trailCtx) { - trailCanvas.width = w; - trailCanvas.height = h; - } }, dispose() { if (canvas) canvas.style.transform = ""; ctx = null; canvas = null; - trailCtx = null; - trailCanvas = null; hasLast = false; }, }; diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index a7fac68..0f29419 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -346,7 +346,7 @@ export const Settings = () => { /> { settings.integratedSeekBar = checked;