Merge pull request #32 from nuttylmao/beta

Beta
This commit is contained in:
nutty
2026-08-18 02:19:17 +10:00
committed by GitHub
8 changed files with 169 additions and 131 deletions
+34 -30
View File
@@ -337,6 +337,7 @@ function RefreshWidgetPreview() {
} }
function UpdateStreamerBotConnection() { function UpdateStreamerBotConnection() {
// If Streamer.bot is not being used, exit early
if (!useStreamerBot) if (!useStreamerBot)
return; return;
@@ -359,40 +360,43 @@ function UpdateStreamerBotConnection() {
////////////////// //////////////////
// Connect to Streamer.bot and get list of actions // Connect to Streamer.bot and get list of actions
let sbServerAddress = '127.0.0.1'; if (useStreamerBot)
let sbServerPort = '8080'; {
let client = new StreamerbotClient({ let sbServerAddress = '127.0.0.1';
host: sbServerAddress, let sbServerPort = '8080';
port: sbServerPort, let client = new StreamerbotClient({
host: sbServerAddress,
port: sbServerPort,
onConnect: (data) => { onConnect: (data) => {
console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`) console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`)
console.debug(data); console.debug(data);
// Get list of actions // Get list of actions
GetSBActions(); GetSBActions();
}, },
onDisconnect: () => { onDisconnect: () => {
console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`) 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() { async function PopulateFontDatalist() {
+3 -2
View File
@@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<head> <head>
<title>nutty | Clocko</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="shortcut icon" href="#" /> <link rel="shortcut icon" href="#" />
<link rel="stylesheet" href="./style.css" /> <link rel="stylesheet" href="./style.css" />
@@ -19,6 +20,6 @@
<script src="/.common/utils/helpers.js"></script> <script src="/.common/utils/helpers.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/utc.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/utc.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/timezone.min.js"></script>' <script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/timezone.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/advancedFormat.js"></script>' <script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/advancedFormat.js"></script>
<script src="./script.js"></script> <script src="./script.js"></script>
+3 -3
View File
@@ -52,7 +52,7 @@
{ "value": "800", "label": "Extra Bold" }, { "value": "800", "label": "Extra Bold" },
{ "value": "900", "label": "Black" } { "value": "900", "label": "Black" }
], ],
"defaultValue": "700", "defaultValue": "800",
"showIf": "enableLine1", "showIf": "enableLine1",
"group": "Appearance" "group": "Appearance"
}, },
@@ -119,7 +119,7 @@
"label": "Format", "label": "Format",
"description": "<a href='https://day.js.org/docs/en/display/format' target='_blank'>Click here for formatting options</a>", "description": "<a href='https://day.js.org/docs/en/display/format' target='_blank'>Click here for formatting options</a>",
"type": "text", "type": "text",
"defaultValue": "ddd, MMM D", "defaultValue": "ddd D MMM YY",
"showIf": "enableLine2", "showIf": "enableLine2",
"group": "Appearance" "group": "Appearance"
}, },
@@ -168,7 +168,7 @@
"label": "Font Opacity", "label": "Font Opacity",
"description": "", "description": "",
"type": "number", "type": "number",
"defaultValue": "0.7", "defaultValue": "0.9",
"min": 0, "min": 0,
"max": 1, "max": 1,
"step": ".01", "step": ".01",
+61 -12
View File
@@ -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 // This is a more advanced marquee implementation that calculates the overflow distance and adjusts the scroll speed accordingly
const labelData = new Map(); 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) { function SetLabelText(elementId, text) {
const label = document.getElementById(elementId); const label = document.getElementById(elementId);
if (!label) return; 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) { function ApplyMarquee(label, text) {
// 1. Render single span to measure dimensions
label.classList.remove('is-overflowing'); label.classList.remove('is-overflowing');
label.innerHTML = `<span class="scroll-content"></span>`; label.innerHTML = `<span class="scroll-content"></span>`;
label.querySelector('.scroll-content').textContent = text; label.querySelector('.scroll-content').textContent = text;
@@ -507,19 +507,68 @@ function ApplyMarquee(label, text) {
const containerWidth = label.clientWidth; const containerWidth = label.clientWidth;
const overflowDistance = textWidth - containerWidth; const overflowDistance = textWidth - containerWidth;
// 2. Check if text exceeds container
if (overflowDistance > 0) { if (overflowDistance > 0) {
// Calculate travel distance in percentage relative to scrollContent's width
const distancePercent = (overflowDistance / textWidth) * 100; const distancePercent = (overflowDistance / textWidth) * 100;
// Compute duration to keep speed consistent regardless of length // --- CONSTANT TIMINGS ---
// We multiply travel time by 2 (for both directions) plus 3 seconds for pauses const travelSec = overflowDistance / SCROLL_SPEED_PX_PER_SEC;
const travelTime = overflowDistance / SCROLL_SPEED_PX_PER_SEC; const pauseSec = PAUSE_SEC; // Always constant at each end
const totalDuration = (travelTime * 2) + 3;
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);
// Pass calculated variables to CSS
scrollContent.style.setProperty('--scroll-distance', `-${distancePercent}%`); scrollContent.style.setProperty('--scroll-distance', `-${distancePercent}%`);
label.style.setProperty('--marquee-duration', `${totalDuration}s`);
// 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'); label.classList.add('is-overflowing');
} }
-50
View File
@@ -183,13 +183,7 @@ body {
will-change: transform; 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 { .text-label.is-overflowing {
animation: ping-pong-mask var(--marquee-duration, 8s) ease-in-out infinite;
/* Static gradient structure referencing dynamic color variables */ /* Static gradient structure referencing dynamic color variables */
mask-image: linear-gradient( mask-image: linear-gradient(
to right, to right,
@@ -206,47 +200,3 @@ body {
var(--mask-right) 100% 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;
}
}
+2 -2
View File
@@ -81,8 +81,8 @@
#progress-bar { #progress-bar {
height: 1%; height: 1%;
width: 0%; /* JS will control this */ width: 0%; /* JS will control this */
background: linear-gradient(to top, #ffffff00, var(--accent-color, #ffffff)); /* background: linear-gradient(to top, #ffffff00, var(--accent-color, #ffffff)); */
/* background-color: var(--accent-color, #ffffff); */ background-color: var(--accent-color, #ffffff);
transition: width 1s ease-in-out, background 1s ease-in-out; transition: width 1s ease-in-out, background 1s ease-in-out;
/* border-radius: 10000px; */ /* border-radius: 10000px; */
position: absolute; position: absolute;
+62 -16
View File
@@ -102,19 +102,14 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) {
// Ensure we don't divide by zero or exceed 100% // Ensure we don't divide by zero or exceed 100%
const durationMs = timelineProps.EndTime; 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 // Calculate the progress percentage
let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0; let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0;
progressPercent = Math.min(100, Math.max(0, progressPercent)); 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) // Calculate how much needs to be hidden (the right-side offset)
const clipRight = 100 - progressPercent; const clipRight = 100 - progressPercent;
@@ -145,7 +140,6 @@ function UpdateSingleSongLabel(containerId, trackName, artistName) {
let scrollContent = container.querySelector('.scroll-content'); 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 currentTrackEl = container.querySelector('.track-label');
const currentArtistEl = container.querySelector('.artist-label'); const currentArtistEl = container.querySelector('.artist-label');
@@ -160,7 +154,6 @@ function UpdateSingleSongLabel(containerId, trackName, artistName) {
scrollContent = container.querySelector('.scroll-content'); scrollContent = container.querySelector('.scroll-content');
} }
// Encapsulate measurement logic so it can be called safely by the observer
const updateMetrics = () => { const updateMetrics = () => {
scrollContent = container.querySelector('.scroll-content'); scrollContent = container.querySelector('.scroll-content');
@@ -170,20 +163,73 @@ function UpdateSingleSongLabel(containerId, trackName, artistName) {
if (overflowDistance > 0) { if (overflowDistance > 0) {
const distancePercent = (overflowDistance / textWidth) * 100; const distancePercent = (overflowDistance / textWidth) * 100;
const travelTime = overflowDistance / SCROLL_SPEED_PX_PER_SEC;
const totalDuration = (travelTime * 2) + 3;
container.style.setProperty('--scroll-distance', `-${distancePercent}%`); // --- CONSTANT TIMINGS ---
container.style.setProperty('--marquee-duration', `${totalDuration}s`); 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.classList.add('is-overflowing'); container.classList.add('is-overflowing');
} else { } else {
container.classList.remove('is-overflowing'); container.classList.remove('is-overflowing');
if (scrollContent._anim) scrollContent._anim.cancel();
if (container._anim) container._anim.cancel();
} }
}; };
updateMetrics(); updateMetrics();
// Attach ResizeObserver once to update metrics only (No recursive function resets!)
if (!container._resizeObserver) { if (!container._resizeObserver) {
container._resizeObserver = new ResizeObserver(() => { container._resizeObserver = new ResizeObserver(() => {
updateMetrics(); updateMetrics();
-12
View File
@@ -88,15 +88,3 @@
opacity: 0.7; opacity: 0.7;
display: var(--show-secondary); 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;
}