mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-10 18:03:47 +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 {
|
||||
height: 12.5vh;
|
||||
width: 70%;
|
||||
|
||||
font-family: monospace;
|
||||
font-weight: 400;
|
||||
color: var(--studio-active-label, $cyan-active);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Playback } from 'ontime-types';
|
||||
import { millisToString, removeSeconds, secondsInMillis } from 'ontime-utils';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/constants';
|
||||
import { FitText } from '../../../common/components/fit-text/FitText';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import useFitText from '../../../common/hooks/useFitText';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
|
||||
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
|
||||
@@ -32,15 +32,17 @@ interface StudioClockProps {
|
||||
export default function StudioClock(props: StudioClockProps) {
|
||||
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, viewSettings, settings } = props;
|
||||
|
||||
// TODO: can we prevent the Flash of Unstyled Content on the 7segment fonts?
|
||||
// deferring rendering seems to affect styling (font and useFitText)
|
||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ minFontSize: 150, maxFontSize: 500 });
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useWindowTitle('Studio Clock');
|
||||
|
||||
// defer rendering until we load stylesheets
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeIndicators = [...Array(12).keys()];
|
||||
const secondsIndicators = [...Array(60).keys()];
|
||||
|
||||
@@ -77,13 +79,7 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
<div className='clock-container'>
|
||||
{hasAmPm && <div className='clock__ampm'>{hasAmPm}</div>}
|
||||
<div className={`studio-timer ${!hideSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
||||
<div
|
||||
ref={titleRef}
|
||||
className='next-title'
|
||||
style={{ fontSize: titleFontSize, height: '12.5vh', width: '100%', maxWidth: '80%' }}
|
||||
>
|
||||
{eventNext?.title}
|
||||
</div>
|
||||
<FitText className='next-title'>{eventNext?.title}</FitText>
|
||||
<div
|
||||
className={`
|
||||
next-countdown ${isNegative ? ' next-countdown--overtime' : ''} ${isPaused ? ' next-countdown--paused' : ''}
|
||||
|
||||
Reference in New Issue
Block a user