// 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(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): 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 (
{loopText(text)}
); }); export default Marquee;