-
Notifications
You must be signed in to change notification settings - Fork 921
/
Copy pathmeasureText.js
165 lines (137 loc) · 4.92 KB
/
measureText.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
'use strict';
var FontFace = require('./FontFace');
var FontUtils = require('./FontUtils');
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var Hypher = require('hypher'); // FIXME: Lazy load
var english = require('hyphenation.en-us'); // FIXME: l10n
var h = new Hypher(english);
var _fragmentWidthMemos = {};
var measureFragmentWidth = function (fragment, fontFace, fontSize) {
var memoKey = fragment + fontFace.id + fontSize;
var memoized = _fragmentWidthMemos[memoKey];
if (memoized) { return memoized; }
ctx.font = fontFace.attributes.style + ' normal ' +
fontFace.attributes.weight + ' ' + fontSize + 'pt ' +
fontFace.family;
var fragmentWidth = ctx.measureText(fragment).width;
_fragmentWidthMemos[memoKey] = fragmentWidth;
return fragmentWidth;
};
var _hyphenationOpportunityMemos = {};
var getHyphenationOpportunities = function(text, hyphens) {
var memoKey = hyphens + text;
var memoized = _hyphenationOpportunityMemos[memoKey];
if (memoized) { return memoized; }
var words = text.split(/\s+/);
if (hyphens === 'auto') {
words = words.map(function(word) {
return h.hyphenate(word);
});
} else {
words = words.map(function(word) {
return [word];
});
}
_hyphenationOpportunityMemos[memoKey] = words;
return words;
};
var _paragraphMetricsMemos = {};
var getParagraphMetrics = function(text, hyphens, fontFace, fontSize) {
var memoKey = text + hyphens + fontFace.id + fontSize;
var memoized = _paragraphMetricsMemos[memoKey];
if (memoized) { return memoized; }
var metrics = {};
metrics.fragments = getHyphenationOpportunities(text, hyphens);
metrics.fragmentWidths = metrics.fragments.map(function(word) {
return word.map (function(morpheme) {
return measureFragmentWidth(morpheme, fontFace, fontSize);
});
});
metrics.spaceWidth = measureFragmentWidth(' ', fontFace, fontSize);
metrics.hyphenWidth = measureFragmentWidth('-', fontFace, fontSize);
_paragraphMetricsMemos[memoKey] = metrics;
return metrics;
};
var firstFit = function(maxWidth, metrics) {
function Word() {
this.text = '';
this.width = 0;
}
function Line() {
this.whiteSpace = 0;
this.width = 0;
this.words = [ new Word() ];
}
var lines = [ new Line() ];
Line.prototype.appendMorpheme = function(morpheme, advanceWidth) {
var word = this.words[this.words.length - 1];
word.text += morpheme;
word.width += advanceWidth;
this.width += advanceWidth;
};
Line.prototype.appendHyphen = function() {
this.appendMorpheme('-', metrics.hyphenWidth);
};
Line.prototype.appendSpace = function() {
this.whiteSpace += metrics.spaceWidth;
};
Line.prototype.newWord = function() {
this.appendSpace();
this.words.push( new Word() );
};
function push(morpheme, advanceWidth, initial, final) {
var line = lines[lines.length - 1];
// setting the first morpheme of a line always succeeds
if (line.width === 0) {
// good to go!
// do we need to break the line?
} else if
// handle a middle syllable
((!initial && !final &&
line.width + line.whiteSpace + advanceWidth + metrics.hyphenWidth > maxWidth) ||
// handle an initial syllable
(initial &&
line.width + line.whiteSpace + metrics.spaceWidth + advanceWidth > maxWidth) ||
// handle a final syllable.
(!initial && final &&
line.width + line.whiteSpace + advanceWidth > maxWidth)) {
if (!initial) { line.appendHyphen(); }
line = new Line();
lines.push(line);
} else if (initial) {
line.newWord();
}
line.appendMorpheme(morpheme, advanceWidth);
}
metrics.fragments.forEach(function(word, wordIdx, words) {
word.forEach(function(morpheme, morphemeIdx) {
var advanceWidth = metrics.fragmentWidths[wordIdx][morphemeIdx];
var initial = (morphemeIdx === 0);
var final = (morphemeIdx === word.length - 1);
push(morpheme, advanceWidth, initial, final);
});
});
return lines;
};
// var _lineBreakMemos = {};
module.exports = function measureText(text, width, fontFace, fontSize, lineHeight, hyphens, breakingStrategy) {
// Bail and return zero unless we're sure the font is ready.
if (!FontUtils.isFontLoaded(fontFace)) {
return { width: 0, height: 0, lines: [] };
}
// var memoKey = text + hyphens + fontFace.id + fontSize + width + breakingStrategy;
// var memoized = _lineBreakMemos[memoKey];
// if (memoized) { return memoized; }
var metrics = getParagraphMetrics(text, hyphens, fontFace, fontSize);
var measuredSize = {};
measuredSize.width = width;
if (!breakingStrategy || breakingStrategy === 'firstFit') {
measuredSize.lines = firstFit(width, metrics);
} else {
throw 'TODO: implement global fit linebreaking';
}
measuredSize.height = measuredSize.lines.length * lineHeight;
// _lineBreakMemos[memoKey] = measuredSize;
return measuredSize;
};