underline, strikethrough

This commit is contained in:
Ryan Oldenburg 2021-05-31 20:33:11 -05:00
parent 8ce82c20b2
commit 3b6474b565
40 changed files with 248 additions and 24 deletions

View file

@ -304,11 +304,11 @@ type
lookupList: LookupList
PostTable = ref object
version: float32
italicAngle: float32
underlinePosition: int16
underlineThickness: int16
isFixedPitch: uint32
version*: float32
italicAngle*: float32
underlinePosition*: int16
underlineThickness*: int16
isFixedPitch*: uint32
OpenType* = ref object
buf*: string

View file

@ -18,6 +18,8 @@ type
lineHeight*: float32 ## The line height in pixels or AutoLineHeight for the font's default line height.
paint*: Paint
textCase*: TextCase
underline*: bool ## Apply an underline.
strikethrough*: bool ## Apply a strikethrough.
noKerningAdjustments*: bool ## Optionally disable kerning pair adjustments
Span* = ref object
@ -25,6 +27,7 @@ type
font*: Font
Arrangement* = ref object
lines*: seq[(int, int)] ## The (start, stop) of the lines of text.
spans*: seq[(int, int)] ## The (start, stop) of the spans in the text.
fonts*: seq[Font] ## The font for each span.
runes*: seq[Rune] ## The runes of the text.
@ -72,6 +75,22 @@ proc lineHeight*(typeface: Typeface): float32 {.inline.} =
## The default line height in font units.
typeface.ascent - typeface.descent + typeface.lineGap
proc underlinePosition(typeface: Typeface): float32 {.inline.} =
if typeface.opentype != nil:
result = typeface.opentype.post.underlinePosition.float32
proc underlineThickness(typeface: Typeface): float32 {.inline.} =
if typeface.opentype != nil:
result = typeface.opentype.post.underlineThickness.float32
proc strikeoutPosition(typeface: Typeface): float32 {.inline.} =
if typeface.opentype != nil:
result = typeface.opentype.os2.yStrikeoutPosition.float32
proc strikeoutThickness(typeface: Typeface): float32 {.inline.} =
if typeface.opentype != nil:
result = typeface.opentype.os2.yStrikeoutSize.float32
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)
@ -177,7 +196,7 @@ proc typeset*(
result.positions.setLen(result.runes.len)
result.selectionRects.setLen(result.runes.len)
var lines = @[(0, 0)] # (start, stop)
result.lines = @[(0, 0)] # (start, stop)
block: # Arrange the glyphs horizontally first (handling line breaks)
proc advance(font: Font, runes: seq[Rune], i: int): float32 {.inline.} =
@ -200,10 +219,10 @@ proc typeset*(
at.x = 0
at.y += 1
prevCanWrap = 0
lines[^1][1] = runeIndex
result.lines[^1][1] = runeIndex
# Start a new line if we are not at the end
if runeIndex + 1 < result.runes.len:
lines.add((runeIndex + 1, 0))
result.lines.add((runeIndex + 1, 0))
else:
let advance = advance(font, result.runes, runeIndex)
if wrap and rune != SP and bounds.x > 0 and at.x + advance > bounds.x:
@ -221,8 +240,8 @@ proc typeset*(
at.x += advance(font, result.runes, i)
dec lineStart
lines[^1][1] = lineStart - 1
lines.add((lineStart, 0))
result.lines[^1][1] = lineStart - 1
result.lines.add((lineStart, 0))
if rune.canWrap():
prevCanWrap = runeIndex
@ -231,12 +250,12 @@ proc typeset*(
result.selectionRects[runeIndex] = rect(at.x, at.y, advance, 0)
at.x += advance
lines[^1][1] = result.runes.len - 1
result.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.
for (start, stop) in lines:
for (start, stop) in result.lines:
var furthestX: float32
for i in countdown(stop, start):
if result.runes[i] != SP and result.runes[i] != LF:
@ -289,11 +308,11 @@ proc typeset*(
) / 2
maxInitialY = max(maxInitialY, round(fontUnitInitialY * font.scale))
for runeIndex in start .. stop:
if runeIndex == lines[0][1]:
if runeIndex == result.lines[0][1]:
break outer
maxInitialY
var lineHeights = newSeq[float32](lines.len)
var lineHeights = newSeq[float32](result.lines.len)
block: # Compute each line's line height
var line: int
for spanIndex, (start, stop) in result.spans:
@ -306,11 +325,12 @@ proc typeset*(
font.defaultLineHeight
lineHeights[line] = max(lineHeights[line], fontLineHeight)
for runeIndex in start .. stop:
if line + 1 < lines.len and runeIndex == lines[line + 1][0]:
if line + 1 < result.lines.len and
runeIndex == result.lines[line + 1][0]:
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]:
if line + 1 < result.lines.len and stop == result.lines[line][1]:
inc line
block: # Vertically position the glyphs
@ -326,7 +346,8 @@ proc typeset*(
else:
font.defaultLineHeight
for runeIndex in start .. stop:
if line + 1 < lines.len and runeIndex == lines[line + 1][0]:
if line + 1 < result.lines.len and
runeIndex == result.lines[line + 1][0]:
inc line
baseline += lineHeights[line]
result.positions[runeIndex].y = baseline
@ -410,14 +431,46 @@ proc fillText*(
transform: Vec2 | Mat3 = vec2(0, 0)
) =
## Fills the text arrangement.
var line: int
for spanIndex, (start, stop) in arrangement.spans:
let font = arrangement.fonts[spanIndex]
let
font = arrangement.fonts[spanIndex]
underlineThickness = font.typeface.underlineThickness * font.scale
underlinePosition = font.typeface.underlinePosition * font.scale
strikeoutThickness = font.typeface.strikeoutThickness * font.scale
strikeoutPosition = font.typeface.strikeoutPosition * font.scale
for runeIndex in start .. stop:
let position = arrangement.positions[runeIndex]
var path = font.typeface.getGlyphPath(arrangement.runes[runeIndex])
path.transform(
translate(arrangement.positions[runeIndex]) *
translate(position) *
scale(vec2(font.scale))
)
var applyDecoration = true
if runeIndex == arrangement.lines[line][1]:
inc line
if arrangement.runes[runeIndex] == SP:
# Do not apply decoration to the space at end of lines
applyDecoration = false
if applyDecoration:
if font.underline:
path.rect(
arrangement.selectionRects[runeIndex].x,
position.y - underlinePosition + underlineThickness / 2,
arrangement.selectionRects[runeIndex].w,
underlineThickness
)
elif font.strikethrough:
path.rect(
arrangement.selectionRects[runeIndex].x,
position.y - strikeoutPosition,
arrangement.selectionRects[runeIndex].w,
strikeoutThickness
)
when type(target) is Image:
target.fillPath(path, font.paint, transform)
else: # target is Mask
@ -450,14 +503,46 @@ proc strokeText*(
dashes: seq[float32] = @[]
) =
## Strokes the text arrangement.
var line: int
for spanIndex, (start, stop) in arrangement.spans:
let font = arrangement.fonts[spanIndex]
let
font = arrangement.fonts[spanIndex]
underlineThickness = font.typeface.underlineThickness * font.scale
underlinePosition = font.typeface.underlinePosition * font.scale
strikeoutThickness = font.typeface.strikeoutThickness * font.scale
strikeoutPosition = font.typeface.strikeoutPosition * font.scale
for runeIndex in start .. stop:
let position = arrangement.positions[runeIndex]
var path = font.typeface.getGlyphPath(arrangement.runes[runeIndex])
path.transform(
translate(arrangement.positions[runeIndex]) *
translate(position) *
scale(vec2(font.scale))
)
var applyDecoration = true
if runeIndex == arrangement.lines[line][1]:
inc line
if arrangement.runes[runeIndex] == SP:
# Do not apply decoration to the space at end of lines
applyDecoration = false
if applyDecoration:
if font.underline:
path.rect(
arrangement.selectionRects[runeIndex].x,
position.y - underlinePosition + underlineThickness / 2,
arrangement.selectionRects[runeIndex].w,
underlineThickness
)
elif font.strikethrough:
path.rect(
arrangement.selectionRects[runeIndex].x,
position.y - strikeoutPosition,
arrangement.selectionRects[runeIndex].w,
strikeoutThickness
)
when type(target) is Image:
target.strokePath(
path,

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 961 B

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 865 B

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -781,18 +781,18 @@ block:
var font1 = ubuntu
font1.size = 15
font1.paint.color = parseHtmlColor("#CACACA").rgba()
font1.paint = "#CACACA"
var font2 = ubuntu
font2.size = 84
var font3 = ubuntu
font3.size = 18
font3.paint.color = parseHtmlColor("#007FF4").rgba()
font3.paint = "#007FF4"
var font4 = ubuntu
font4.size = 20
font4.paint.color = parseHtmlColor("#4F4F4F").rgba()
font4.paint = "#4F4F4F"
let spans = @[
newSpan("verb [with object] ", font1),
@ -809,3 +809,142 @@ block:
image.fillText(arrangement, vec2(20, 20))
doDiff(image, "spans5")
block:
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
font.size = 24
font.underline = true
let image = newImage(200, 100)
image.fill(rgba(255, 255, 255, 255))
image.fillText(
font,
"Wrapping text to new line",
bounds = vec2(200, 0)
)
doDiff(image, "underline1")
block:
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
font.size = 24
font.underline = true
font.paint = rgba(0, 0, 0, 127)
let image = newImage(200, 100)
image.fill(rgba(255, 255, 255, 255))
image.fillText(
font,
"Wrapping text to new line",
bounds = vec2(200, 0)
)
doDiff(image, "underline2")
block:
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
font.size = 24
font.underline = true
let image = newImage(200, 100)
image.fill(rgba(255, 255, 255, 255))
image.strokeText(
font,
"Wrapping text to new line",
bounds = vec2(200, 0)
)
doDiff(image, "underline3")
block:
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
font.size = 24
font.strikethrough = true
let image = newImage(200, 100)
image.fill(rgba(255, 255, 255, 255))
image.fillText(
font,
"Wrapping text to new line",
bounds = vec2(200, 0)
)
doDiff(image, "strikethrough1")
block:
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
font.size = 24
font.strikethrough = true
font.paint = rgba(0, 0, 0, 127)
let image = newImage(200, 100)
image.fill(rgba(255, 255, 255, 255))
image.fillText(
font,
"Wrapping text to new line",
bounds = vec2(200, 0)
)
doDiff(image, "strikethrough2")
block:
var font = readFont("tests/fonts/Roboto-Regular_1.ttf")
font.size = 24
font.strikethrough = true
let image = newImage(200, 100)
image.fill(rgba(255, 255, 255, 255))
image.strokeText(
font,
"Wrapping text to new line",
bounds = vec2(200, 0)
)
doDiff(image, "strikethrough3")
block:
let ubuntu = readFont("tests/fonts/Ubuntu-Regular_1.ttf")
var font1 = ubuntu
font1.size = 15
font1.paint = "#CACACA"
var font2 = ubuntu
font2.size = 84
var font3 = ubuntu
font3.size = 18
font3.paint = "#007FF4"
var font4 = ubuntu
font4.size = 20
font4.paint = "#4F4F4F"
var font5 = ubuntu
font5.size = 20
font5.paint = "#4F4F4F"
font5.underline = true
var font6 = ubuntu
font6.size = 20
font6.paint = "#4F4F4F"
font6.strikethrough = true
let spans = @[
newSpan("verb [with object] ", font1),
newSpan("strallow\n", font2),
newSpan("\nstral·low\n", font3),
newSpan("\n1. free (something) from ", font4),
newSpan("restrictive restrictions", font5),
newSpan(" ", font4),
newSpan("\"the regulations are intended to strallow changes in public policy\" ", font6)
]
let image = newImage(400, 400)
image.fill(rgba(255, 255, 255, 255))
let arrangement = typeset(spans, bounds = vec2(360, 360))
image.fillText(arrangement, vec2(20, 20))
doDiff(image, "spans6")