diff --git a/.common/core/settings-core/script.js b/.common/core/settings-core/script.js index 6868574..f0f0e8c 100644 --- a/.common/core/settings-core/script.js +++ b/.common/core/settings-core/script.js @@ -337,9 +337,10 @@ function RefreshWidgetPreview() { } function UpdateStreamerBotConnection() { + // If Streamer.bot is not being used, exit early if (!useStreamerBot) return; - + let addressElement = document.getElementById('address'); let portElement = document.getElementById('port'); @@ -359,40 +360,43 @@ function UpdateStreamerBotConnection() { ////////////////// // Connect to Streamer.bot and get list of actions -let sbServerAddress = '127.0.0.1'; -let sbServerPort = '8080'; -let client = new StreamerbotClient({ - host: sbServerAddress, - port: sbServerPort, +if (useStreamerBot) +{ + let sbServerAddress = '127.0.0.1'; + let sbServerPort = '8080'; + let client = new StreamerbotClient({ + host: sbServerAddress, + port: sbServerPort, - onConnect: (data) => { - console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`) - console.debug(data); + onConnect: (data) => { + console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`) + console.debug(data); - // Get list of actions - GetSBActions(); - }, + // Get list of actions + GetSBActions(); + }, - onDisconnect: () => { - console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`) + onDisconnect: () => { + console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`) + } + }); + + async function GetSBActions() { + const response = await client.getActions(); + + console.debug(response); + + const datalistElement = document.createElement('datalist'); + datalistElement.id = 'streamer-bot-actions'; + + for (const action of response.actions) { + const option = document.createElement('option'); + option.value = action.name; + datalistElement.appendChild(option); + } + + document.body.appendChild(datalistElement); } -}); - -async function GetSBActions() { - const response = await client.getActions(); - - console.debug(response); - - const datalistElement = document.createElement('datalist'); - datalistElement.id = 'streamer-bot-actions'; - - for (const action of response.actions) { - const option = document.createElement('option'); - option.value = action.name; - datalistElement.appendChild(option); - } - - document.body.appendChild(datalistElement); } async function PopulateFontDatalist() { diff --git a/now-playing/script.js b/now-playing/script.js index 64aed80..78c8f2d 100644 --- a/now-playing/script.js +++ b/now-playing/script.js @@ -475,9 +475,6 @@ function SetVisibility(visible) { // This is a more advanced marquee implementation that calculates the overflow distance and adjusts the scroll speed accordingly const labelData = new Map(); -// Adjust this value to change scroll speed (pixels per second) -const SCROLL_SPEED_PX_PER_SEC = 40; - function SetLabelText(elementId, text) { const label = document.getElementById(elementId); if (!label) return; @@ -495,8 +492,11 @@ function SetLabelText(elementId, text) { } } +// Adjust this value to change scroll speed (pixels per second) +const SCROLL_SPEED_PX_PER_SEC = 30; +const PAUSE_SEC = 5; + function ApplyMarquee(label, text) { - // 1. Render single span to measure dimensions label.classList.remove('is-overflowing'); label.innerHTML = ``; label.querySelector('.scroll-content').textContent = text; @@ -507,20 +507,56 @@ function ApplyMarquee(label, text) { const containerWidth = label.clientWidth; const overflowDistance = textWidth - containerWidth; - // 2. Check if text exceeds container if (overflowDistance > 0) { - // Calculate travel distance in percentage relative to scrollContent's width const distancePercent = (overflowDistance / textWidth) * 100; - // Compute duration to keep speed consistent regardless of length - // We multiply travel time by 2 (for both directions) plus 3 seconds for pauses - const travelTime = overflowDistance / SCROLL_SPEED_PX_PER_SEC; - const totalDuration = (travelTime * 2) + 3; - - // Pass calculated variables to CSS - scrollContent.style.setProperty('--scroll-distance', `-${distancePercent}%`); - label.style.setProperty('--marquee-duration', `${totalDuration}s`); + // --- CONSTANT TIMINGS --- + const travelSec = overflowDistance / SCROLL_SPEED_PX_PER_SEC; + const pauseSec = PAUSE_SEC; // Always constant at each end + const totalSec = (travelSec * 2) + (pauseSec * 2); + const totalMs = totalSec * 1000; + + const p1 = (pauseSec / totalSec); + const p2 = ((pauseSec + travelSec) / totalSec); + const p3 = ((pauseSec * 2 + travelSec) / totalSec); + + scrollContent.style.setProperty('--scroll-distance', `-${distancePercent}%`); + + // 1. Animate Text Translation + scrollContent.animate([ + // Stay at start during pause 1 (linear/instant transition into movement) + { transform: 'translateX(0%)', offset: 0, easing: 'linear' }, + { transform: 'translateX(0%)', offset: p1, easing: 'ease-in-out' }, // <--- Easing starts HERE as it leaves the edge + + // Travel to the far end with smooth ease-in-out + { transform: `translateX(-${distancePercent}%)`, offset: p2, easing: 'linear' }, // <--- Easing ends HERE as it arrives + + // Stay at far end during pause 2 + { transform: `translateX(-${distancePercent}%)`, offset: p3, easing: 'ease-in-out' }, // <--- Easing starts HERE for return trip + + // Return to start with smooth ease-in-out + { transform: 'translateX(0%)', offset: 1, easing: 'linear' } // <--- Easing ends HERE back at start + ], { + duration: totalMs, + iterations: Infinity + }); + + // 2. Animate Mask Variables simultaneously to keep soft edges synced + label.animate([ + { '--mask-left': 'black', '--mask-right': 'transparent', offset: 0 }, + { '--mask-left': 'black', '--mask-right': 'transparent', offset: p1 }, + { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: p1 + 0.02 }, + { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: p2 - 0.02 }, + { '--mask-left': 'transparent', '--mask-right': 'black', offset: p2 }, + { '--mask-left': 'transparent', '--mask-right': 'black', offset: p3 }, + { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: p3 + 0.02 }, + { '--mask-left': 'black', '--mask-right': 'transparent', offset: 1 } + ], { + duration: totalMs, + iterations: Infinity + }); + label.classList.add('is-overflowing'); } } \ No newline at end of file diff --git a/now-playing/style.css b/now-playing/style.css index 1d6282c..de16278 100644 --- a/now-playing/style.css +++ b/now-playing/style.css @@ -183,13 +183,7 @@ body { will-change: transform; } -.text-label.is-overflowing .scroll-content { - animation: ping-pong-scroll var(--marquee-duration, 8s) ease-in-out infinite; -} - -.text-label.is-overflowing { - animation: ping-pong-mask var(--marquee-duration, 8s) ease-in-out infinite; - +.text-label.is-overflowing { /* Static gradient structure referencing dynamic color variables */ mask-image: linear-gradient( to right, @@ -205,48 +199,4 @@ body { black calc(100% - var(--fade-width)), var(--mask-right) 100% ); -} - -@keyframes ping-pong-scroll { - 0%, 15% { - transform: translateX(0%); - } - 50%, 65% { - transform: translateX(var(--scroll-distance, -100%)); - } - 100% { - transform: translateX(0%); - } -} - -@keyframes ping-pong-mask { - /* 1. AT START: Left is solid (black), Right is faded (transparent) */ - 0%, 15% { - --mask-left: black; - --mask-right: transparent; - } - - /* 2. MOVING LEFT: Left smoothly fades in (transparent), Right stays faded */ - 22%, 43% { - --mask-left: transparent; - --mask-right: transparent; - } - - /* 3. AT FAR END: Right smoothly solidifies (black), Left stays faded */ - 50%, 65% { - --mask-left: transparent; - --mask-right: black; - } - - /* 4. MOVING RIGHT: Right smoothly fades back out (transparent) */ - 72%, 93% { - --mask-left: transparent; - --mask-right: transparent; - } - - /* 5. RETURN TO START: Left smoothly solidifies (black) */ - 100% { - --mask-left: black; - --mask-right: transparent; - } } \ No newline at end of file diff --git a/now-playing/themes/compact/script.js b/now-playing/themes/compact/script.js index a8b736d..4b10cb0 100644 --- a/now-playing/themes/compact/script.js +++ b/now-playing/themes/compact/script.js @@ -145,7 +145,6 @@ function UpdateSingleSongLabel(containerId, trackName, artistName) { let scrollContent = container.querySelector('.scroll-content'); - // Only rebuild the HTML structure if the text content actually changed or doesn't exist yet const currentTrackEl = container.querySelector('.track-label'); const currentArtistEl = container.querySelector('.artist-label'); @@ -160,7 +159,6 @@ function UpdateSingleSongLabel(containerId, trackName, artistName) { scrollContent = container.querySelector('.scroll-content'); } - // Encapsulate measurement logic so it can be called safely by the observer const updateMetrics = () => { scrollContent = container.querySelector('.scroll-content'); @@ -170,20 +168,61 @@ function UpdateSingleSongLabel(containerId, trackName, artistName) { if (overflowDistance > 0) { const distancePercent = (overflowDistance / textWidth) * 100; - const travelTime = overflowDistance / SCROLL_SPEED_PX_PER_SEC; - const totalDuration = (travelTime * 2) + 3; + + // --- CONSTANT TIMINGS --- + const travelSec = overflowDistance / SCROLL_SPEED_PX_PER_SEC; + const pauseSec = PAUSE_SEC; + + const totalSec = (travelSec * 2) + (pauseSec * 2); + const totalMs = totalSec * 1000; + + const p1 = (pauseSec / totalSec); + const p2 = ((pauseSec + travelSec) / totalSec); + const p3 = ((pauseSec * 2 + travelSec) / totalSec); + + scrollContent.style.setProperty('--scroll-distance', `-${distancePercent}%`); + + // Clear any previous active animations to prevent stacking on resize + if (scrollContent._anim) scrollContent._anim.cancel(); + if (container._anim) container._anim.cancel(); + + // 1. Animate Text Translation inside container + scrollContent._anim = scrollContent.animate([ + { transform: 'translateX(0%)', offset: 0, easing: 'linear' }, + { transform: 'translateX(0%)', offset: p1, easing: 'ease-in-out' }, + { transform: `translateX(-${distancePercent}%)`, offset: p2, easing: 'linear' }, + { transform: `translateX(-${distancePercent}%)`, offset: p3, easing: 'ease-in-out' }, + { transform: 'translateX(0%)', offset: 1, easing: 'linear' } + ], { + duration: totalMs, + iterations: Infinity + }); + + // 2. Animate Mask Variables simultaneously on the container + container._anim = container.animate([ + { '--mask-left': 'black', '--mask-right': 'transparent', offset: 0 }, + { '--mask-left': 'black', '--mask-right': 'transparent', offset: p1 }, + { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: p1 + 0.02 }, + { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: p2 - 0.02 }, + { '--mask-left': 'transparent', '--mask-right': 'black', offset: p2 }, + { '--mask-left': 'transparent', '--mask-right': 'black', offset: p3 }, + { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: p3 + 0.02 }, + { '--mask-left': 'black', '--mask-right': 'transparent', offset: 1 } + ], { + duration: totalMs, + iterations: Infinity + }); - container.style.setProperty('--scroll-distance', `-${distancePercent}%`); - container.style.setProperty('--marquee-duration', `${totalDuration}s`); container.classList.add('is-overflowing'); } else { container.classList.remove('is-overflowing'); + if (scrollContent._anim) scrollContent._anim.cancel(); + if (container._anim) container._anim.cancel(); } }; updateMetrics(); - // Attach ResizeObserver once to update metrics only (No recursive function resets!) if (!container._resizeObserver) { container._resizeObserver = new ResizeObserver(() => { updateMetrics(); diff --git a/now-playing/themes/compact/style.css b/now-playing/themes/compact/style.css index 9b87c0c..31b651d 100644 --- a/now-playing/themes/compact/style.css +++ b/now-playing/themes/compact/style.css @@ -87,16 +87,4 @@ font-weight: 400; opacity: 0.7; display: var(--show-secondary); -} - -/* Marquee scroll animation */ -#song-label.is-overflowing .scroll-content, -#song-label-background.is-overflowing .scroll-content { - animation: ping-pong-scroll var(--marquee-duration, 8s) ease-in-out infinite; -} - -/* Mask animation applied to container */ -#song-label.is-overflowing, -#song-label-background.is-overflowing { - animation: ping-pong-mask var(--marquee-duration, 8s) ease-in-out infinite; } \ No newline at end of file