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/clocko/index.html b/clocko/index.html
index 8078048..f58af89 100644
--- a/clocko/index.html
+++ b/clocko/index.html
@@ -1,6 +1,7 @@
+ nutty | Clocko
@@ -19,6 +20,6 @@
-'
-'
+
+
\ No newline at end of file
diff --git a/clocko/settings/settings.json b/clocko/settings/settings.json
index bf27ef7..de49bb5 100644
--- a/clocko/settings/settings.json
+++ b/clocko/settings/settings.json
@@ -52,7 +52,7 @@
{ "value": "800", "label": "Extra Bold" },
{ "value": "900", "label": "Black" }
],
- "defaultValue": "700",
+ "defaultValue": "800",
"showIf": "enableLine1",
"group": "Appearance"
},
@@ -119,7 +119,7 @@
"label": "Format",
"description": "Click here for formatting options",
"type": "text",
- "defaultValue": "ddd, MMM D",
+ "defaultValue": "ddd D MMM YY",
"showIf": "enableLine2",
"group": "Appearance"
},
@@ -168,7 +168,7 @@
"label": "Font Opacity",
"description": "",
"type": "number",
- "defaultValue": "0.7",
+ "defaultValue": "0.9",
"min": 0,
"max": 1,
"step": ".01",
diff --git a/now-playing/script.js b/now-playing/script.js
index 64aed80..f910ced 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,69 @@ 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
+ const maxOffset = 1;
+ // Dynamically scale buffer so it never overlaps text segments if overflow is small
+ const fadeBuffer = Math.min(0.02, (p2 - p1) / 4);
+
+ const off0 = 0;
+ const off1 = p1;
+ const off2 = Math.min(p1 + fadeBuffer, p2);
+ const off3 = Math.max(off2, p2 - fadeBuffer);
+ const off4 = p2;
+ const off5 = p3;
+ const off6 = Math.min(p3 + fadeBuffer, maxOffset);
+ const off7 = 1;
+
+ label._anim = label.animate([
+ { '--mask-left': 'black', '--mask-right': 'transparent', offset: off0 },
+ { '--mask-left': 'black', '--mask-right': 'transparent', offset: off1 },
+ { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: off2 },
+ { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: off3 },
+ { '--mask-left': 'transparent', '--mask-right': 'black', offset: off4 },
+ { '--mask-left': 'transparent', '--mask-right': 'black', offset: off5 },
+ { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: off6 },
+ { '--mask-left': 'black', '--mask-right': 'transparent', offset: off7 }
+ ], {
+ 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/album-art/style.css b/now-playing/themes/album-art/style.css
index 06ddf7b..bd86dd2 100644
--- a/now-playing/themes/album-art/style.css
+++ b/now-playing/themes/album-art/style.css
@@ -81,8 +81,8 @@
#progress-bar {
height: 1%;
width: 0%; /* JS will control this */
- background: linear-gradient(to top, #ffffff00, var(--accent-color, #ffffff));
- /* background-color: var(--accent-color, #ffffff); */
+ /* background: linear-gradient(to top, #ffffff00, var(--accent-color, #ffffff)); */
+ background-color: var(--accent-color, #ffffff);
transition: width 1s ease-in-out, background 1s ease-in-out;
/* border-radius: 10000px; */
position: absolute;
diff --git a/now-playing/themes/compact/script.js b/now-playing/themes/compact/script.js
index a8b736d..7945afb 100644
--- a/now-playing/themes/compact/script.js
+++ b/now-playing/themes/compact/script.js
@@ -102,19 +102,14 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
// Ensure we don't divide by zero or exceed 100%
const durationMs = timelineProps.EndTime;
- // If the duration is 0, that means the session isn't returning any timeline info, so just put the progress at 100%
- if (durationMs <= 0 || !showProgressBar)
- {
- progressBarTrack.style.display = 'none';
- return;
- }
- else
- progressBarTrack.style.display = '';
-
// Calculate the progress percentage
let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
progressPercent = Math.min(100, Math.max(0, progressPercent));
+ // If the duration is 0, that means the session isn't returning any timeline info, so just put the progress at 100%
+ if (durationMs <= 0 || !showProgressBar)
+ progressPercent = 100;
+
// Calculate how much needs to be hidden (the right-side offset)
const clipRight = 100 - progressPercent;
@@ -145,7 +140,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 +154,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 +163,73 @@ 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 to keep soft edges synced
+ const maxOffset = 1;
+ const fadeBuffer = Math.min(0.02, (p2 - p1) / 4); // Dynamically scale buffer if text overflow is tiny
+
+ const off0 = 0;
+ const off1 = p1;
+ const off2 = Math.min(p1 + fadeBuffer, p2);
+ const off3 = Math.max(off2, p2 - fadeBuffer);
+ const off4 = p2;
+ const off5 = p3;
+ const off6 = Math.min(p3 + fadeBuffer, maxOffset);
+ const off7 = 1;
+
+ container._anim = container.animate([
+ { '--mask-left': 'black', '--mask-right': 'transparent', offset: off0 },
+ { '--mask-left': 'black', '--mask-right': 'transparent', offset: off1 },
+ { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: off2 },
+ { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: off3 },
+ { '--mask-left': 'transparent', '--mask-right': 'black', offset: off4 },
+ { '--mask-left': 'transparent', '--mask-right': 'black', offset: off5 },
+ { '--mask-left': 'transparent', '--mask-right': 'transparent', offset: off6 },
+ { '--mask-left': 'black', '--mask-right': 'transparent', offset: off7 }
+ ], {
+ 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