This commit is contained in:
Ryan Oldenburg 2021-05-11 00:26:03 -05:00
parent 10311fae70
commit eae3b5e443
16 changed files with 324 additions and 135 deletions

View file

@ -326,6 +326,25 @@ proc strokePolygon*(
path.polygon(pos, size, sides)
mask.strokePath(path, strokeWidth)
proc fillText*(
target: Image | Mask,
arrangement: Arrangement,
transform: Vec2 | Mat3 = vec2(0, 0)
) =
## Fills the text arrangement.
for spanIndex, (start, stop) in arrangement.spans:
let font = arrangement.fonts[spanIndex]
for runeIndex in start .. stop:
var path = font.typeface.getGlyphPath(arrangement.runes[runeIndex])
path.transform(
translate(arrangement.positions[runeIndex]) *
scale(vec2(font.scale))
)
when type(target) is Image:
target.fillPath(path, font.paint, transform)
else: # target is Mask
target.fillPath(path, transform)
proc fillText*(
target: Image | Mask,
font: Font,
@ -334,18 +353,33 @@ proc fillText*(
bounds = vec2(0, 0),
hAlign = haLeft,
vAlign = vaTop
) =
) {.inline.} =
## Typesets and fills the text. Optional parameters:
## transform: translation or matrix to apply
## bounds: width determines wrapping and hAlign, height for vAlign
## hAlign: horizontal alignment of the text
## vAlign: vertical alignment of the text
let arrangement = font.typeset(text, bounds, hAlign, vAlign)
for i in 0 ..< arrangement.runes.len:
fillText(target, font.typeset(text, bounds, hAlign, vAlign), transform)
proc strokeText*(
target: Image | Mask,
arrangement: Arrangement,
transform: Vec2 | Mat3 = vec2(0, 0),
strokeWidth = 1.0
) =
## Strokes the text arrangement.
for spanIndex, (start, stop) in arrangement.spans:
let font = arrangement.fonts[spanIndex]
for runeIndex in start .. stop:
var path = font.typeface.getGlyphPath(arrangement.runes[runeIndex])
path.transform(
translate(arrangement.positions[runeIndex]) *
scale(vec2(font.scale))
)
when type(target) is Image:
target.fillPath(arrangement.getPath(i), font.paint, transform)
target.strokePath(path, font.paint, transform, strokeWidth)
else: # target is Mask
target.fillPath(arrangement.getPath(i), transform)
target.strokePath(path, transform, strokeWidth)
proc strokeText*(
target: Image | Mask,
@ -356,17 +390,15 @@ proc strokeText*(
bounds = vec2(0, 0),
hAlign = haLeft,
vAlign = vaTop
) =
) {.inline.} =
## Typesets and strokes the text. Optional parameters:
## transform: translation or matrix to apply
## bounds: width determines wrapping and hAlign, height for vAlign
## hAlign: horizontal alignment of the text
## vAlign: vertical alignment of the text
let arrangement = font.typeset(text, bounds, hAlign, vAlign)
for i in 0 ..< arrangement.runes.len:
when type(target) is Image:
target.strokePath(
arrangement.getPath(i), font.paint, transform, strokeWidth
strokeText(
target,
font.typeset(text, bounds, hAlign, vAlign),
transform,
strokeWidth
)
else: # target is Mask
target.strokePath(arrangement.getPath(i), transform, strokeWidth)

View file

@ -19,8 +19,13 @@ type
textCase*: TextCase
noKerningAdjustments*: bool ## Optionally disable kerning pair adjustments
Arrangement* = ref object
Span* = ref object
text*: string
font*: Font
Arrangement* = ref object
spans*: seq[(int, int)]
fonts*: seq[Font]
runes*: seq[Rune]
positions*: seq[Vec2]
selectionRects*: seq[Rect]
@ -62,6 +67,10 @@ proc lineGap*(typeface: Typeface): float32 {.inline.} =
if typeface.opentype != nil:
result = typeface.opentype.hhea.lineGap.float32
proc lineHeight*(typeface: Typeface): float32 {.inline.} =
## The default line height in font units.
typeface.ascent - typeface.descent + typeface.lineGap
proc getGlyphPath*(typeface: Typeface, rune: Rune): Path {.inline.} =
## The glyph path for the rune.
if rune.uint32 > SP.uint32: # Empty paths for control runes (not tofu)
@ -99,6 +108,11 @@ proc defaultLineHeight*(font: Font): float32 {.inline.} =
font.typeface.ascent - font.typeface.descent + font.typeface.lineGap
round(fontUnits * font.scale)
proc newSpan*(text: string, font: Font): Span =
result = Span()
result.text = text
result.font = font
proc convertTextCase(runes: var seq[Rune], textCase: TextCase) =
case textCase:
of tcNormal:
@ -120,8 +134,7 @@ proc canWrap(rune: Rune): bool {.inline.} =
rune == Rune(32) or rune.isWhiteSpace()
proc typeset*(
font: Font,
text: string,
spans: seq[Span],
bounds = vec2(0, 0),
hAlign = haLeft,
vAlign = vaTop,
@ -133,94 +146,89 @@ proc typeset*(
## hAlign: horizontal alignment of the text
## vAlign: vertical alignment of the text
## wrap: enable/disable text wrapping
result = Arrangement()
result.font = font
block: # Walk and filter runes
result = Arrangement()
block: # Walk and filter the spans
var start: int
for span in spans:
var
i = 0
rune: Rune
while i < text.len:
fastRuneAt(text, i, rune, true)
runes: seq[Rune]
while i < span.text.len:
fastRuneAt(span.text, i, rune, true)
# Ignore control runes (0 - 31) except LF for now
if rune.uint32 >= SP.uint32 or rune.uint32 == LF.uint32:
result.runes.add(rune)
runes.add(rune)
if result.runes.len == 0:
# No runes to typeset, early return
return
if runes.len > 0:
runes.convertTextCase(span.font.textCase)
result.runes.add(runes)
result.spans.add((start, start + runes.len - 1))
result.fonts.add(span.font)
start += runes.len
result.runes.convertTextCase(font.textCase)
result.positions.setLen(result.runes.len)
result.selectionRects.setLen(result.runes.len)
let lineHeight =
if font.lineheight >= 0:
font.lineheight
else:
font.defaultLineHeight
proc advance(font: Font, runes: seq[Rune], i: int): float32 {.inline.} =
if not font.noKerningAdjustments and i + 1 < runes.len:
result += font.typeface.getKerningAdjustment(runes[i], runes[i + 1])
result += font.typeface.getAdvance(runes[i])
result *= font.scale
var fontUnitInitialY = font.typeface.ascent + font.typeface.lineGap / 2
if lineHeight != font.defaultLineHeight:
fontUnitInitialY += (
(lineHeight / font.scale) -
(font.typeface.ascent - font.typeface.descent + font.typeface.lineGap)
) / 2
let initialY = round(fontUnitInitialY * font.scale)
var lines = @[(0, 0)] # (start, stop)
block: # Arrange the glyphs horizontally first (handling line breaks)
var
at: Vec2
prevCanWrap: int
at.y = initialY
for i, rune in result.runes:
for spanIndex, (start, stop) in result.spans:
let font = result.fonts[spanIndex]
for runeIndex in start .. stop:
let rune = result.runes[runeIndex]
if rune == LF:
let advance = font.typeface.getAdvance(SP) * font.scale
result.positions[i] = at
result.selectionRects[i] = rect(at.x, at.y - initialY, advance, lineHeight)
result.positions[runeIndex] = at
result.selectionRects[runeIndex] = rect(at.x, at.y, advance, 0)
at.x = 0
at.y += lineHeight
at.y += 1
prevCanWrap = 0
lines[^1][1] = runeIndex
lines.add((runeIndex + 1, 0))
else:
if rune.canWrap():
prevCanWrap = i
prevCanWrap = runeIndex
let advance = advance(font, result.runes, i)
let advance = advance(font, result.runes, runeIndex)
if wrap and rune != SP and bounds.x > 0 and at.x + advance > bounds.x:
# Wrap to new line
at.x = 0
at.y += lineHeight
at.y += 1
var lineStart = runeIndex
# Go back and wrap glyphs after the wrap index down to the next line
if prevCanWrap > 0 and prevCanWrap != i:
for j in prevCanWrap + 1 ..< i:
result.positions[j] = at
result.selectionRects[j].xy = vec2(at.x, at.y - initialY)
at.x += advance(font, result.runes, j)
if prevCanWrap > 0 and prevCanWrap != runeIndex:
for i in prevCanWrap + 1 ..< runeIndex:
result.positions[i] = at
result.selectionRects[i] = rect(at.x, at.y - initialY, advance, lineHeight)
result.selectionRects[i].xy = vec2(at.x, at.y)
at.x += advance(font, result.runes, i)
dec lineStart
lines[^1][1] = lineStart - 1
lines.add((lineStart, 0))
result.positions[runeIndex] = at
result.selectionRects[runeIndex] = rect(at.x, at.y, advance, 0)
at.x += advance
lines[^1][1] = result.runes.len - 1
if hAlign != haLeft:
# Since horizontal alignment adjustments are different for each line,
# find the start and stop of each line of text.
var
lines: seq[(uint32, uint32)] # (start, stop)
start: uint32
prevY = result.positions[0].y
for i, pos in result.positions:
if pos.y != prevY:
lines.add((start, i.uint32 - 1))
start = i.uint32
prevY = pos.y
lines.add((start, result.positions.len.uint32 - 1))
for (start, stop) in lines:
var furthestX: float32
for i in countdown(stop, start):
@ -242,6 +250,84 @@ proc typeset*(
result.positions[i].x += xAdjustment
result.selectionRects[i].x += xAdjustment
block: # Nudge selection rects to pixel grid
var at = result.selectionRects[0]
at.x = round(at.x)
for rect in result.selectionRects.mitems:
if rect.y == at.y:
rect.x = at.x
rect.w = round(rect.w)
at.x = rect.x + rect.w
else:
rect.w = round(rect.w)
at.x = rect.w
at.y = rect.y
block: # Arrange the lines vertically
let initialY = block:
var maxInitialY: float32
block outer:
for spanIndex, (start, stop) in result.spans:
let
font = result.fonts[spanIndex]
lineHeight =
if font.lineheight >= 0:
font.lineheight
else:
font.defaultLineHeight
var fontUnitInitialY = font.typeface.ascent + font.typeface.lineGap / 2
if lineHeight != font.defaultLineHeight:
fontUnitInitialY += (
(lineHeight / font.scale) - font.typeface.lineHeight
) / 2
maxInitialY = max(maxInitialY, round(fontUnitInitialY * font.scale))
for runeIndex in start .. stop:
if runeIndex == lines[0][1]:
break outer
maxInitialY
var lineHeights = newSeq[float32](lines.len)
block: # Compute each line's line height
var line: int
for spanIndex, (start, stop) in result.spans:
let
font = result.fonts[spanIndex]
fontLineHeight =
if font.lineheight >= 0:
font.lineheight
else:
font.defaultLineHeight
lineHeights[line] = max(lineHeights[line], fontLineHeight)
for runeIndex in start .. stop:
if line + 1 < lines.len and runeIndex == lines[line][1]:
inc line
lineHeights[line] = max(lineHeights[line], fontLineHeight)
# Handle when span and line endings coincide
if line + 1 < lines.len and stop == lines[line][1]:
inc line
lineHeights[line] = max(lineHeights[line], fontLineHeight)
block: # Vertically position the glyphs
var
line: int
baseline = initialY
for spanIndex, (start, stop) in result.spans:
let
font = result.fonts[spanIndex]
lineHeight =
if font.lineheight >= 0:
font.lineheight
else:
font.defaultLineHeight
for runeIndex in start .. stop:
if line + 1 < lines.len and runeIndex == lines[line + 1][0]:
inc line
baseline += lineHeights[line]
result.positions[runeIndex].y = baseline
result.selectionRects[runeIndex].y =
baseline - round(font.typeface.ascent * font.scale)
result.selectionRects[runeIndex].h = lineHeight
if vAlign != vaTop:
let
finalSelectionRect = result.selectionRects[^1]
@ -261,6 +347,22 @@ proc typeset*(
result.positions[i].y += yAdjustment
result.selectionRects[i].y += yAdjustment
proc typeset*(
font: Font,
text: string,
bounds = vec2(0, 0),
hAlign = haLeft,
vAlign = vaTop,
wrap = true
): Arrangement {.inline.} =
## Lays out the character glyphs and returns the arrangement.
## Optional parameters:
## bounds: width determines wrapping and hAlign, height for vAlign
## hAlign: horizontal alignment of the text
## vAlign: vertical alignment of the text
## wrap: enable/disable text wrapping
typeset(@[newSpan(text, font)], bounds, hAlign, vAlign, wrap)
proc computeBounds*(arrangement: Arrangement): Vec2 =
if arrangement.runes.len > 0:
for i in 0 ..< arrangement.runes.len:
@ -274,13 +376,8 @@ proc computeBounds*(font: Font, text: string): Vec2 {.inline.} =
## Computes the width and height of the text in pixels.
font.typeset(text).computeBounds()
proc getPath*(arrangement: Arrangement, index: int): Path =
## Returns the path for the rune index.
result = arrangement.font.typeface.getGlyphPath(arrangement.runes[index])
result.transform(
translate(arrangement.positions[index]) *
scale(vec2(arrangement.font.scale))
)
proc computeBounds*(spans: seq[Span]): Vec2 {.inline.} =
typeset(spans).computeBounds()
proc parseOtf*(buf: string): Font =
result.typeface = Typeface()

View file

@ -14,6 +14,6 @@ timeIt "typeset":
timeIt "rasterize":
image.fill(rgba(255, 255, 255, 255))
image.fillText(font, text, rgba(0, 0, 0, 255), bounds = image.wh)
image.fillText(font, text, bounds = image.wh)
# mask.fill(0)
# mask.fillText(font, text, bounds = mask.wh)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -13,7 +13,7 @@ block:
font.size = 24
let bounds = font.computeBounds("Word")
doAssert bounds == vec2(56.05078125, 28)
doAssert bounds == vec2(57, 28)
block:
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
@ -629,3 +629,63 @@ block:
image.fillText(font, "Text")
image.writeFile("tests/fonts/image_paint_fill.png")
block:
var font1 = readFont("tests/fonts/Roboto-Regular_1.ttf")
font1.size = 80
var font2 = readFont("tests/fonts/Aclonica-Regular_1.ttf")
font2.size = 100
var font3 = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
font3.size = 48
let spans = @[
newSpan("One span ", font1),
newSpan("Two span", font2),
newSpan(" Three span", font3)
]
let image = newImage(700, 250)
image.fill(rgba(255, 255, 255, 255))
let arrangement = typeset(spans, bounds = image.wh)
image.fillText(arrangement)
doDiff(image, "spans1")
for i, rect in arrangement.selectionRects:
image.fillRect(rect, rgba(128, 128, 128, 128))
doDiff(image, "selection_rects1")
block:
var font1 = readFont("tests/fonts/Roboto-Regular_1.ttf")
font1.size = 80
var font2 = readFont("tests/fonts/Aclonica-Regular_1.ttf")
font2.size = 100
var font3 = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
font3.size = 48
let spans = @[
newSpan("One span ", font1),
newSpan("Two span", font2),
newSpan(" Three span", font3)
]
let image = newImage(475, 400)
image.fill(rgba(255, 255, 255, 255))
let arrangement = typeset(spans, bounds = image.wh)
image.fillText(arrangement)
doDiff(image, "spans2")
for i, rect in arrangement.selectionRects:
image.fillRect(rect, rgba(128, 128, 128, 128))
doDiff(image, "selection_rects2")