Skip to content

Commit

Permalink
feat: add marquee effect (#337)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerixyz authored Aug 4, 2023
1 parent d160b55 commit 810f5b2
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 8 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CurrentSong 2

![example screenshot with default theme](https://i.imgur.com/qAs7xXh.png)
![example screenshot with default theme](https://github.com/Nerixyz/current-song2/assets/19953266/9b2ac5cd-4135-4eea-8383-bc738c865da9)

_For more examples, look at the [example themes](themes)!_

Expand Down
2 changes: 2 additions & 0 deletions js/client/src/dom/animation.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
export function animateOnChange(
el: HTMLElement,
updated: string,
resetAnimations: () => void,
keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
options?: number | KeyframeAnimationOptions,
) {
if (el.textContent !== updated) {
resetAnimations();
el.textContent = updated;
el.animate(keyframes, options);
}
Expand Down
58 changes: 53 additions & 5 deletions js/client/src/index.css
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
:root {
--theme-color: #c3f831;
--theme-color: #c7cedb;
--text-color: #000;
--font: 'Arial', sans;
--shadow-color: #0008;
--shadow-color: #0006;

--rounded: 10px;
/*--max-width: 40rem;*/
--max-width: 30rem;
--height: 4.4rem;
--max-height: 4.4rem;
--min-height: 3rem;

--container-shadow: 0 2px 22px 0 var(--shadow-color);

--progress-height: 0.4rem;
--progress-color: #007e7a;
--progress-color: #2e3532;
--progress-border-radius: 3px;
--progress-shadow: 0 0 3px 0 var(--shadow-color);

--use-marquee: true;
--marquee-speed: 0.2;
--marquee-pause-duration: 1200;
--marquee-repeat-pause-duration: 5000;
}

* {
box-sizing: border-box;
}

body {
Expand All @@ -34,6 +45,7 @@ body {
transition: 250ms cubic-bezier(0.33, 1, 0.68, 1);
transition-property: opacity, transform;
transform-origin: left;
box-shadow: var(--container-shadow);
}

#song-container.with-image {
Expand All @@ -50,6 +62,7 @@ body {
justify-content: center;
padding: 0.25rem 1rem 0.3rem 1rem;
white-space: nowrap;
min-width: 1;
}

h1,
Expand All @@ -65,6 +78,7 @@ h1 {
}
h2 {
font-size: large;
font-weight: normal;
}

#progress {
Expand All @@ -82,10 +96,11 @@ h2 {
}

#image-container {
flex-shrink: 1;
flex: 1 1;
height: 70%;
max-height: 100%;
max-width: 100%;
min-width: fit-content;
margin: auto 0 auto 1rem;
overflow: hidden;
box-shadow: 0 2px 10px 0 var(--shadow-color);
Expand Down Expand Up @@ -121,3 +136,36 @@ img.spotify {
.hidden {
display: none;
}

/* marquee */
.mq-wrap {
--content-padding: 6px;
width: 100%;
}

.mq-mask {
margin-left: -6px;
margin-right: -6px;
mask-image: linear-gradient(
90deg,
transparent 0,
#000 var(--content-padding),
#000 calc(100% - 2 * var(--content-padding)),
transparent
);
overflow: hidden;
position: relative;
}

.mq-overflow-guard {
overflow: hidden;
}

.mq-user-wrap {
display: flex;
white-space: nowrap;
width: fit-content;
padding-inline-end: calc(2 * var(--content-padding));
padding-inline-start: var(--content-padding);
transform: translateX(var(--marquee));
}
49 changes: 47 additions & 2 deletions js/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,46 @@ import {
ReconnectingWebsocket,
} from '../../shared/reconnecting-websocket';
import { startUserScript } from './user-scripts';
import { MarqueeEl, MarqueeOptions, wrapMarquee } from './text/marquee';

function wrapMarqueeElements(
root: HTMLElement,
titleEl: HTMLElement,
subtitleEl: HTMLElement,
): MarqueeEl {
const style = getComputedStyle(root);
const useIt = style.getPropertyValue('--use-marquee') === 'true';
if (!useIt) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return { pause() {}, reset() {}, start() {} };
}
const opt = (name: string, defaultValue: number) => {
const parsed = parseInt(style.getPropertyValue(name));
return Number.isNaN(parsed) ? defaultValue : parsed;
};
const opts: MarqueeOptions = {
speed: opt('--marquee-speed', 0.2),
pauseDuration: opt('--marquee-pause-duration', 1200),
repeatPauseDuration: opt('--marquee-repeat-pause-duration', 2000),
};
const title = wrapMarquee(titleEl, opts);
const subtitle = wrapMarquee(subtitleEl, opts);

return {
pause: () => {
title.pause();
subtitle.pause();
},
start: () => {
title.start();
subtitle.start();
},
reset: () => {
title.reset();
subtitle.reset();
},
};
}

(async function main() {
const [container, imageContainer, imageEl, titleEl, subtitleEl, progressEl] = getElements<
Expand All @@ -34,6 +74,7 @@ import { startUserScript } from './user-scripts';
HTMLDivElement,
]
>('song-container', 'image-container', 'image', 'title', 'subtitle', 'progress');
const resetMarquee = wrapMarqueeElements(container, titleEl, subtitleEl);

const progressManager = createProgress(progressEl);

Expand Down Expand Up @@ -63,9 +104,12 @@ import { startUserScript } from './user-scripts';
container.classList.remove('vanish');
const state = makeState(data);
tree.update(state);
resetMarquee.start();

animateOnChange(titleEl, state.title, ...TextChangeAnimation);
if (state.subtitle) animateOnChange(subtitleEl, state.subtitle, ...TextChangeAnimation);
animateOnChange(titleEl, state.title, resetMarquee.reset, ...TextChangeAnimation);
if (state.subtitle) {
animateOnChange(subtitleEl, state.subtitle, resetMarquee.reset, ...TextChangeAnimation);
}

if (state.imageUrl) {
imageEl.src = state.imageUrl;
Expand Down Expand Up @@ -93,6 +137,7 @@ import { startUserScript } from './user-scripts';
progressManager.pause();

userScript.onPause();
resetMarquee.pause();
});
await ws.connect();
})();
103 changes: 103 additions & 0 deletions js/client/src/text/marquee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
export interface MarqueeOptions {
speed: number;
pauseDuration: number;
repeatPauseDuration: number;
}

export interface MarqueeEl {
pause: () => void;
start: () => void;
reset: () => void;
}

export function wrapMarquee(el: HTMLElement, opts: MarqueeOptions): MarqueeEl {
const wrap = make('div', 'mq-wrap');
wrap.id = `mq-${el.id}`;
const mask = make('div', 'mq-mask');
const guard = make('div', 'mq-overflow-guard');
const userWrap = make('div', 'mq-user-wrap');

const parent = el.parentElement ?? document.documentElement;
const sibling = el.nextSibling;
el.remove();
parent.insertBefore(wrap, sibling);

wrap.appendChild(mask);
mask.appendChild(guard);
guard.appendChild(userWrap);
userWrap.appendChild(el);

const diffWidth = () => userWrap.clientWidth - mask.clientWidth;

let cbID: null | number = null;
let lastTime = performance.now();
let pos = 0;
let waitFrom = lastTime;
let dir = 1;
const applyPos = () => userWrap.style.setProperty('--marquee', `${-pos}px`);

const actualAnimationFrame = (time: number) => {
const deltaTime = time - lastTime;
lastTime = time;
const overflow = diffWidth();

if (overflow <= 0) {
if (pos != 0) {
pos = 0;
applyPos();
}
return;
}

if (waitFrom > 0) {
if (time > waitFrom + opts.pauseDuration) {
waitFrom = 0;
}
} else {
pos += dir * ((60 * deltaTime) / 1000) * opts.speed;
if (pos > overflow) {
dir *= -1;
waitFrom = time;
pos = overflow;
} else if (pos < 0) {
dir *= -1;
waitFrom = time + opts.repeatPauseDuration;
pos = 0;
}
}
applyPos();
};
const onAnimationFrame = (time: number) => {
actualAnimationFrame(time);
if (cbID) {
cbID = requestAnimationFrame(onAnimationFrame);
}
};

return {
start: () => {
if (!cbID) {
lastTime = performance.now();
cbID = requestAnimationFrame(onAnimationFrame);
}
},
pause: () => {
if (cbID) {
cancelAnimationFrame(cbID);
cbID = null;
}
},
reset: () => {
dir = 1;
pos = 0;
waitFrom = lastTime;
applyPos();
},
};
}

function make(ty: keyof HTMLElementTagNameMap, ...classes: string[]): HTMLElement {
const el = document.createElement(ty);
el.classList.add(...classes);
return el;
}

0 comments on commit 810f5b2

Please sign in to comment.