webamp/js/components/MainWindow/Marquee.tsx
Jordan Eldredge e7c474e7f6 Inline style
2019-11-30 13:42:53 -08:00

154 lines
4.5 KiB
TypeScript

// Single line text display that can animate and hold multiple registers
// Knows how to display various modes like tracking, volume, balance, etc.
import React from "react";
import CharacterString from "../CharacterString";
import * as Actions from "../../actionCreators";
import * as Selectors from "../../selectors";
import { useTypedSelector, useActionCreator } from "../../hooks";
const SEPARATOR = " *** ";
const CHAR_WIDTH = 5;
const MARQUEE_MAX_LENGTH = 31;
// Always positive modulus
export const mod = (n: number, m: number): number => ((n % m) + m) % m;
const isLong = (text: string): boolean => text.length >= MARQUEE_MAX_LENGTH;
// Given text and step, how many pixels should it be shifted?
export const stepOffset = (
text: string,
step: number,
pixels: number
): number => {
if (!isLong(text)) {
return 0;
}
const stepOffsetWidth = step * CHAR_WIDTH; // Steps move one char at a time
const offset = stepOffsetWidth + pixels;
const stringLength = (text.length + SEPARATOR.length) * CHAR_WIDTH;
return mod(offset, stringLength);
};
// Format an int as pixels
export const pixelUnits = (pixels: number): string => `${pixels}px`;
// If text is wider than the marquee, it needs to loop
export const loopText = (text: string): string =>
isLong(text)
? `${text}${SEPARATOR}${text}`
: text.padEnd(MARQUEE_MAX_LENGTH, " ");
interface UseStepperArgs {
step: () => void;
dragging: boolean;
}
// Call `step` every second, except when dragging. Resume stepping 1 second after dragging ceases.
function useStepper({ step, dragging }: UseStepperArgs): void {
const [stepping, setStepping] = React.useState(true);
React.useEffect(() => {
if (stepping === false) {
return;
}
const stepHandle = setInterval(step, 220);
return () => clearInterval(stepHandle);
}, [step, stepping]);
React.useEffect(() => {
if (dragging) {
setStepping(false);
return;
}
const steppingTimeout = window.setTimeout(() => {
setStepping(true);
}, 1000);
return () => {
window.clearTimeout(steppingTimeout);
};
}, [dragging]);
}
// When user calls `handleMouseDown`, and moves the mouse, `dragOffset` will update as they drag.
function useDragX() {
const [mouseDownX, setMouseDownX] = React.useState<number | null>(null);
const [dragOffset, setDragOffset] = React.useState(0);
React.useEffect(() => {
if (mouseDownX == null) {
return;
}
const xStart = mouseDownX;
const handleMouseMove = (ee: MouseEvent) => {
const diff = ee.clientX - xStart;
setDragOffset(-diff);
};
// TODO: Use `once` or something instead of this flag nonsense
let cleanedUp = false;
const handleMouseUp = () => {
if (cleanedUp) {
return;
}
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseUp", handleMouseUp);
setMouseDownX(null);
cleanedUp = true;
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return handleMouseUp;
}, [mouseDownX]);
const handleMouseDown = React.useCallback(
(e: React.MouseEvent<HTMLDivElement>): void => {
setMouseDownX(e.clientX);
},
[]
);
return { handleMouseDown, dragOffset, dragging: mouseDownX != null };
}
const Marquee = React.memo(() => {
const text = useTypedSelector(Selectors.getMarqueeText);
const doubled = useTypedSelector(Selectors.getDoubled);
const marqueeStep = useTypedSelector(Selectors.getMarqueeStep);
const stepMarquee = useActionCreator(Actions.stepMarquee);
const { handleMouseDown, dragOffset, dragging } = useDragX();
const offset = stepOffset(text, marqueeStep, dragOffset);
const offsetPixels = pixelUnits(-offset);
useStepper({ step: stepMarquee, dragging });
return (
<div
id="marquee"
className="text"
onMouseDown={handleMouseDown}
title="Song Title"
>
<div
style={{
whiteSpace: "nowrap",
willChange: "transform",
transform: `translateX(${offsetPixels})`,
}}
// Force the DOM node to be recreated when the doubled size changes.
// This works around a Chrome browser bug where the `will-change: transform;`
// on this node seems to cause a change to the `image-rendering:
// pixelated;` which we inherit from `#webamp` not to be respected.
key={doubled ? "doubled" : "not-doubled"}
>
<CharacterString>{loopText(text)}</CharacterString>
</div>
</div>
);
});
export default Marquee;