-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrenderer_run_compute.go
367 lines (335 loc) · 13.4 KB
/
renderer_run_compute.go
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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
package ptxt
import "image"
import "github.com/tinne26/ptxt/internal"
import "github.com/tinne26/ggfnt"
// Helper methods for drawing and measuring.
// Main axis centering is not accounted for, that's applied directly
// while drawing.
//
// The workingAscent and workingDescent parameters are necessary
// for those cases where we are using MaskBounding. These values
// will be obtained either from computeRunAdvances() or
func (self *Renderer) computeTextOrigin(x, y int) (int, int) {
const unexpectedAlign = "unexpected Renderer align value"
strand := self.Strand()
scale := int(self.scale)
font := strand.Font()
var shift int
switch self.align.Vert() {
case Top:
shift = -self.run.top
case CapLine:
shift = int(font.Metrics().UppercaseAscent())*scale
if shift == 0 {
// if uppercase ascent is undefined, we use this heuristic.
// this is of course very debatable, but I'd say that in
// practice this tends to be more helpful than nothing
shift = -self.run.top
}
case Midline:
shift = int(font.Metrics().MidlineAscent())*scale
if shift == 0 {
// if lowercase ascent is undefined, we use this heuristic.
// this is of course very debatable, but I'd say that in
// practice this tends to be more helpful than nothing
shift = (-self.run.top >> 1)
}
case VertCenter:
shift = -self.run.top - ((self.run.bottom - self.run.top) >> 1)
if self.direction == Vertical { shift = 0 } // done per line on computeVertLineStart
case Baseline:
shift = 0
case Bottom:
shift = -self.run.bottom
if self.direction == Vertical { shift = 0 } // done per line on computeVertLineStart
case LastBaseline:
if self.direction == Vertical { // TODO: maybe I want to define this
panic("Vertical rendering doesn't support LastBaseline align")
}
if self.run.isMultiline { // multi-line text
shift = -(self.run.bottom - self.run.lastRowDescent)
} else { // single line text, last baseline == first baseline
shift = 0
}
default:
panic(unexpectedAlign)
}
switch self.direction {
case Horizontal : return x, y + shift
case Vertical:
switch self.align.Horz() {
case Left : x = x - self.run.left
case HorzCenter : x = x - ((self.run.right + self.run.left) >> 1)
case Right : x = x - self.run.right
default:
panic(brokenCode)
}
return x, y + shift
case Sideways : return x + shift, y
case SidewaysRight : return x - shift, y
default:
panic("unexpected direction '" + self.direction.String() + "'")
}
}
// - I can have a uint16 to mark "twine effect", and then a
// second uint16 as the offset to it. but then maybe calling
// the buffer "run.glyphIndices" is not ideal. otherwise, I
// need to port everything to a twine model first, and operate
// only with the twine. I don't like that because even in the
// twine, when ligature replacements are needed, we can't
// modify the twine itself. so, we kinda always need that
// first conversion step. ok I guess.
// Spaces at the end of line are counted. They can be elided on wrap modes,
// but not here.
func (self *Renderer) computeRunLayout(maxLineLen int) {
// resize advances buffer, clear metrics
self.run.isMultiline = false
self.run.advances = setBufferSize(self.run.advances, len(self.run.glyphIndices))
self.run.kernings = setBufferSize(self.run.kernings, len(self.run.glyphIndices))
self.run.wrapIndices = self.run.wrapIndices[ : 0]
self.run.lineLengths = self.run.lineLengths[ : 0]
self.run.top, self.run.bottom, self.run.left, self.run.right = 0, 0, 0, 0
self.run.firstRowAscent = 0
self.run.lastRowDescent = 0
if self.boundingMode == 0 { self.boundingMode = LogicalBounding } // default init
if len(self.run.glyphIndices) == 0 { return } // trivial case
const unexpectedBoundingMode = "unexpected bounding mode"
if self.direction == Vertical {
self.run.horzShifts = setBufferSize(self.run.horzShifts, len(self.run.glyphIndices))
switch self.boundingMode & ^noDescent {
case LogicalBounding:
self.computeVerticalRunLogicalLayout(maxLineLen)
case MaskBounding:
self.computeVerticalRunMaskLayout(maxLineLen)
default:
panic(unexpectedBoundingMode)
}
} else {
switch self.boundingMode & ^noDescent {
case LogicalBounding:
self.computeRunLogicalLayout(maxLineLen)
case MaskBounding:
self.computeRunMaskLayout(maxLineLen)
default:
panic(unexpectedBoundingMode)
}
}
}
// Precondition: except glyph indices, run data has been cleared
// and slices resized to len(glyphIndices), and we have at least
// one glyph index.
func (self *Renderer) computeRunLogicalLayout(maxLineLen int) {
if len(self.run.glyphIndices) > 32000 { panic(preViolation) }
currentStrand := self.Strand()
currentFont := currentStrand.Font()
currentScale := int(self.scale)
currentGlyphInterspacing := strandFullGlyphInterspacing(currentStrand)*currentScale
var layoutBreak layoutLineBreakTempVariables
layoutBreak.Init(strandFullLineHeight(currentStrand)*currentScale)
self.run.firstRowAscent = int(currentFont.Metrics().Ascent())*currentScale
self.run.top = -self.run.firstRowAscent
if self.boundingMode & noDescent == 0 {
self.run.lastRowDescent = int(currentFont.Metrics().Descent())*currentScale
}
var prevEffectiveGlyph ggfnt.GlyphIndex = ggfnt.GlyphMissing
var prevInterspacing int
var x, index int
var layoutWrap layoutWrapTempVariables
for index < len(self.run.glyphIndices) {
glyphIndex := self.run.glyphIndices[index]
if glyphIndex < ggfnt.MaxGlyphs {
layoutBreak.NotifyNonBreak()
layoutWrap.IncreaseLineCharCount()
memoX := x
kerning := int(currentFont.Kerning().Get(prevEffectiveGlyph, glyphIndex))*currentScale
self.run.kernings[index] = int16(kerning)
advance := int(currentFont.Glyphs().Advance(glyphIndex))*currentScale
if advance < 0 || advance > 65535 { panic("advance > 65535") } // discretional assertion
self.run.advances[index] = uint16(advance)
x += prevInterspacing + kerning + advance
prevInterspacing = currentGlyphInterspacing
prevEffectiveGlyph = glyphIndex
// line wrapping pain
if x <= maxLineLen {
layoutWrap.GlyphNonBreak(currentStrand, glyphIndex, index, memoX, x)
} else {
index, x = layoutWrap.GlyphBreak(self, currentStrand, glyphIndex, index, memoX, x)
self.run.bottom = layoutBreak.NotifyBreak(self, 0, x, self.run.bottom)
x, prevInterspacing = 0, 0
prevEffectiveGlyph = ggfnt.GlyphMissing
continue
}
} else { // control glyph
switch glyphIndex {
case ggfnt.GlyphNewLine:
if self.elideLineBreak(index) {
// line break should be elided, absorbed by immediately previous line wrapping break
} else {
// apply break
self.run.bottom = layoutBreak.NotifyBreak(self, 0, x, self.run.bottom)
x, prevInterspacing = 0, 0
prevEffectiveGlyph = ggfnt.GlyphMissing
layoutWrap.PostBreakUpdate(index + 1)
}
case ggfnt.GlyphMissing:
// should typically be triggered at an earlier point,
// so I'm not even sure you can reach this normally
panic("missing glyph")
case ggfnt.GlyphZilch: // we don't change the prevEffectiveGlyph here
self.run.advances[index] = 0
self.run.kernings[index] = 0
case internal.TwineEffectMarkerGlyph:
panic("unimplemented")
// lineBreaksOnly = false // yes or no? it depends?
// prevEffectiveGlyph = glyphIndex
// TODO: whether this breaks or doesn't break prev effective glyph
// should probably depend on the effect. color changes shouldn't break
// anything. other effects might break. basically, effects with padding
// should break kerning, but maybe we want to leave it to the user,
// as some cases might be ambiguous
default:
// ... some other control glyph, possibly a custom control glyph
// for the font or user code. we are not breaking kerning nor
// interrupting the previous glyph, but that could be discussed (TODO)
self.run.advances[index] = 0
self.run.kernings[index] = 0
}
}
index += 1
}
// take last x and descent into account
layoutBreak.NotifyTextEnd(self, x)
}
// Precondition: except glyph indices, run data has been cleared
// and slices resized to len(glyphIndices), and we have at least
// one glyph index.
func (self *Renderer) computeRunMaskLayout(maxLineLen int) {
if len(self.run.glyphIndices) > 32000 { panic(preViolation) }
self.run.bottom = -9999
self.run.left = +9999
currentStrand := self.Strand()
currentFont := currentStrand.Font()
currentScale := int(self.scale)
currentGlyphInterspacing := strandFullGlyphInterspacing(currentStrand)*currentScale
var layoutBreak layoutLineBreakTempVariables
layoutBreak.Init(strandFullLineHeight(currentStrand)*currentScale)
var prevEffectiveGlyph ggfnt.GlyphIndex = ggfnt.GlyphMissing
var prevInterspacing, prevMaskRight, maskLeft int = 0, -9999, +9999
var x, y, index int
var firstNonEmptyLine uint8 // 0 for first non empty not reached, 1 for reached, 2 for exceeded
var layoutWrap layoutWrapTempVariables
for index < len(self.run.glyphIndices) {
glyphIndex := self.run.glyphIndices[index]
if glyphIndex < ggfnt.MaxGlyphs {
layoutBreak.NotifyNonBreak()
layoutWrap.IncreaseLineCharCount()
// get glyph mask for bounds and adjust run bounds
mask := self.loadMask(glyphIndex, currentFont)
var bounds image.Rectangle
if mask != nil {
bounds = mask.Bounds()
if x + bounds.Min.X*currentScale < maskLeft {
maskLeft = x + bounds.Min.X*currentScale
}
}
kerning := int(currentFont.Kerning().Get(prevEffectiveGlyph, glyphIndex))*currentScale
self.run.kernings[index] = int16(kerning)
advance := int(currentFont.Glyphs().Advance(glyphIndex))*currentScale
if advance < 0 || advance > 65535 { panic("advance > 65535") } // discretional assertion
self.run.advances[index] = uint16(advance)
maskRight := x + bounds.Max.X*currentScale + prevInterspacing + kerning
// line wrapping pain
if x <= maxLineLen {
layoutWrap.GlyphNonBreak(currentStrand, glyphIndex, index, prevMaskRight, maskRight)
if mask != nil {
if firstNonEmptyLine == 0 {
firstNonEmptyLine = 1
self.run.top = y // this is not correct here yet, but it's corrected with first line ascent later
}
if firstNonEmptyLine == 1 {
self.run.firstRowAscent = max(self.run.firstRowAscent, -bounds.Min.Y*currentScale)
}
if self.boundingMode & noDescent == 0 { // descent case
self.run.lastRowDescent = max(self.run.lastRowDescent, bounds.Max.Y*currentScale)
}
elevation := min(bounds.Max.Y*currentScale, 0)
self.run.bottom = max(self.run.bottom, y + elevation, y + elevation + self.run.lastRowDescent)
prevMaskRight = maskRight // this must stay inside this if
}
x += prevInterspacing + kerning + advance
prevInterspacing = currentGlyphInterspacing
prevEffectiveGlyph = glyphIndex
} else {
index, x = layoutWrap.GlyphBreak(self, currentStrand, glyphIndex, index, prevMaskRight, maskRight)
y = layoutBreak.NotifyBreak(self, maskLeft, x, y)
self.run.lastRowDescent = 0
x, prevInterspacing, prevMaskRight, maskLeft = 0, 0, -9999, 9999
prevEffectiveGlyph = ggfnt.GlyphMissing
if firstNonEmptyLine == 1 { firstNonEmptyLine = 2 }
continue
}
} else { // control glyph
switch glyphIndex {
case ggfnt.GlyphNewLine:
if self.elideLineBreak(index) {
// line break should be elided, absorbed by immediately previous line wrapping break
} else {
// apply break
y = layoutBreak.NotifyBreak(self, maskLeft, prevMaskRight, y)
layoutWrap.PostBreakUpdate(index + 1)
self.run.lastRowDescent = 0
x, prevInterspacing, prevMaskRight, maskLeft = 0, 0, -9999, 9999
prevEffectiveGlyph = ggfnt.GlyphMissing
if firstNonEmptyLine == 1 { firstNonEmptyLine = 2 }
}
case ggfnt.GlyphMissing:
// should typically be triggered at an earlier point,
// so I'm not even sure you can reach this normally
panic("missing glyph")
case ggfnt.GlyphZilch: // we don't change the prevEffectiveGlyph here
self.run.advances[index] = 0
self.run.kernings[index] = 0
case internal.TwineEffectMarkerGlyph:
panic("unimplemented")
default:
self.run.advances[index] = 0
self.run.kernings[index] = 0
}
}
index += 1
}
// final adjustments
self.run.top -= self.run.firstRowAscent
self.run.left = min(self.run.left, maskLeft)
if prevMaskRight > self.run.right { self.run.right = prevMaskRight }
lineLen := self.run.right - self.run.left
self.run.lineLengths = append(self.run.lineLengths, uint16(max(0, lineLen))) // TODO: big hack max
self.run.bottom = max(self.run.bottom, self.run.top)
self.run.left = min(self.run.right, self.run.left)
}
func (self *Renderer) computeLineStart(o int, lineIndex uint16) int {
switch self.align.Horz() {
case Left : return o
case HorzCenter : return o - int(self.run.lineLengths[lineIndex] >> 1)
case Right : return o - int(self.run.lineLengths[lineIndex])
default:
panic(brokenCode)
}
}
func (self *Renderer) elideLineBreak(index int) bool {
wrapsLen := len(self.run.wrapIndices)
return wrapsLen > 0 && (self.run.wrapIndices[wrapsLen - 1] & 0x7FFF) + 1 == uint16(index)
}
// Basically, if we are on the second line break and par break is enabled,
// we either return half the height or none at all.
func (self *Renderer) adjustParLineBreakHeightFor(lineBreakHeight, consecutiveLineBreaks int) int {
// no par break case
if !self.parBreakEnabled { return lineBreakHeight }
// par break case
switch consecutiveLineBreaks {
case 2: return (lineBreakHeight >> 1)
case 3: return lineBreakHeight - (lineBreakHeight >> 1) // (complete prev half break)
default:
return lineBreakHeight
}
}