mirror of
https://github.com/bloeys/nterm.git
synced 2025-12-29 06:28:20 +00:00
Accurate vertical positioning of text+return fractional bearing/descent
This commit is contained in:
@ -22,8 +22,8 @@ type FontAtlas struct {
|
|||||||
Glyphs map[rune]FontAtlasGlyph
|
Glyphs map[rune]FontAtlasGlyph
|
||||||
|
|
||||||
//Advance is global to the atlas because we only support monospaced fonts
|
//Advance is global to the atlas because we only support monospaced fonts
|
||||||
Advance int
|
Advance float32
|
||||||
LineHeight int
|
LineHeight float32
|
||||||
}
|
}
|
||||||
|
|
||||||
type FontAtlasGlyph struct {
|
type FontAtlasGlyph struct {
|
||||||
@ -120,16 +120,13 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Create atlas
|
//Create atlas
|
||||||
// atlasSizeXF32 := float32(atlasSizeX)
|
|
||||||
// atlasSizeYF32 := float32(atlasSizeY)
|
|
||||||
atlas := &FontAtlas{
|
atlas := &FontAtlas{
|
||||||
Font: f,
|
Font: f,
|
||||||
Img: image.NewRGBA(image.Rect(0, 0, atlasSizeX, atlasSizeY)),
|
Img: image.NewRGBA(image.Rect(0, 0, atlasSizeX, atlasSizeY)),
|
||||||
Glyphs: make(map[rune]FontAtlasGlyph, len(glyphs)),
|
Glyphs: make(map[rune]FontAtlasGlyph, len(glyphs)),
|
||||||
|
|
||||||
Advance: charAdv - charPaddingX,
|
Advance: float32(charAdv - charPaddingX),
|
||||||
LineHeight: lineHeight,
|
LineHeight: float32(lineHeight),
|
||||||
// SizeUV: *gglm.NewVec2(float32(charAdv-charPaddingX)/atlasSizeXF32, float32(lineHeight)/atlasSizeYF32),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Clear background to black
|
//Clear background to black
|
||||||
@ -145,9 +142,9 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
|
|||||||
charPaddingYFixed := fixed.I(charPaddingY)
|
charPaddingYFixed := fixed.I(charPaddingY)
|
||||||
|
|
||||||
charsOnLine := 0
|
charsOnLine := 0
|
||||||
drawer.Dot = fixed.P(atlas.Advance+charPaddingX, lineHeight)
|
drawer.Dot = fixed.P(int(atlas.Advance+charPaddingX), lineHeight)
|
||||||
const drawBoundingBoxes bool = false
|
|
||||||
|
|
||||||
|
const drawBoundingBoxes bool = false
|
||||||
for currGlyphCount, g := range glyphs {
|
for currGlyphCount, g := range glyphs {
|
||||||
|
|
||||||
gBounds, gAdvance, _ := face.GlyphBounds(g)
|
gBounds, gAdvance, _ := face.GlyphBounds(g)
|
||||||
@ -172,10 +169,10 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
|
|||||||
SizeU: float32(gBotRight.X - gTopLeft.X),
|
SizeU: float32(gBotRight.X - gTopLeft.X),
|
||||||
SizeV: float32(gBotRight.Y - gTopLeft.Y),
|
SizeV: float32(gBotRight.Y - gTopLeft.Y),
|
||||||
|
|
||||||
Ascent: float32(ascentAbsFixed.Ceil()),
|
Ascent: I26_6ToF32(ascentAbsFixed),
|
||||||
Descent: float32(descentAbsFixed.Ceil()),
|
Descent: I26_6ToF32(descentAbsFixed),
|
||||||
BearingX: float32(bearingX.Ceil()),
|
BearingX: I26_6ToF32(bearingX),
|
||||||
Advance: float32(gAdvance.Ceil()),
|
Advance: I26_6ToF32(gAdvance),
|
||||||
}
|
}
|
||||||
|
|
||||||
imgRect, mask, maskp, _, _ := face.Glyph(drawer.Dot, g)
|
imgRect, mask, maskp, _, _ := face.Glyph(drawer.Dot, g)
|
||||||
@ -197,7 +194,7 @@ func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*Fo
|
|||||||
if charsOnLine == charsPerLine || currGlyphCount == len(glyphs)-1 {
|
if charsOnLine == charsPerLine || currGlyphCount == len(glyphs)-1 {
|
||||||
|
|
||||||
charsOnLine = 0
|
charsOnLine = 0
|
||||||
drawer.Dot.X = fixed.I(atlas.Advance) + charPaddingXFixed
|
drawer.Dot.X = fixed.I(int(atlas.Advance)) + charPaddingXFixed
|
||||||
drawer.Dot.Y += lineHeightFixed + charPaddingYFixed
|
drawer.Dot.Y += lineHeightFixed + charPaddingYFixed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,6 +249,16 @@ func DrawRect(img *image.RGBA, rect image.Rectangle, color color.NRGBA) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func I26_6ToF32(x fixed.Int26_6) float32 {
|
||||||
|
const lower6BitMask = 1<<6 - 1
|
||||||
|
|
||||||
|
if x > 0 {
|
||||||
|
return float32(x.Floor()) + float32(x&lower6BitMask)/64
|
||||||
|
} else {
|
||||||
|
return float32(x.Floor()) - float32(x&lower6BitMask)/64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func DrawVerticalLine(img *image.RGBA, posX int, color color.NRGBA) {
|
func DrawVerticalLine(img *image.RGBA, posX int, color color.NRGBA) {
|
||||||
|
|
||||||
rowLength := img.Stride
|
rowLength := img.Stride
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package glyphs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/bloeys/gglm/gglm"
|
"github.com/bloeys/gglm/gglm"
|
||||||
@ -68,11 +70,8 @@ func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color
|
|||||||
}
|
}
|
||||||
|
|
||||||
pos := screenPos.Clone()
|
pos := screenPos.Clone()
|
||||||
advanceF32 := float32(gr.Atlas.Advance)
|
|
||||||
lineHeightF32 := float32(gr.Atlas.LineHeight)
|
lineHeightF32 := float32(gr.Atlas.LineHeight)
|
||||||
|
|
||||||
bufIndex := gr.GlyphCount * floatsPerGlyph
|
bufIndex := gr.GlyphCount * floatsPerGlyph
|
||||||
|
|
||||||
for runIndex := 0; runIndex < len(runs); runIndex++ {
|
for runIndex := 0; runIndex < len(runs); runIndex++ {
|
||||||
|
|
||||||
run := &runs[runIndex]
|
run := &runs[runIndex]
|
||||||
@ -81,14 +80,14 @@ func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color
|
|||||||
if run.IsLtr {
|
if run.IsLtr {
|
||||||
|
|
||||||
for i := 0; i < len(run.Runes); i++ {
|
for i := 0; i < len(run.Runes); i++ {
|
||||||
gr.drawRune(run, i, prevRune, screenPos, pos, color, advanceF32, lineHeightF32, &bufIndex)
|
gr.drawRune(run, i, prevRune, screenPos, pos, color, lineHeightF32, &bufIndex)
|
||||||
prevRune = run.Runes[i]
|
prevRune = run.Runes[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
for i := len(run.Runes) - 1; i >= 0; i-- {
|
for i := len(run.Runes) - 1; i >= 0; i-- {
|
||||||
gr.drawRune(run, i, prevRune, screenPos, pos, color, advanceF32, lineHeightF32, &bufIndex)
|
gr.drawRune(run, i, prevRune, screenPos, pos, color, lineHeightF32, &bufIndex)
|
||||||
prevRune = run.Runes[i]
|
prevRune = run.Runes[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,21 +95,20 @@ func (gr *GlyphRend) DrawTextOpenGLAbs(text string, screenPos *gglm.Vec3, color
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gr *GlyphRend) drawRune(run *TextRun, i int, prevRune rune, screenPos, pos *gglm.Vec3, color *gglm.Vec4, advanceF32, lineHeightF32 float32, bufIndex *uint32) {
|
var PrintPositions bool
|
||||||
|
|
||||||
|
func (gr *GlyphRend) drawRune(run *TextRun, i int, prevRune rune, screenPos, pos *gglm.Vec3, color *gglm.Vec4, lineHeightF32 float32, bufIndex *uint32) {
|
||||||
|
|
||||||
r := run.Runes[i]
|
r := run.Runes[i]
|
||||||
if r == '\n' {
|
if r == '\n' {
|
||||||
screenPos.SetY(screenPos.Y() - lineHeightF32)
|
screenPos.SetY(screenPos.Y() - lineHeightF32)
|
||||||
*pos = *screenPos.Clone()
|
*pos = *screenPos.Clone()
|
||||||
// prevRune = r
|
|
||||||
return
|
return
|
||||||
} else if r == ' ' {
|
} else if r == ' ' {
|
||||||
pos.AddX(advanceF32)
|
pos.AddX(gr.Atlas.Advance)
|
||||||
// prevRune = r
|
|
||||||
return
|
return
|
||||||
} else if r == '\t' {
|
} else if r == '\t' {
|
||||||
pos.AddX(advanceF32 * float32(gr.SpacesPerTab))
|
pos.AddX(gr.Atlas.Advance * float32(gr.SpacesPerTab))
|
||||||
// prevRune = r
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,8 +133,16 @@ func (gr *GlyphRend) drawRune(run *TextRun, i int, prevRune rune, screenPos, pos
|
|||||||
|
|
||||||
//We must adjust char positioning according to: https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png
|
//We must adjust char positioning according to: https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png
|
||||||
drawPos := *pos
|
drawPos := *pos
|
||||||
drawPos.SetX(drawPos.X() + g.BearingX)
|
//The flooring to an integer pixel must happen AFTER the (potentially) fractional adjustments have been made.
|
||||||
drawPos.SetY(drawPos.Y() - g.Descent)
|
//This is what the truetype face.Rasterizer does and seems to give good results
|
||||||
|
drawPos.SetX(floorF32(drawPos.X() + g.BearingX))
|
||||||
|
drawPos.SetY(floorF32(drawPos.Y() - g.Descent))
|
||||||
|
|
||||||
|
if PrintPositions {
|
||||||
|
oldXY := gglm.NewVec2(pos.X(), pos.Y())
|
||||||
|
newXY := gglm.NewVec2(drawPos.X(), drawPos.Y())
|
||||||
|
fmt.Printf("char=%s; PosBefore=%s, PosAfter=%s; Bearing/Decent=(%f, %f)\n", string(r), oldXY.String(), newXY.String(), g.BearingX, g.Descent)
|
||||||
|
}
|
||||||
|
|
||||||
//Add the glyph information to the vbo
|
//Add the glyph information to the vbo
|
||||||
//UV
|
//UV
|
||||||
@ -168,7 +174,8 @@ func (gr *GlyphRend) drawRune(run *TextRun, i int, prevRune rune, screenPos, pos
|
|||||||
*bufIndex += 2
|
*bufIndex += 2
|
||||||
|
|
||||||
gr.GlyphCount++
|
gr.GlyphCount++
|
||||||
pos.AddX(g.Advance)
|
pos.AddX(gr.Atlas.Advance)
|
||||||
|
// pos.AddX(g.Advance)
|
||||||
|
|
||||||
//If we fill the buffer we issue a draw call
|
//If we fill the buffer we issue a draw call
|
||||||
if gr.GlyphCount == MaxGlyphsPerBatch {
|
if gr.GlyphCount == MaxGlyphsPerBatch {
|
||||||
@ -177,6 +184,18 @@ func (gr *GlyphRend) drawRune(run *TextRun, i int, prevRune rune, screenPos, pos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func roundF32(x float32) float32 {
|
||||||
|
return float32(math.Round(float64(x)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ceilF32(x float32) float32 {
|
||||||
|
return float32(math.Ceil(float64(x)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func floorF32(x float32) float32 {
|
||||||
|
return float32(math.Floor(float64(x)))
|
||||||
|
}
|
||||||
|
|
||||||
type TextRun struct {
|
type TextRun struct {
|
||||||
Runes []rune
|
Runes []rune
|
||||||
IsLtr bool
|
IsLtr bool
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[Window][Debug##Default]
|
[Window][Debug##Default]
|
||||||
Pos=814,53
|
Pos=814,53
|
||||||
Size=399,134
|
Size=368,157
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
|
|||||||
7
main.go
7
main.go
@ -63,7 +63,7 @@ func main() {
|
|||||||
rend: rend,
|
rend: rend,
|
||||||
imguiInfo: nmageimgui.NewImGUI(),
|
imguiInfo: nmageimgui.NewImGUI(),
|
||||||
|
|
||||||
FontSize: 14,
|
FontSize: 36,
|
||||||
}
|
}
|
||||||
|
|
||||||
p.win.EventCallbacks = append(p.win.EventCallbacks, func(e sdl.Event) {
|
p.win.EventCallbacks = append(p.win.EventCallbacks, func(e sdl.Event) {
|
||||||
@ -185,6 +185,7 @@ func (p *program) Update() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
imgui.Checkbox("Draw many", &drawManyLines)
|
imgui.Checkbox("Draw many", &drawManyLines)
|
||||||
|
glyphs.PrintPositions = imgui.Button("Print positions")
|
||||||
}
|
}
|
||||||
|
|
||||||
var isDrawingBounds = false
|
var isDrawingBounds = false
|
||||||
@ -236,7 +237,7 @@ func (p *program) Render() {
|
|||||||
} else {
|
} else {
|
||||||
charsPerFrame := float64(charCount)
|
charsPerFrame := float64(charCount)
|
||||||
p.GlyphRend.DrawTextOpenGLAbs(str, gglm.NewVec3(xOff, float32(p.GlyphRend.Atlas.LineHeight)*5+yOff, 0), textColor)
|
p.GlyphRend.DrawTextOpenGLAbs(str, gglm.NewVec3(xOff, float32(p.GlyphRend.Atlas.LineHeight)*5+yOff, 0), textColor)
|
||||||
p.win.SDLWin.SetTitle(fmt.Sprint("FPS: ", fps, " Draws/f: ", math.Ceil(charsPerFrame/glyphs.MaxGlyphsPerBatch), " chars/f: ", charsPerFrame, " chars/s: ", fps*int(charsPerFrame)))
|
p.win.SDLWin.SetTitle(fmt.Sprint("FPS: ", fps, " Draws/f: ", math.Ceil(charsPerFrame/glyphs.MaxGlyphsPerBatch), " chars/f: ", int(charsPerFrame), " chars/s: ", fps*int(charsPerFrame)))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -259,7 +260,7 @@ func (p *program) drawGrid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *program) FrameEnd() {
|
func (p *program) FrameEnd() {
|
||||||
|
// engine.Quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *program) DeInit() {
|
func (p *program) DeInit() {
|
||||||
|
|||||||
46
main_test.go
Executable file
46
main_test.go
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bloeys/nterm/glyphs"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestI26_6ToF32(t *testing.T) {
|
||||||
|
|
||||||
|
x := fixed.I(55)
|
||||||
|
var ans float32 = 55
|
||||||
|
Check(t, ans, glyphs.I26_6ToF32(x))
|
||||||
|
|
||||||
|
x = fixed.I(-10)
|
||||||
|
ans = -10
|
||||||
|
Check(t, ans, glyphs.I26_6ToF32(x))
|
||||||
|
|
||||||
|
x = fixed.Int26_6(0<<6 + 1<<0)
|
||||||
|
ans = 1 / 64.0
|
||||||
|
Check(t, ans, glyphs.I26_6ToF32(x))
|
||||||
|
|
||||||
|
x = fixed.Int26_6(12<<6 + 0<<0)
|
||||||
|
ans = 12
|
||||||
|
Check(t, ans, glyphs.I26_6ToF32(x))
|
||||||
|
|
||||||
|
x = fixed.Int26_6(-3<<6 + 1<<2)
|
||||||
|
ans = -(3.0 + 4/64.0)
|
||||||
|
Check(t, ans, glyphs.I26_6ToF32(x))
|
||||||
|
|
||||||
|
//Test min/max values
|
||||||
|
x = fixed.I(33554431)
|
||||||
|
ans = 33554431
|
||||||
|
Check(t, ans, glyphs.I26_6ToF32(x))
|
||||||
|
|
||||||
|
x = fixed.I(-33554432)
|
||||||
|
ans = -33554432
|
||||||
|
Check(t, ans, glyphs.I26_6ToF32(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Check[T comparable](t *testing.T, expected, got T) {
|
||||||
|
if got != expected {
|
||||||
|
t.Fatalf("Expected %v but got %v\n", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user