-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
fit-to-width.js
204 lines (171 loc) · 5.94 KB
/
fit-to-width.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
// fit-to-width.js
/*
Fits text to the width of its DOM container using various methods.
The core function is ftw_fit(), to which you pass a DOM element or an array of DOM elements. This applies various width adjustment methods, until either succeeding or giving up. Default (which is easily configurable) is to first try CSS font-stretch, then CSS letter-spacing, then finally CSS transform.
*/
// set up defaults for each method
const ftw_methods = {
"font-stretch": {min: 0.00001, max: 0x8000 - 1/0x10000, bsFunction: ftw_setFontStretch},
"font-variation-settings:wdth": {min: -0x8000, max: 0x8000 - 1/0x10000, bsFunction: ftw_setFontVariationSettingsWdth},
"letter-spacing": {min: -0.05, max: 1, bsFunction: ftw_setLetterSpacing},
"word-spacing": {min: -0.2, max: 20, bsFunction: ftw_setWordSpacing},
"transform": {},
"ligatures": {}
};
// function to check if iterable
const ftw_ArgIsIterable = object => object != null && typeof object[Symbol.iterator] === 'function';
function ftw_setFontStretch (el, val, operation) {
el.style.fontStretch = val + "%";
}
function ftw_setFontVariationSettingsWdth (el, val, operation) {
let fvsString = "'wdth' " + val;
if (operation.axes)
fvsString += "," + operation.axes;
el.style.fontVariationSettings = fvsString;
}
function ftw_setLetterSpacing (el, val) {
el.style.letterSpacing = val + "em";
}
function ftw_setWordSpacing (el, val) {
el.style.wordSpacing = val + "em";
}
function ftw_Operation (method, min, max, maxDiff, maxIterations, axes) {
if (ftw_methods[method]) {
this.method = method;
this.min = min === undefined ? ftw_methods[method].min : min;
this.max = min === undefined ? ftw_methods[method].max : max;
this.bsFunction = ftw_methods[method].bsFunction;
this.maxDiff = maxDiff === undefined ? 1 : maxDiff; // allows 0
this.maxIterations = maxIterations === undefined ? 50 : maxIterations;
this.axes = axes;
}
else
this.method = null;
}
// main function
function ftw_fit (elements, ftwOperations, targetWidth) {
let startTime = performance.now();
let config = {
operations: ftwOperations || ["font-variation-settings:wdth", "transform"]
};
let els;
// get all elements selected by the string elements
if (typeof elements === "string")
els = document.querySelectorAll(elements);
// is elements already a NodeList or array of elements? if so, fine; otherwise make it an array
else if (ftw_ArgIsIterable(elements))
els = elements;
else
els = [elements]; // convert to an array
// user config?
if (!Array.isArray(config.operations)) {
if (!config.operations)
config.operations = ["font-stretch"];
else if (typeof config.operations === "string" || typeof config.operations === "object")
config.operations = [config.operations];
}
// for each element supplied by the user
for (let el of els)
{
let success = false;
config.targetWidth = targetWidth || el.clientWidth;
el.style.whiteSpace = "nowrap";
el.style.width = "max-content";
el.style.transform = "none";
// for each operation specified by the user
for (let op of config.operations) {
let operation;
if (typeof op === "string")
operation = new ftw_Operation(op);
else
operation = new ftw_Operation(op.method, op.min, op.max, op.maxDiff, op.maxIterations, op.axes);
switch (operation.method) {
case "transform": ftw_fit_transform (el, config); break;
case "ligatures": ftw_fit_ligatures (el, config); break;
case "font-stretch":
case "font-variation-settings:wdth":
case "letter-spacing":
case "word-spacing":
success = ftw_fit_binary_search (el, operation, config.targetWidth);
break;
// ignore unrecognized methods
}
if (success)
{
console.log (operation.method);
break;
}
}
// reset element width
el.style.width = config.targetWidth+"px"; // TODO: revert it to its original getComputedStyle() width, e.g. "10em"?
}
config.elapsedTime = performance.now() - startTime;
return config;
}
function ftw_fit_binary_search (el, operation, targetWidth) {
let iterations = 0;
let min = operation.min, max = operation.max;
let minClientWidth, maxClientWidth;
let done = false;
let success = false;
// checks before binary search
if (min > max)
done = true;
else {
operation.bsFunction(el, min, operation); // above the min?
if ((minClientWidth=el.clientWidth) >= targetWidth) {
done = true;
if (minClientWidth == targetWidth)
success = true;
}
else {
operation.bsFunction(el, max, operation); // below the max?
if ((maxClientWidth=el.clientWidth) < targetWidth) {
done = true;
if (maxClientWidth == targetWidth)
success = true;
}
else if (minClientWidth >= maxClientWidth) {// check width at min != width at max
done = true;
}
}
}
// the binary search
while (!done) {
let val = 0.5 * (min+max);
operation.bsFunction(el, val, operation); // set the CSS
let diff = el.clientWidth - targetWidth; // are we under or over?
if (diff <= 0) {
if (diff > -operation.maxDiff) { // SUCCESS: <maxDiff
console.log ("success, diff="+diff);
success = true;
done = true;
}
else
min = val; // we guessed too low
}
else
max = val; // we guessed too high
// next iteration
iterations++;
if (iterations >= operation.maxIterations) { // FAIL: wght did not converge
done = true;
if (diff>0) // better to leave the element at minWdth rather than > targetWidth
operation.bsFunction(el, min, operation);
}
}
return success;
}
function ftw_fit_transform (el, config) {
el.style.transformOrigin = "left";
el.style.transform = "scale(" + (config.targetWidth / el.clientWidth) + ",1)";
}
function ftw_fit_ligatures (el, config) {
el.style.fontFeatureSettings = "'liga' 1, 'dlig' 1";
// EXPERIMENTAL
// * to reduce width, should turn on ligatures
// * to increase width, should turn off ligatures
// * for both, should check the effect on width
// * should really add these to any existing settings using getComputedStyle
// Good candidate string: "VAMPIRE HELL" set in Skia
}