mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-27 09:59:08 +00:00
refactor: migrate fit text logic
This commit is contained in:
committed by
Carlos Valente
parent
bc6b7c5596
commit
92a16ea33b
@@ -1,159 +0,0 @@
|
|||||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
export type TLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
|
||||||
|
|
||||||
export type TOptions = {
|
|
||||||
logLevel?: TLogLevel;
|
|
||||||
maxFontSize?: number;
|
|
||||||
minFontSize?: number;
|
|
||||||
onFinish?: (fontSize: number) => void;
|
|
||||||
onStart?: () => void;
|
|
||||||
resolution?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LOG_LEVEL: Record<TLogLevel, number> = {
|
|
||||||
debug: 10,
|
|
||||||
info: 20,
|
|
||||||
warn: 30,
|
|
||||||
error: 40,
|
|
||||||
none: 100,
|
|
||||||
};
|
|
||||||
|
|
||||||
const useFitText = ({
|
|
||||||
logLevel: logLevelOption = 'info',
|
|
||||||
maxFontSize = 100,
|
|
||||||
minFontSize = 20,
|
|
||||||
onFinish,
|
|
||||||
onStart,
|
|
||||||
resolution = 5,
|
|
||||||
}: TOptions = {}) => {
|
|
||||||
const logLevel = LOG_LEVEL[logLevelOption];
|
|
||||||
|
|
||||||
const initState = useCallback(() => {
|
|
||||||
return {
|
|
||||||
calcKey: 0,
|
|
||||||
fontSize: maxFontSize,
|
|
||||||
fontSizePrev: minFontSize,
|
|
||||||
fontSizeMax: maxFontSize,
|
|
||||||
fontSizeMin: minFontSize,
|
|
||||||
};
|
|
||||||
}, [maxFontSize, minFontSize]);
|
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const innerHtmlPrevRef = useRef<string | null>();
|
|
||||||
const isCalculatingRef = useRef(false);
|
|
||||||
const [state, setState] = useState(initState);
|
|
||||||
const { calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;
|
|
||||||
|
|
||||||
// Monitor div size changes and recalculate on resize
|
|
||||||
let animationFrameId: number | null = null;
|
|
||||||
const [ro] = useState(
|
|
||||||
() =>
|
|
||||||
new ResizeObserver(() => {
|
|
||||||
animationFrameId = window.requestAnimationFrame(() => {
|
|
||||||
if (isCalculatingRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onStart && onStart();
|
|
||||||
isCalculatingRef.current = true;
|
|
||||||
// `calcKey` is used in the dependencies array of
|
|
||||||
// `useIsoLayoutEffect` below. It is incremented so that the font size
|
|
||||||
// will be recalculated even if the previous state didn't change (e.g.
|
|
||||||
// when the text fit initially).
|
|
||||||
setState({
|
|
||||||
...initState(),
|
|
||||||
calcKey: calcKey + 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ref.current) {
|
|
||||||
ro.observe(ref.current);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
animationFrameId && window.cancelAnimationFrame(animationFrameId);
|
|
||||||
ro.disconnect();
|
|
||||||
};
|
|
||||||
}, [animationFrameId, ro]);
|
|
||||||
|
|
||||||
// Recalculate when the div contents change
|
|
||||||
const innerHtml = ref.current && ref.current.innerHTML;
|
|
||||||
useEffect(() => {
|
|
||||||
if (calcKey === 0 || isCalculatingRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (innerHtml !== innerHtmlPrevRef.current) {
|
|
||||||
onStart && onStart();
|
|
||||||
setState({
|
|
||||||
...initState(),
|
|
||||||
calcKey: calcKey + 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
innerHtmlPrevRef.current = innerHtml;
|
|
||||||
}, [calcKey, initState, innerHtml, onStart]);
|
|
||||||
|
|
||||||
// Check overflow and resize font
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
// Don't start calculating font size until the `resizeKey` is incremented
|
|
||||||
// above in the `ResizeObserver` callback. This avoids an extra resize
|
|
||||||
// on initialization.
|
|
||||||
if (calcKey === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;
|
|
||||||
const isOverflow =
|
|
||||||
!!ref.current &&
|
|
||||||
(ref.current.scrollHeight > ref.current.offsetHeight || ref.current.scrollWidth > ref.current.offsetWidth);
|
|
||||||
const isFailed = isOverflow && fontSize === fontSizePrev;
|
|
||||||
const isAsc = fontSize > fontSizePrev;
|
|
||||||
|
|
||||||
// Return if the font size has been adjusted "enough" (change within `resolution`)
|
|
||||||
// reduce font size by one increment if it's overflowing.
|
|
||||||
if (isWithinResolution) {
|
|
||||||
if (isFailed) {
|
|
||||||
isCalculatingRef.current = false;
|
|
||||||
if (logLevel <= LOG_LEVEL.info) {
|
|
||||||
console.info(`[use-fit-text] reached \`minFontSize = ${minFontSize}\` without fitting text`);
|
|
||||||
}
|
|
||||||
} else if (isOverflow) {
|
|
||||||
setState({
|
|
||||||
fontSize: isAsc ? fontSizePrev : fontSizeMin,
|
|
||||||
fontSizeMax,
|
|
||||||
fontSizeMin,
|
|
||||||
fontSizePrev,
|
|
||||||
calcKey,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
isCalculatingRef.current = false;
|
|
||||||
onFinish && onFinish(fontSize);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary search to adjust font size
|
|
||||||
let delta: number;
|
|
||||||
let newMax = fontSizeMax;
|
|
||||||
let newMin = fontSizeMin;
|
|
||||||
if (isOverflow) {
|
|
||||||
delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;
|
|
||||||
newMax = Math.min(fontSizeMax, fontSize);
|
|
||||||
} else {
|
|
||||||
delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;
|
|
||||||
newMin = Math.max(fontSizeMin, fontSize);
|
|
||||||
}
|
|
||||||
setState({
|
|
||||||
calcKey,
|
|
||||||
fontSize: fontSize + delta / 2,
|
|
||||||
fontSizeMax: newMax,
|
|
||||||
fontSizeMin: newMin,
|
|
||||||
fontSizePrev: fontSize,
|
|
||||||
});
|
|
||||||
}, [calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev, onFinish, ref, resolution]);
|
|
||||||
|
|
||||||
return { fontSize: `${fontSize}%`, ref };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useFitText;
|
|
||||||
@@ -120,6 +120,9 @@ $orange-active: #f60;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.next-title {
|
.next-title {
|
||||||
|
height: 12.5vh;
|
||||||
|
width: 70%;
|
||||||
|
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--studio-active-label, $cyan-active);
|
color: var(--studio-active-label, $cyan-active);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Playback } from 'ontime-types';
|
|||||||
import { millisToString, removeSeconds, secondsInMillis } from 'ontime-utils';
|
import { millisToString, removeSeconds, secondsInMillis } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/constants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
|
import { FitText } from '../../../common/components/fit-text/FitText';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import useFitText from '../../../common/hooks/useFitText';
|
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
|
||||||
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||||
@@ -32,15 +32,17 @@ interface StudioClockProps {
|
|||||||
export default function StudioClock(props: StudioClockProps) {
|
export default function StudioClock(props: StudioClockProps) {
|
||||||
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, viewSettings, settings } = props;
|
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, viewSettings, settings } = props;
|
||||||
|
|
||||||
// TODO: can we prevent the Flash of Unstyled Content on the 7segment fonts?
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
// deferring rendering seems to affect styling (font and useFitText)
|
|
||||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
|
||||||
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ minFontSize: 150, maxFontSize: 500 });
|
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
useWindowTitle('Studio Clock');
|
useWindowTitle('Studio Clock');
|
||||||
|
|
||||||
|
// defer rendering until we load stylesheets
|
||||||
|
if (!shouldRender) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const activeIndicators = [...Array(12).keys()];
|
const activeIndicators = [...Array(12).keys()];
|
||||||
const secondsIndicators = [...Array(60).keys()];
|
const secondsIndicators = [...Array(60).keys()];
|
||||||
|
|
||||||
@@ -77,13 +79,7 @@ export default function StudioClock(props: StudioClockProps) {
|
|||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
{hasAmPm && <div className='clock__ampm'>{hasAmPm}</div>}
|
{hasAmPm && <div className='clock__ampm'>{hasAmPm}</div>}
|
||||||
<div className={`studio-timer ${!hideSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
<div className={`studio-timer ${!hideSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
||||||
<div
|
<FitText className='next-title'>{eventNext?.title}</FitText>
|
||||||
ref={titleRef}
|
|
||||||
className='next-title'
|
|
||||||
style={{ fontSize: titleFontSize, height: '12.5vh', width: '100%', maxWidth: '80%' }}
|
|
||||||
>
|
|
||||||
{eventNext?.title}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
next-countdown ${isNegative ? ' next-countdown--overtime' : ''} ${isPaused ? ' next-countdown--paused' : ''}
|
next-countdown ${isNegative ? ' next-countdown--overtime' : ''} ${isPaused ? ' next-countdown--paused' : ''}
|
||||||
|
|||||||
Reference in New Issue
Block a user