-
Notifications
You must be signed in to change notification settings - Fork 4k
/
fluid-utils.js
221 lines (193 loc) · 7.74 KB
/
fluid-utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/**
* The fluid utilities must match the backend equivalent.
* See: gutenberg_get_typography_font_size_value() in lib/block-supports/typography.php
* ---------------------------------------------------------------
*/
// Defaults.
const DEFAULT_MAXIMUM_VIEWPORT_WIDTH = '1600px';
const DEFAULT_MINIMUM_VIEWPORT_WIDTH = '768px';
const DEFAULT_SCALE_FACTOR = 1;
const DEFAULT_MINIMUM_FONT_SIZE_FACTOR = 0.75;
const DEFAULT_MAXIMUM_FONT_SIZE_FACTOR = 1.5;
/**
* Computes a fluid font-size value that uses clamp(). A minimum and maxinmum
* font size OR a single font size can be specified.
*
* If a single font size is specified, it is scaled up and down by
* minimumFontSizeFactor and maximumFontSizeFactor to arrive at the minimum and
* maximum sizes.
*
* @example
* ```js
* // Calculate fluid font-size value from a minimum and maximum value.
* const fontSize = getComputedFluidTypographyValue( {
* minimumFontSize: '20px',
* maximumFontSize: '45px'
* } );
* // Calculate fluid font-size value from a single font size.
* const fontSize = getComputedFluidTypographyValue( {
* fontSize: '30px',
* } );
* ```
*
* @param {Object} args
* @param {?string} args.minimumViewPortWidth Minimum viewport size from which type will have fluidity. Optional if fontSize is specified.
* @param {?string} args.maximumViewPortWidth Maximum size up to which type will have fluidity. Optional if fontSize is specified.
* @param {string|number} [args.fontSize] Size to derive maximumFontSize and minimumFontSize from, if necessary. Optional if minimumFontSize and maximumFontSize are specified.
* @param {?string} args.maximumFontSize Maximum font size for any clamp() calculation. Optional.
* @param {?string} args.minimumFontSize Minimum font size for any clamp() calculation. Optional.
* @param {?number} args.scaleFactor A scale factor to determine how fast a font scales within boundaries. Optional.
* @param {?number} args.minimumFontSizeFactor How much to scale defaultFontSize by to derive minimumFontSize. Optional.
* @param {?number} args.maximumFontSizeFactor How much to scale defaultFontSize by to derive maximumFontSize. Optional.
*
* @return {string|null} A font-size value using clamp().
*/
export function getComputedFluidTypographyValue( {
minimumFontSize,
maximumFontSize,
fontSize,
minimumViewPortWidth = DEFAULT_MINIMUM_VIEWPORT_WIDTH,
maximumViewPortWidth = DEFAULT_MAXIMUM_VIEWPORT_WIDTH,
scaleFactor = DEFAULT_SCALE_FACTOR,
minimumFontSizeFactor = DEFAULT_MINIMUM_FONT_SIZE_FACTOR,
maximumFontSizeFactor = DEFAULT_MAXIMUM_FONT_SIZE_FACTOR,
} ) {
// Calculate missing minimumFontSize and maximumFontSize from
// defaultFontSize if provided.
if ( fontSize && ( ! minimumFontSize || ! maximumFontSize ) ) {
// Parse default font size.
const fontSizeParsed = getTypographyValueAndUnit( fontSize );
// Protect against invalid units.
if ( ! fontSizeParsed?.unit ) {
return null;
}
// If no minimumFontSize is provided, derive using min scale factor.
if ( ! minimumFontSize ) {
minimumFontSize =
fontSizeParsed.value * minimumFontSizeFactor +
fontSizeParsed.unit;
}
// If no maximumFontSize is provided, derive using max scale factor.
if ( ! maximumFontSize ) {
maximumFontSize =
fontSizeParsed.value * maximumFontSizeFactor +
fontSizeParsed.unit;
}
}
// Return early if one of the provided inputs is not provided.
if ( ! minimumFontSize || ! maximumFontSize ) {
return null;
}
// Grab the minimum font size and normalize it in order to use the value for calculations.
const minimumFontSizeParsed = getTypographyValueAndUnit( minimumFontSize );
// We get a 'preferred' unit to keep units consistent when calculating,
// otherwise the result will not be accurate.
const fontSizeUnit = minimumFontSizeParsed?.unit || 'rem';
// Grab the maximum font size and normalize it in order to use the value for calculations.
const maximumFontSizeParsed = getTypographyValueAndUnit( maximumFontSize, {
coerceTo: fontSizeUnit,
} );
// Protect against unsupported units.
if ( ! minimumFontSizeParsed || ! maximumFontSizeParsed ) {
return null;
}
// Use rem for accessible fluid target font scaling.
const minimumFontSizeRem = getTypographyValueAndUnit( minimumFontSize, {
coerceTo: 'rem',
} );
// Viewport widths defined for fluid typography. Normalize units
const maximumViewPortWidthParsed = getTypographyValueAndUnit(
maximumViewPortWidth,
{ coerceTo: fontSizeUnit }
);
const minumumViewPortWidthParsed = getTypographyValueAndUnit(
minimumViewPortWidth,
{ coerceTo: fontSizeUnit }
);
// Protect against unsupported units.
if (
! maximumViewPortWidthParsed ||
! minumumViewPortWidthParsed ||
! minimumFontSizeRem
) {
return null;
}
// Build CSS rule.
// Borrowed from https://websemantics.uk/tools/responsive-font-calculator/.
const minViewPortWidthOffsetValue = roundToPrecision(
minumumViewPortWidthParsed.value / 100,
3
);
const viewPortWidthOffset = minViewPortWidthOffsetValue + fontSizeUnit;
let linearFactor =
100 *
( ( maximumFontSizeParsed.value - minimumFontSizeParsed.value ) /
( maximumViewPortWidthParsed.value -
minumumViewPortWidthParsed.value ) );
linearFactor = roundToPrecision( linearFactor, 3 ) || 1;
const linearFactorScaled = linearFactor * scaleFactor;
const fluidTargetFontSize = `${ minimumFontSizeRem.value }${ minimumFontSizeRem.unit } + ((1vw - ${ viewPortWidthOffset }) * ${ linearFactorScaled })`;
return `clamp(${ minimumFontSize }, ${ fluidTargetFontSize }, ${ maximumFontSize })`;
}
/**
* Internal method that checks a string for a unit and value and returns an array consisting of `'value'` and `'unit'`, e.g., [ '42', 'rem' ].
* A raw font size of `value + unit` is expected. If the value is an integer, it will convert to `value + 'px'`.
*
* @param {string|number} rawValue Raw size value from theme.json.
* @param {Object|undefined} options Calculation options.
*
* @return {{ unit: string, value: number }|null} An object consisting of `'value'` and `'unit'` properties.
*/
export function getTypographyValueAndUnit( rawValue, options = {} ) {
if ( typeof rawValue !== 'string' && typeof rawValue !== 'number' ) {
return null;
}
// Converts numeric values to pixel values by default.
if ( isFinite( rawValue ) ) {
rawValue = `${ rawValue }px`;
}
const { coerceTo, rootSizeValue, acceptableUnits } = {
coerceTo: '',
// Default browser font size. Later we could inject some JS to compute this `getComputedStyle( document.querySelector( "html" ) ).fontSize`.
rootSizeValue: 16,
acceptableUnits: [ 'rem', 'px', 'em' ],
...options,
};
const acceptableUnitsGroup = acceptableUnits?.join( '|' );
const regexUnits = new RegExp(
`^(\\d*\\.?\\d+)(${ acceptableUnitsGroup }){1,1}$`
);
const matches = rawValue.match( regexUnits );
// We need a number value and a unit.
if ( ! matches || matches.length < 3 ) {
return null;
}
let [ , value, unit ] = matches;
let returnValue = parseFloat( value );
if ( 'px' === coerceTo && ( 'em' === unit || 'rem' === unit ) ) {
returnValue = returnValue * rootSizeValue;
unit = coerceTo;
}
if ( 'px' === unit && ( 'em' === coerceTo || 'rem' === coerceTo ) ) {
returnValue = returnValue / rootSizeValue;
unit = coerceTo;
}
return {
value: returnValue,
unit,
};
}
/**
* Returns a value rounded to defined precision.
* Returns `undefined` if the value is not a valid finite number.
*
* @param {number} value Raw value.
* @param {number} digits The number of digits to appear after the decimal point
*
* @return {number|undefined} Value rounded to standard precision.
*/
export function roundToPrecision( value, digits = 3 ) {
return Number.isFinite( value )
? parseFloat( value.toFixed( digits ) )
: undefined;
}