Skip to content

Commit 063d81a

Browse files
committed
wip: scaling of labels
1 parent 8474080 commit 063d81a

File tree

3 files changed

+294
-13
lines changed

3 files changed

+294
-13
lines changed

packages/webui/src/client/styles/countdown/director.scss

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ $hold-status-color: $liveline-timecode-color;
6060
22em
6161
4fr
6262
6fr / #{'min(13vw, 27vh)'} auto;
63+
white-space: nowrap;
64+
6365

6466
.director-screen__body__segment-name {
6567
grid-row: 1;
@@ -72,6 +74,7 @@ $hold-status-color: $liveline-timecode-color;
7274
letter-spacing: 0%;
7375
vertical-align: middle;
7476
color: #fff;
77+
position: relative;
7578

7679
&.live {
7780
background: $general-live-color;
@@ -82,16 +85,18 @@ $hold-status-color: $liveline-timecode-color;
8285
background: $general-next-color;
8386
text-shadow: 0px 0px 6px #000000;
8487
}
85-
}
86-
.director-screen__body__segment__countdown {
87-
height: 100%;
88-
width: 30vw;
89-
color: #FF5218;
90-
float: right;
91-
text-align: right;
92-
padding-right: 10px;
93-
background: linear-gradient(90deg, rgba(223, 0, 0, 0) 0%, #DF0000 7.86%, rgba(116, 0, 0, 0.808) 16.21%, rgba(0, 0, 0, 0.6) 24.94%);
94-
88+
.director-screen__body__segment__countdown {
89+
position: absolute;
90+
top: 0;
91+
right: 0;
92+
height: 100%;
93+
width: 20vw;
94+
color: #FF5218;
95+
float: right;
96+
text-align: right;
97+
padding-right: 10px;
98+
background: linear-gradient(90deg, rgba(223, 0, 0, 0) 0%, #DF0000 7.86%, rgba(116, 0, 0, 0.808) 16.21%, rgba(0, 0, 0, 0.6) 24.94%);
99+
}
95100
}
96101

97102
.director-screen__body__rundown-countdown {

packages/webui/src/client/ui/ClockView/DirectorScreen.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
TimesSincePlannedEndComponent,
4343
TimeToPlannedEndComponent,
4444
} from '../../lib/Components/CounterComponents'
45+
import { AdjustLabelFit } from '../util/AdjustLabelFit'
4546

4647
interface SegmentUi extends DBSegment {
4748
items: Array<PartUi>
@@ -396,7 +397,15 @@ function DirectorScreenRender({
396397
live: currentSegment !== undefined,
397398
})}
398399
>
399-
<span>{currentSegment?.name}</span>
400+
<AdjustLabelFit
401+
label={currentSegment?.name || ''}
402+
width={'80vw'}
403+
fontFamily="Roboto"
404+
fontSize="1em"
405+
minLetterSpacing={0}
406+
useVariableFont={true}
407+
hardCutText={true}
408+
/>
400409
<span className="director-screen__body__segment__countdown">
401410
<CurrentPartOrSegmentRemaining
402411
currentPartInstanceId={playlist.currentPartInfo?.partInstanceId || null}
@@ -424,10 +433,9 @@ function DirectorScreenRender({
424433
playlistActivationId={playlist?.activationId}
425434
autowidth={{
426435
label: '',
427-
width: '80%',
436+
width: '90vw',
428437
fontFamily: 'Roboto Flex',
429438
fontSize: '2em',
430-
minWidthPercentage: 5,
431439
minLetterSpacing: 2,
432440
useVariableFont: true,
433441
}}
@@ -499,6 +507,14 @@ function DirectorScreenRender({
499507
showStyleBaseId={nextShowStyleBaseId}
500508
rundownIds={rundownIds}
501509
playlistActivationId={playlist?.activationId}
510+
autowidth={{
511+
label: '',
512+
width: '90vw',
513+
fontFamily: 'Roboto Flex',
514+
fontSize: '2em',
515+
minLetterSpacing: 2,
516+
useVariableFont: true,
517+
}}
502518
/>
503519
) : (
504520
'_'
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import React, { useEffect, useRef, CSSProperties } from 'react'
2+
3+
export interface AdjustLabelFitProps {
4+
/**
5+
* The text label to display and adjust
6+
*/
7+
label: string
8+
9+
/**
10+
* The available width for the text in any valid CSS width format (px, vw, %, etc.)
11+
* If not specified, it will use the parent container's width
12+
*/
13+
width?: string | number
14+
15+
/**
16+
* Optional font family (defaults to the parent element's font)
17+
*/
18+
fontFamily?: string
19+
20+
/**
21+
* Initial font size in any valid CSS unit (px, pt, rem, etc.)
22+
* Default is inherited from parent
23+
*/
24+
fontSize?: string | number
25+
26+
/**
27+
* Minimum font size in pixels (for auto-scaling)
28+
* Default is 10px
29+
*/
30+
minFontSize?: number
31+
32+
/**
33+
* Maximum font size in pixels (for auto-scaling)
34+
* Default is 100px
35+
*/
36+
maxFontSize?: number
37+
38+
/**
39+
* Minimum letter spacing in pixels
40+
* Default is -1px
41+
*/
42+
minLetterSpacing?: number
43+
44+
/**
45+
* Additional CSS styles for the container
46+
*/
47+
containerStyle?: CSSProperties
48+
49+
/**
50+
* Additional CSS styles for the label
51+
*/
52+
labelStyle?: CSSProperties
53+
54+
/**
55+
* Additional class name for the container
56+
*/
57+
className?: string
58+
59+
/**
60+
* Whether to use font variation settings for adjustment (requires variable font)
61+
* If false, will only use letter-spacing
62+
*/
63+
useVariableFont?: boolean
64+
65+
/**
66+
* Whether to adjust font size to fill the container width
67+
* Default is true
68+
*/
69+
adjustFontSize?: boolean
70+
71+
/**
72+
* Hard cut length of the text if it doesn't fit
73+
*/
74+
hardCutText?: boolean
75+
}
76+
77+
/**
78+
* A component that automatically adjusts text to fit within a specified width
79+
* using font size scaling, variable font width adjustment, and letter spacing.
80+
*/
81+
export const AdjustLabelFit: React.FC<AdjustLabelFitProps> = ({
82+
label,
83+
width,
84+
fontFamily,
85+
fontSize,
86+
minFontSize = 10,
87+
maxFontSize = 100,
88+
minLetterSpacing = -1,
89+
containerStyle = {},
90+
labelStyle = {},
91+
className = '',
92+
useVariableFont = true,
93+
adjustFontSize = true,
94+
hardCutText = false,
95+
}) => {
96+
const labelRef = useRef<HTMLSpanElement>(null)
97+
const containerRef = useRef<HTMLDivElement>(null)
98+
99+
// Convert to CSS values:
100+
const widthValue = typeof width === 'number' ? `${width}px` : width
101+
const fontSizeValue = typeof fontSize === 'number' ? `${fontSize}px` : fontSize
102+
const finalContainerStyle: CSSProperties = {
103+
display: 'block',
104+
overflow: 'hidden',
105+
...containerStyle,
106+
...(widthValue ? { width: widthValue } : {}),
107+
}
108+
109+
// Label style - add optional font settings
110+
const finalLabelStyle: CSSProperties = {
111+
display: 'inline-block',
112+
...labelStyle,
113+
...(fontFamily ? { fontFamily } : {}),
114+
...(fontSizeValue ? { fontSize: fontSizeValue } : {}),
115+
}
116+
117+
const adjustTextToFit = () => {
118+
const labelElement = labelRef.current
119+
const containerElement = containerRef.current
120+
121+
if (!labelElement || !containerElement) return
122+
123+
const DEFAULT_WIDTH = 100
124+
labelElement.style.letterSpacing = '0px'
125+
126+
if (useVariableFont) {
127+
labelElement.style.fontVariationSettings = `'wdth' ${DEFAULT_WIDTH}`
128+
}
129+
130+
// Reset label content if it was cut
131+
labelElement.textContent = label
132+
133+
// Reset font size to initial value if specified, or to computed style if not
134+
if (adjustFontSize) {
135+
if (fontSizeValue) {
136+
labelElement.style.fontSize = fontSizeValue
137+
} else {
138+
// Use computed style if no fontSize was specified
139+
const computedStyle = window.getComputedStyle(labelElement)
140+
const initialFontSize = computedStyle.fontSize
141+
labelElement.style.fontSize = initialFontSize
142+
}
143+
}
144+
145+
// Force reflow to ensure measurements are accurate
146+
void labelElement.offsetWidth
147+
148+
// Measure the container and text widths
149+
const containerWidth = containerElement.clientWidth
150+
const textWidth = labelElement.getBoundingClientRect().width
151+
152+
if (textWidth <= containerWidth) {
153+
// If text fits but we want to expand it to fill the width
154+
if (adjustFontSize) {
155+
const currentFontSize = parseFloat(window.getComputedStyle(labelElement).fontSize)
156+
const scaleFactor = containerWidth / textWidth
157+
const newFontSize = Math.min(currentFontSize * scaleFactor, maxFontSize)
158+
159+
labelElement.style.fontSize = `${newFontSize}px`
160+
161+
// Re-center text vertically if needed
162+
labelElement.style.lineHeight = '1'
163+
}
164+
return
165+
}
166+
167+
// Text doesn't fit - adjust size first if enabled
168+
if (adjustFontSize) {
169+
const currentFontSize = parseFloat(window.getComputedStyle(labelElement).fontSize)
170+
const scaleFactor = containerWidth / textWidth
171+
const newFontSize = Math.max(currentFontSize * scaleFactor, minFontSize)
172+
173+
labelElement.style.fontSize = `${newFontSize}px`
174+
175+
// Remeasure after font size adjustment
176+
void labelElement.offsetWidth
177+
const newTextWidth = labelElement.getBoundingClientRect().width
178+
179+
// If text now fits with font size adjustment alone, we're done
180+
if (newTextWidth <= containerWidth) return
181+
}
182+
183+
// Further adjustments if still needed
184+
if (useVariableFont) {
185+
const textWidth = labelElement.getBoundingClientRect().width
186+
const widthRatio = containerWidth / textWidth
187+
let currentWidth = DEFAULT_WIDTH * widthRatio
188+
189+
// Use a reasonable range for width variation
190+
currentWidth = Math.max(currentWidth, 75) // minimum 75%
191+
currentWidth = Math.min(currentWidth, 110) // maximum 110%
192+
193+
labelElement.style.fontVariationSettings = `'wdth' ${currentWidth}`
194+
195+
// Remeasure text width after adjustment:
196+
void labelElement.offsetWidth
197+
const adjustedTextWidth = labelElement.getBoundingClientRect().width
198+
199+
// Letter spacing if text still overflows
200+
if (adjustedTextWidth > containerWidth) {
201+
const overflow = adjustedTextWidth - containerWidth
202+
const letterCount = label.length - 1 // Spaces between letters
203+
let letterSpacing = letterCount > 0 ? -overflow / letterCount : 0
204+
205+
letterSpacing = Math.max(letterSpacing, minLetterSpacing)
206+
labelElement.style.letterSpacing = `${letterSpacing}px`
207+
208+
// Hard cut text if enabled and letterspacing is not enough:
209+
if (hardCutText) {
210+
void labelElement.offsetWidth
211+
const finalTextWidth = labelElement.getBoundingClientRect().width
212+
if (finalTextWidth > containerWidth) {
213+
const ratio = containerWidth / finalTextWidth
214+
const visibleChars = Math.floor(label.length * ratio) - 1
215+
labelElement.textContent = label.slice(0, Math.max(visibleChars, 1))
216+
}
217+
}
218+
}
219+
} else {
220+
// No variable font type
221+
const textWidth = labelElement.getBoundingClientRect().width
222+
const overflow = textWidth - containerWidth
223+
const letterCount = label.length - 1
224+
let letterSpacing = letterCount > 0 ? -overflow / letterCount : 0
225+
226+
// Limit by minLetterSpacing
227+
letterSpacing = Math.max(letterSpacing, minLetterSpacing)
228+
labelElement.style.letterSpacing = `${letterSpacing}px`
229+
230+
// Hard cut text if enabled and letterspacing is not enough:
231+
if (hardCutText) {
232+
void labelElement.offsetWidth
233+
const finalTextWidth = labelElement.getBoundingClientRect().width
234+
if (finalTextWidth > containerWidth) {
235+
const ratio = containerWidth / finalTextWidth
236+
const visibleChars = Math.floor(label.length * ratio) - 1
237+
labelElement.textContent = label.slice(0, Math.max(visibleChars, 1))
238+
}
239+
}
240+
}
241+
}
242+
243+
useEffect(() => {
244+
adjustTextToFit()
245+
246+
// Adjust on window resize
247+
window.addEventListener('resize', adjustTextToFit)
248+
return () => {
249+
window.removeEventListener('resize', adjustTextToFit)
250+
}
251+
}, [label, width, fontFamily, fontSize, minFontSize, maxFontSize, minLetterSpacing, useVariableFont, adjustFontSize])
252+
253+
return (
254+
<div ref={containerRef} className={`adjust-label-fit ${className}`} style={finalContainerStyle}>
255+
<span ref={labelRef} style={finalLabelStyle}>
256+
{label}
257+
</span>
258+
</div>
259+
)
260+
}

0 commit comments

Comments
 (0)