mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-10 04:57:26 +01:00
parent
46600b42f9
commit
fe35180b78
@ -22,115 +22,62 @@ import (
|
||||
_ "embed"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"log"
|
||||
"math"
|
||||
|
||||
"github.com/go-text/typesetting/di"
|
||||
"github.com/go-text/typesetting/font"
|
||||
"github.com/go-text/typesetting/language"
|
||||
"github.com/go-text/typesetting/opentype/api"
|
||||
"github.com/go-text/typesetting/shaping"
|
||||
"golang.org/x/image/math/fixed"
|
||||
"golang.org/x/image/vector"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
)
|
||||
|
||||
//go:embed NotoSansArabic-Regular.ttf
|
||||
var arabicTTF []byte
|
||||
|
||||
var arabicOut shaping.Output
|
||||
var arabicFaceSource *text.GoTextFaceSource
|
||||
|
||||
func init() {
|
||||
face, err := font.ParseTTF(bytes.NewReader(arabicTTF))
|
||||
s, err := text.NewGoTextFaceSource(bytes.NewReader(arabicTTF))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
runes := []rune("لمّا كان الاعتراف بالكرامة المتأصلة في جميع")
|
||||
input := shaping.Input{
|
||||
Text: runes,
|
||||
RunStart: 0,
|
||||
RunEnd: len(runes),
|
||||
Direction: di.DirectionRTL,
|
||||
Face: face,
|
||||
Size: fixed.I(24),
|
||||
Script: language.Arabic,
|
||||
Language: "ar",
|
||||
}
|
||||
arabicOut = (&shaping.HarfbuzzShaper{}).Shape(input)
|
||||
arabicFaceSource = s
|
||||
}
|
||||
|
||||
//go:embed NotoSansDevanagari-Regular.ttf
|
||||
var devanagariTTF []byte
|
||||
|
||||
var devanagariOut shaping.Output
|
||||
var devanagariFaceSource *text.GoTextFaceSource
|
||||
|
||||
func init() {
|
||||
face, err := font.ParseTTF(bytes.NewReader(devanagariTTF))
|
||||
s, err := text.NewGoTextFaceSource(bytes.NewReader(devanagariTTF))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
runes := []rune("चूंकि मानव परिवार के सभी सदस्यों के जन्मजात गौरव और समान")
|
||||
input := shaping.Input{
|
||||
Text: runes,
|
||||
RunStart: 0,
|
||||
RunEnd: len(runes),
|
||||
Direction: di.DirectionLTR,
|
||||
Face: face,
|
||||
Size: fixed.I(24),
|
||||
Script: language.Devanagari,
|
||||
Language: "hi",
|
||||
}
|
||||
devanagariOut = (&shaping.HarfbuzzShaper{}).Shape(input)
|
||||
devanagariFaceSource = s
|
||||
}
|
||||
|
||||
//go:embed NotoSansThai-Regular.ttf
|
||||
var thaiTTF []byte
|
||||
|
||||
var thaiOut shaping.Output
|
||||
var thaiFaceSource *text.GoTextFaceSource
|
||||
|
||||
func init() {
|
||||
face, err := font.ParseTTF(bytes.NewReader(thaiTTF))
|
||||
s, err := text.NewGoTextFaceSource(bytes.NewReader(thaiTTF))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
runes := []rune("โดยที่การไม่นำพาและการหมิ่นในคุณค่าของสิทธิมนุษยชน")
|
||||
input := shaping.Input{
|
||||
Text: runes,
|
||||
RunStart: 0,
|
||||
RunEnd: len(runes),
|
||||
Direction: di.DirectionLTR,
|
||||
Face: face,
|
||||
Size: fixed.I(24),
|
||||
Script: language.Thai,
|
||||
Language: "th",
|
||||
}
|
||||
thaiOut = (&shaping.HarfbuzzShaper{}).Shape(input)
|
||||
thaiFaceSource = s
|
||||
}
|
||||
|
||||
var japaneseOut shaping.Output
|
||||
var japaneseFaceSource *text.GoTextFaceSource
|
||||
|
||||
func init() {
|
||||
const japanese = language.Script(('j' << 24) | ('p' << 16) | ('a' << 8) | 'n')
|
||||
|
||||
face, err := font.ParseTTF(bytes.NewReader(fonts.MPlus1pRegular_ttf))
|
||||
s, err := text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
runes := []rune("ラーメン。")
|
||||
input := shaping.Input{
|
||||
Text: runes,
|
||||
RunStart: 0,
|
||||
RunEnd: len(runes),
|
||||
Direction: di.DirectionTTB,
|
||||
Face: face,
|
||||
Size: fixed.I(24),
|
||||
Script: japanese,
|
||||
Language: "ja",
|
||||
}
|
||||
japaneseOut = (&shaping.HarfbuzzShaper{}).Shape(input)
|
||||
japaneseFaceSource = s
|
||||
}
|
||||
|
||||
var (
|
||||
@ -151,21 +98,6 @@ const (
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
vertices []ebiten.Vertex
|
||||
indices []uint16
|
||||
|
||||
glyphCache map[glyphCacheKey]glyphCacheValue
|
||||
}
|
||||
|
||||
type glyphCacheKey struct {
|
||||
output *shaping.Output // TODO: This should be a font.Face instead of shaping.Output.
|
||||
glyphID api.GID
|
||||
origin fixed.Point26_6
|
||||
}
|
||||
|
||||
type glyphCacheValue struct {
|
||||
image *ebiten.Image
|
||||
point image.Point
|
||||
}
|
||||
|
||||
func (g *Game) Update() error {
|
||||
@ -173,67 +105,47 @@ func (g *Game) Update() error {
|
||||
}
|
||||
|
||||
func (g *Game) Draw(screen *ebiten.Image) {
|
||||
g.drawGlyphs(screen, &arabicOut, 20, 100)
|
||||
g.drawGlyphs(screen, &devanagariOut, 20, 150)
|
||||
g.drawGlyphs(screen, &thaiOut, 20, 200)
|
||||
g.drawGlyphs(screen, &japaneseOut, 20, 250)
|
||||
}
|
||||
const arabicText = "لمّا كان الاعتراف بالكرامة المتأصلة في جميع"
|
||||
|
||||
func fixed26_6ToFloat32(x fixed.Int26_6) float32 {
|
||||
return float32(x>>6) + (float32(x&(1<<6-1)) / (1 << 6))
|
||||
}
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Translate(screenWidth-20, 100)
|
||||
text.Draw(screen, arabicText, &text.GoTextFace{
|
||||
Source: arabicFaceSource,
|
||||
Direction: text.DirectionRightToLeft,
|
||||
SizeInPoints: 24,
|
||||
Language: language.Arabic,
|
||||
}, op)
|
||||
|
||||
func float32ToFixed26_6(x float32) fixed.Int26_6 {
|
||||
i := float32(math.Floor(float64(x)))
|
||||
return (fixed.Int26_6(i) << 6) + fixed.Int26_6((x-i)*(1<<6))
|
||||
}
|
||||
const hindiText = "चूंकि मानव परिवार के सभी सदस्यों के जन्मजात गौरव और समान"
|
||||
|
||||
func (g *Game) drawGlyphs(dst *ebiten.Image, output *shaping.Output, originX, originY float32) {
|
||||
g.vertices = g.vertices[:0]
|
||||
g.indices = g.indices[:0]
|
||||
op.GeoM.Reset()
|
||||
op.GeoM.Translate(20, 150)
|
||||
text.Draw(screen, hindiText, &text.GoTextFace{
|
||||
Source: devanagariFaceSource,
|
||||
SizeInPoints: 24,
|
||||
Language: language.Hindi,
|
||||
}, op)
|
||||
|
||||
scale := fixed26_6ToFloat32(output.Size) / float32(output.Face.Font.Upem())
|
||||
const thaiText = "โดยที่การไม่นำพาและการหมิ่นในคุณค่าของสิทธิมนุษยชน"
|
||||
|
||||
orig := fixed.Point26_6{
|
||||
X: float32ToFixed26_6(originX),
|
||||
Y: float32ToFixed26_6(originY),
|
||||
}
|
||||
for _, glyph := range output.Glyphs {
|
||||
key := glyphCacheKey{
|
||||
output: output,
|
||||
glyphID: glyph.GlyphID,
|
||||
origin: orig,
|
||||
}
|
||||
op.GeoM.Reset()
|
||||
op.GeoM.Translate(20, 200)
|
||||
text.Draw(screen, thaiText, &text.GoTextFace{
|
||||
Source: thaiFaceSource,
|
||||
SizeInPoints: 24,
|
||||
Language: language.Thai,
|
||||
}, op)
|
||||
|
||||
v, ok := g.glyphCache[key]
|
||||
if !ok {
|
||||
data := output.Face.GlyphData(glyph.GlyphID).(api.GlyphOutline)
|
||||
if len(data.Segments) > 0 {
|
||||
segs := make([]api.Segment, len(data.Segments))
|
||||
for i, seg := range data.Segments {
|
||||
segs[i] = seg
|
||||
for j := range seg.Args {
|
||||
segs[i].Args[j].X *= scale
|
||||
segs[i].Args[j].Y *= scale
|
||||
segs[i].Args[j].Y *= -1
|
||||
}
|
||||
}
|
||||
v.image, v.point = segmentsToImage(segs, orig)
|
||||
}
|
||||
if g.glyphCache == nil {
|
||||
g.glyphCache = map[glyphCacheKey]glyphCacheValue{}
|
||||
}
|
||||
g.glyphCache[key] = v
|
||||
}
|
||||
const japaneseText = "ラーメン。"
|
||||
|
||||
if v.image != nil {
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Translate(float64(v.point.X), float64(v.point.Y))
|
||||
dst.DrawImage(v.image, op)
|
||||
}
|
||||
|
||||
orig = orig.Add(fixed.Point26_6{X: glyph.XAdvance, Y: glyph.YAdvance * -1})
|
||||
}
|
||||
op.GeoM.Reset()
|
||||
op.GeoM.Translate(20, 250)
|
||||
text.Draw(screen, japaneseText, &text.GoTextFace{
|
||||
Source: japaneseFaceSource,
|
||||
Direction: text.DirectionTopToBottomAndRightToLeft,
|
||||
SizeInPoints: 24,
|
||||
Language: language.Thai,
|
||||
}, op)
|
||||
}
|
||||
|
||||
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||
@ -247,94 +159,3 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func segmentsToRect(segs []api.Segment) fixed.Rectangle26_6 {
|
||||
if len(segs) == 0 {
|
||||
return fixed.Rectangle26_6{}
|
||||
}
|
||||
|
||||
minX := float32(math.Inf(1))
|
||||
minY := float32(math.Inf(1))
|
||||
maxX := float32(math.Inf(-1))
|
||||
maxY := float32(math.Inf(-1))
|
||||
|
||||
for _, seg := range segs {
|
||||
n := 1
|
||||
switch seg.Op {
|
||||
case api.SegmentOpQuadTo:
|
||||
n = 2
|
||||
case api.SegmentOpCubeTo:
|
||||
n = 3
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
x := seg.Args[i].X
|
||||
y := seg.Args[i].Y
|
||||
if minX > x {
|
||||
minX = x
|
||||
}
|
||||
if minY > y {
|
||||
minY = y
|
||||
}
|
||||
if maxX < x {
|
||||
maxX = x
|
||||
}
|
||||
if maxY < y {
|
||||
maxY = y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fixed.Rectangle26_6{
|
||||
Min: fixed.Point26_6{
|
||||
X: float32ToFixed26_6(minX),
|
||||
Y: float32ToFixed26_6(minY),
|
||||
},
|
||||
Max: fixed.Point26_6{
|
||||
X: float32ToFixed26_6(maxX),
|
||||
Y: float32ToFixed26_6(maxY),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func segmentsToImage(segs []api.Segment, orig fixed.Point26_6) (*ebiten.Image, image.Point) {
|
||||
dBounds := segmentsToRect(segs).Add(orig)
|
||||
dr := image.Rect(
|
||||
dBounds.Min.X.Floor(),
|
||||
dBounds.Min.Y.Floor(),
|
||||
dBounds.Max.X.Ceil(),
|
||||
dBounds.Max.Y.Ceil(),
|
||||
)
|
||||
biasX := fixed26_6ToFloat32(orig.X) - float32(dr.Min.X)
|
||||
biasY := fixed26_6ToFloat32(orig.Y) - float32(dr.Min.Y)
|
||||
|
||||
width, height := dr.Dx(), dr.Dy()
|
||||
if width <= 0 || height <= 0 {
|
||||
return nil, image.Point{}
|
||||
}
|
||||
|
||||
rast := vector.NewRasterizer(width, height)
|
||||
rast.DrawOp = draw.Src
|
||||
for _, seg := range segs {
|
||||
switch seg.Op {
|
||||
case api.SegmentOpMoveTo:
|
||||
rast.MoveTo(seg.Args[0].X+biasX, seg.Args[0].Y+biasY)
|
||||
case api.SegmentOpLineTo:
|
||||
rast.LineTo(seg.Args[0].X+biasX, seg.Args[0].Y+biasY)
|
||||
case api.SegmentOpQuadTo:
|
||||
rast.QuadTo(
|
||||
seg.Args[0].X+biasX, seg.Args[0].Y+biasY,
|
||||
seg.Args[1].X+biasX, seg.Args[1].Y+biasY,
|
||||
)
|
||||
case api.SegmentOpCubeTo:
|
||||
rast.CubeTo(
|
||||
seg.Args[0].X+biasX, seg.Args[0].Y+biasY,
|
||||
seg.Args[1].X+biasX, seg.Args[1].Y+biasY,
|
||||
seg.Args[2].X+biasX, seg.Args[2].Y+biasY,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dst := image.NewAlpha(image.Rect(0, 0, width, height))
|
||||
rast.Draw(dst, dst.Bounds(), image.Opaque, image.Point{})
|
||||
return ebiten.NewImageFromImage(dst), dr.Min
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ package text
|
||||
|
||||
import (
|
||||
"math"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/image/math/fixed"
|
||||
@ -40,10 +39,24 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
type faceCacheKey struct {
|
||||
stdFaceID uint64
|
||||
|
||||
goTextFaceSourceID uint64
|
||||
goTextFaceDirection Direction
|
||||
goTextFaceSizeInPoints float64
|
||||
goTextFaceLanguage string
|
||||
goTextFaceScript string
|
||||
goTextFaceVariations string
|
||||
goTextFaceFeatures string
|
||||
}
|
||||
|
||||
type glyphImageCacheKey struct {
|
||||
// For StdFace
|
||||
rune rune
|
||||
// id is rune for StdFace, and GID for GoTextFace.
|
||||
id uint32
|
||||
|
||||
xoffset fixed.Int26_6
|
||||
yoffset fixed.Int26_6
|
||||
}
|
||||
|
||||
type glyphImageCacheEntry struct {
|
||||
@ -109,10 +122,13 @@ func (g *glyphImageCache) getOrCreate(face Face, key glyphImageCacheKey, create
|
||||
return img
|
||||
}
|
||||
|
||||
func (g *glyphImageCache) clear(face Face) {
|
||||
runtime.SetFinalizer(face, nil)
|
||||
|
||||
func (g *glyphImageCache) clear(comp func(key faceCacheKey) bool) {
|
||||
g.m.Lock()
|
||||
defer g.m.Unlock()
|
||||
delete(g.cache, face.faceCacheKey())
|
||||
|
||||
for key := range g.cache {
|
||||
if comp(key) {
|
||||
delete(g.cache, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
347
text/v2/gotext.go
Normal file
347
text/v2/gotext.go
Normal file
@ -0,0 +1,347 @@
|
||||
// Copyright 2023 The Ebitengine Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package text
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-text/typesetting/di"
|
||||
glanguage "github.com/go-text/typesetting/language"
|
||||
"github.com/go-text/typesetting/opentype/api/font"
|
||||
"github.com/go-text/typesetting/opentype/loader"
|
||||
"github.com/go-text/typesetting/shaping"
|
||||
"golang.org/x/image/math/fixed"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
var _ Face = (*GoTextFace)(nil)
|
||||
|
||||
// GoTextFace is a Face implementation for go-text's font.Face (github.com/go-text/typesetting).
|
||||
// With a GoTextFace, shaping.HarfBuzzShaper is always used as a shaper internally.
|
||||
// GoTextFace includes the source and various options.
|
||||
type GoTextFace struct {
|
||||
Source *GoTextFaceSource
|
||||
|
||||
Direction Direction
|
||||
SizeInPoints float64
|
||||
|
||||
// Language is a hiint for a language (BCP 47).
|
||||
Language language.Tag
|
||||
|
||||
// Script is a hint for a script code hint of (ISO 15924).
|
||||
// If this is empty, the script is guessed from the specified language.
|
||||
Script language.Script
|
||||
|
||||
variations []font.Variation
|
||||
features []shaping.FontFeature
|
||||
|
||||
variationsString string
|
||||
featuresString string
|
||||
}
|
||||
|
||||
// SetVariation sets a variation value.
|
||||
// For font variations, see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_fonts/Variable_fonts_guide for more details.
|
||||
func (g *GoTextFace) SetVariation(tag Tag, value float32) {
|
||||
idx := len(g.variations)
|
||||
for i, v := range g.variations {
|
||||
if uint32(v.Tag) < uint32(tag) {
|
||||
continue
|
||||
}
|
||||
if uint32(v.Tag) > uint32(tag) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
if v.Value == value {
|
||||
return
|
||||
}
|
||||
g.variations[i].Value = value
|
||||
g.variationsString = ""
|
||||
return
|
||||
}
|
||||
|
||||
// Keep the alphabetical order in order to make the cache key deterministic.
|
||||
g.variations = append(g.variations, font.Variation{})
|
||||
copy(g.variations[idx+1:], g.variations[idx:])
|
||||
g.variations[idx] = font.Variation{
|
||||
Tag: loader.Tag(tag),
|
||||
Value: value,
|
||||
}
|
||||
g.variationsString = ""
|
||||
}
|
||||
|
||||
// RemoveVariation removes a variation value.
|
||||
func (g *GoTextFace) RemoveVariation(tag Tag) {
|
||||
for i, v := range g.variations {
|
||||
if uint32(v.Tag) < uint32(tag) {
|
||||
continue
|
||||
}
|
||||
if uint32(v.Tag) > uint32(tag) {
|
||||
return
|
||||
}
|
||||
|
||||
copy(g.variations[i:], g.variations[i+1:])
|
||||
g.variations = g.variations[:len(g.variations)-1]
|
||||
g.variationsString = ""
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SetFeature sets a feature value.
|
||||
// For font features, see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_fonts/OpenType_fonts_guide for more details.
|
||||
func (g *GoTextFace) SetFeature(tag Tag, value uint32) {
|
||||
idx := len(g.features)
|
||||
for i, f := range g.features {
|
||||
if uint32(f.Tag) < uint32(tag) {
|
||||
continue
|
||||
}
|
||||
if uint32(f.Tag) > uint32(tag) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
if f.Value == value {
|
||||
return
|
||||
}
|
||||
g.features[i].Value = value
|
||||
g.featuresString = ""
|
||||
return
|
||||
}
|
||||
|
||||
// Keep the alphabetical order in order to make the cache key deterministic.
|
||||
g.features = append(g.features, shaping.FontFeature{})
|
||||
copy(g.features[idx+1:], g.features[idx:])
|
||||
g.features[idx] = shaping.FontFeature{
|
||||
Tag: loader.Tag(tag),
|
||||
Value: value,
|
||||
}
|
||||
g.featuresString = ""
|
||||
}
|
||||
|
||||
// RemoveFeature removes a feature value.
|
||||
func (g *GoTextFace) RemoveFeature(tag Tag) {
|
||||
for i, v := range g.features {
|
||||
if uint32(v.Tag) < uint32(tag) {
|
||||
continue
|
||||
}
|
||||
if uint32(v.Tag) > uint32(tag) {
|
||||
return
|
||||
}
|
||||
|
||||
copy(g.features[i:], g.features[i+1:])
|
||||
g.features = g.features[:len(g.features)-1]
|
||||
g.featuresString = ""
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Tag is a tag for font variations and features.
|
||||
// Tag is a 4-byte value like 'cmap'.
|
||||
type Tag uint32
|
||||
|
||||
// String returns the Tag's string representation.
|
||||
func (t Tag) String() string {
|
||||
return string([]byte{byte(t >> 24), byte(t >> 16), byte(t >> 8), byte(t)})
|
||||
}
|
||||
|
||||
// PraseTag converts a string to Tag.
|
||||
func ParseTag(str string) (Tag, error) {
|
||||
if len(str) != 4 {
|
||||
return 0, fmt.Errorf("text: a string's length must be 4 but was %d at ParseTag", len(str))
|
||||
}
|
||||
return Tag((uint32(str[0]) << 24) | (uint32(str[1]) << 16) | (uint32(str[2]) << 8) | uint32(str[3])), nil
|
||||
}
|
||||
|
||||
// MustPraseTag converts a string to Tag.
|
||||
// If parsing fails, MustParseTag panics.
|
||||
func MustParseTag(str string) Tag {
|
||||
t, err := ParseTag(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Metrics implements Face.
|
||||
func (g *GoTextFace) Metrics() Metrics {
|
||||
return g.Source.Metrics()
|
||||
}
|
||||
|
||||
// UnsafeInternal implements Face.
|
||||
func (g *GoTextFace) UnsafeInternal() any {
|
||||
return g.Source.f
|
||||
}
|
||||
|
||||
func (g *GoTextFace) ensureVariationsString() string {
|
||||
if g.variationsString != "" {
|
||||
return g.variationsString
|
||||
}
|
||||
if len(g.variations) == 0 {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for _, t := range g.variations {
|
||||
_ = binary.Write(&buf, binary.LittleEndian, t.Tag)
|
||||
_ = binary.Write(&buf, binary.LittleEndian, t.Value)
|
||||
}
|
||||
g.variationsString = buf.String()
|
||||
return g.variationsString
|
||||
}
|
||||
|
||||
func (g *GoTextFace) ensureFeaturesString() string {
|
||||
if g.featuresString != "" {
|
||||
return g.featuresString
|
||||
}
|
||||
if len(g.features) == 0 {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for _, t := range g.features {
|
||||
_ = binary.Write(&buf, binary.LittleEndian, t.Tag)
|
||||
_ = binary.Write(&buf, binary.LittleEndian, t.Value)
|
||||
}
|
||||
g.featuresString = buf.String()
|
||||
return g.featuresString
|
||||
}
|
||||
|
||||
// faceCacheKey implements Face.
|
||||
func (g *GoTextFace) faceCacheKey() faceCacheKey {
|
||||
return faceCacheKey{
|
||||
goTextFaceSourceID: g.Source.id,
|
||||
goTextFaceDirection: g.Direction,
|
||||
goTextFaceSizeInPoints: g.SizeInPoints,
|
||||
goTextFaceLanguage: g.Language.String(),
|
||||
goTextFaceScript: g.Script.String(),
|
||||
goTextFaceVariations: g.ensureVariationsString(),
|
||||
goTextFaceFeatures: g.ensureFeaturesString(),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GoTextFace) outputCacheKey(text string) goTextOutputCacheKey {
|
||||
return goTextOutputCacheKey{
|
||||
text: text,
|
||||
direction: g.Direction,
|
||||
sizeInPoints: g.SizeInPoints,
|
||||
language: g.Language.String(),
|
||||
script: g.Script.String(),
|
||||
variations: g.ensureVariationsString(),
|
||||
features: g.ensureFeaturesString(),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GoTextFace) diDirection() di.Direction {
|
||||
switch g.Direction {
|
||||
case DirectionLeftToRight:
|
||||
return di.DirectionLTR
|
||||
case DirectionRightToLeft:
|
||||
return di.DirectionRTL
|
||||
default:
|
||||
return di.DirectionTTB
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GoTextFace) gScript() glanguage.Script {
|
||||
var str string
|
||||
if g.Script != (language.Script{}) {
|
||||
str = g.Script.String()
|
||||
} else {
|
||||
s, _ := g.Language.Script()
|
||||
str = s.String()
|
||||
}
|
||||
|
||||
// ISO 15924 itself is case-insensitive [1], but go-text uses small-caps.
|
||||
// [1] https://www.unicode.org/iso15924/faq.html#17
|
||||
str = strings.ToLower(str)
|
||||
s, err := glanguage.ParseScript(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// advance implements Face.
|
||||
func (g *GoTextFace) advance(text string) float64 {
|
||||
output, _ := g.Source.shape(text, g)
|
||||
return fixed26_6ToFloat64(output.Advance)
|
||||
}
|
||||
|
||||
// appendGlyphs implements Face.
|
||||
func (g *GoTextFace) appendGlyphs(glyphs []Glyph, text string, indexOffset int, originX, originY float64) []Glyph {
|
||||
_, gs := g.Source.shape(text, g)
|
||||
|
||||
origin := fixed.Point26_6{
|
||||
X: float64ToFixed26_6(originX),
|
||||
Y: float64ToFixed26_6(originY),
|
||||
}
|
||||
for _, glyph := range gs {
|
||||
img, imgX, imgY := g.glyphImage(glyph, origin)
|
||||
if img != nil {
|
||||
glyphs = append(glyphs, Glyph{
|
||||
StartIndexInBytes: indexOffset + glyph.startIndex,
|
||||
EndIndexInBytes: indexOffset + glyph.endIndex,
|
||||
GID: uint32(glyph.shapingGlyph.GlyphID),
|
||||
Image: img,
|
||||
X: float64(imgX),
|
||||
Y: float64(imgY),
|
||||
})
|
||||
}
|
||||
origin = origin.Add(fixed.Point26_6{
|
||||
X: glyph.shapingGlyph.XAdvance,
|
||||
Y: -glyph.shapingGlyph.YAdvance,
|
||||
})
|
||||
}
|
||||
|
||||
return glyphs
|
||||
}
|
||||
|
||||
func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Image, int, int) {
|
||||
if g.direction().isHorizontal() {
|
||||
origin.X = adjustGranularity(origin.X)
|
||||
origin.Y &^= ((1 << 6) - 1)
|
||||
} else {
|
||||
origin.X &^= ((1 << 6) - 1)
|
||||
origin.Y = adjustGranularity(origin.Y)
|
||||
}
|
||||
|
||||
b := segmentsToBounds(glyph.scaledSegments)
|
||||
subpixelOffset := fixed.Point26_6{
|
||||
X: (origin.X + b.Min.X) & ((1 << 6) - 1),
|
||||
Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1),
|
||||
}
|
||||
key := glyphImageCacheKey{
|
||||
id: uint32(glyph.shapingGlyph.GlyphID),
|
||||
xoffset: subpixelOffset.X,
|
||||
yoffset: subpixelOffset.Y,
|
||||
}
|
||||
img := theGlyphImageCache.getOrCreate(g, key, func() *ebiten.Image {
|
||||
return segmentsToImage(glyph.scaledSegments, subpixelOffset, b)
|
||||
})
|
||||
|
||||
imgX := (origin.X + b.Min.X).Floor()
|
||||
imgY := (origin.Y + b.Min.Y).Floor()
|
||||
return img, imgX, imgY
|
||||
}
|
||||
|
||||
// direction implements Face.
|
||||
func (g *GoTextFace) direction() Direction {
|
||||
return g.Direction
|
||||
}
|
||||
|
||||
// private implements Face.
|
||||
func (g *GoTextFace) private() {
|
||||
}
|
235
text/v2/gotextfacesource.go
Normal file
235
text/v2/gotextfacesource.go
Normal file
@ -0,0 +1,235 @@
|
||||
// Copyright 2023 The Ebitengine Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package text
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/go-text/typesetting/font"
|
||||
"github.com/go-text/typesetting/language"
|
||||
"github.com/go-text/typesetting/opentype/api"
|
||||
"github.com/go-text/typesetting/shaping"
|
||||
)
|
||||
|
||||
type goTextOutputCacheKey struct {
|
||||
text string
|
||||
direction Direction
|
||||
sizeInPoints float64
|
||||
language string
|
||||
script string
|
||||
variations string
|
||||
features string
|
||||
}
|
||||
|
||||
type glyph struct {
|
||||
shapingGlyph *shaping.Glyph
|
||||
startIndex int
|
||||
endIndex int
|
||||
scaledSegments []api.Segment
|
||||
}
|
||||
|
||||
type goTextOutputCacheValue struct {
|
||||
output shaping.Output
|
||||
glyphs []glyph
|
||||
atime int64
|
||||
}
|
||||
|
||||
// GoTextFaceSource is a source of a GoTextFace. This can be shared by multiple GoTextFace objects.
|
||||
type GoTextFaceSource struct {
|
||||
f font.Face
|
||||
id uint64
|
||||
|
||||
outputCache map[goTextOutputCacheKey]*goTextOutputCacheValue
|
||||
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func toFontResource(source io.ReadSeeker) (font.Resource, error) {
|
||||
// font.Resource has io.ReaderAt in addition to io.ReadSeeker.
|
||||
// If source has it, use it as it is.
|
||||
if s, ok := source.(font.Resource); ok {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Read all the bytes and convert this to bytes.Reader.
|
||||
// This is a very rough solution, but it works.
|
||||
// TODO: Implement io.ReaderAt in a more efficient way.
|
||||
bs, err := io.ReadAll(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes.NewReader(bs), nil
|
||||
}
|
||||
|
||||
// NewGoTextFaceSource parses an OpenType or TrueType font and returns a GoTextFaceSource object.
|
||||
func NewGoTextFaceSource(source io.ReadSeeker) (*GoTextFaceSource, error) {
|
||||
src, err := toFontResource(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := font.ParseTTF(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &GoTextFaceSource{
|
||||
f: f,
|
||||
id: nextUniqueID(),
|
||||
}
|
||||
runtime.SetFinalizer(s, finalizeGoTextFaceSource)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// NewGoTextFaceSourcesFromCollection parses an OpenType or TrueType font collection and returns a slice of GoTextFaceSource objects.
|
||||
func NewGoTextFaceSourcesFromCollection(source io.ReadSeeker) ([]*GoTextFaceSource, error) {
|
||||
src, err := toFontResource(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fs, err := font.ParseTTC(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sources := make([]*GoTextFaceSource, len(fs))
|
||||
for i, f := range fs {
|
||||
s := &GoTextFaceSource{
|
||||
f: f,
|
||||
id: nextUniqueID(),
|
||||
}
|
||||
runtime.SetFinalizer(s, finalizeGoTextFaceSource)
|
||||
sources[i] = s
|
||||
}
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func finalizeGoTextFaceSource(source *GoTextFaceSource) {
|
||||
runtime.SetFinalizer(source, nil)
|
||||
theGlyphImageCache.clear(func(key faceCacheKey) bool {
|
||||
return key.goTextFaceSourceID == source.id
|
||||
})
|
||||
}
|
||||
|
||||
// Metrics returns the font's metrics.
|
||||
func (g *GoTextFaceSource) Metrics() Metrics {
|
||||
upem := float64(g.f.Font.Upem())
|
||||
|
||||
var m Metrics
|
||||
if h, ok := g.f.FontHExtents(); ok {
|
||||
m.Height = float64(h.Ascender-h.Descender+h.LineGap) / upem
|
||||
m.HAscent = float64(h.Ascender) / upem
|
||||
m.HDescent = float64(-h.Descender) / upem
|
||||
}
|
||||
if v, ok := g.f.FontVExtents(); ok {
|
||||
m.Width = float64(v.Ascender-v.Descender+v.LineGap) / upem
|
||||
m.VAscent = float64(v.Ascender) / upem
|
||||
m.VDescent = float64(-v.Descender) / upem
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output, []glyph) {
|
||||
g.m.Lock()
|
||||
defer g.m.Unlock()
|
||||
|
||||
key := face.outputCacheKey(text)
|
||||
if out, ok := g.outputCache[key]; ok {
|
||||
out.atime = now()
|
||||
return out.output, out.glyphs
|
||||
}
|
||||
|
||||
g.f.SetVariations(face.variations)
|
||||
runes := []rune(text)
|
||||
input := shaping.Input{
|
||||
Text: runes,
|
||||
RunStart: 0,
|
||||
RunEnd: len(runes),
|
||||
Direction: face.diDirection(),
|
||||
Face: face.Source.f,
|
||||
FontFeatures: face.features,
|
||||
Size: float64ToFixed26_6(face.SizeInPoints),
|
||||
Script: face.gScript(),
|
||||
Language: language.Language(face.Language.String()),
|
||||
}
|
||||
out := (&shaping.HarfbuzzShaper{}).Shape(input)
|
||||
if g.outputCache == nil {
|
||||
g.outputCache = map[goTextOutputCacheKey]*goTextOutputCacheValue{}
|
||||
}
|
||||
|
||||
var indices []int
|
||||
for i := range text {
|
||||
indices = append(indices, i)
|
||||
}
|
||||
indices = append(indices, len(text))
|
||||
|
||||
gs := make([]glyph, len(out.Glyphs))
|
||||
for i, gl := range out.Glyphs {
|
||||
gl := gl
|
||||
var segs []api.Segment
|
||||
switch data := g.f.GlyphData(gl.GlyphID).(type) {
|
||||
case api.GlyphOutline:
|
||||
segs = data.Segments
|
||||
case api.GlyphSVG:
|
||||
segs = data.Outline.Segments
|
||||
case api.GlyphBitmap:
|
||||
if data.Outline != nil {
|
||||
segs = data.Outline.Segments
|
||||
}
|
||||
}
|
||||
|
||||
scaledSegs := make([]api.Segment, len(segs))
|
||||
scale := fixed26_6ToFloat32(out.Size) / float32(out.Face.Font.Upem())
|
||||
for i, seg := range segs {
|
||||
scaledSegs[i] = seg
|
||||
for j := range seg.Args {
|
||||
scaledSegs[i].Args[j].X *= scale
|
||||
scaledSegs[i].Args[j].Y *= scale
|
||||
scaledSegs[i].Args[j].Y *= -1
|
||||
}
|
||||
}
|
||||
|
||||
gs[i] = glyph{
|
||||
shapingGlyph: &gl,
|
||||
startIndex: indices[gl.ClusterIndex],
|
||||
endIndex: indices[gl.ClusterIndex+gl.RuneCount],
|
||||
scaledSegments: scaledSegs,
|
||||
}
|
||||
}
|
||||
g.outputCache[key] = &goTextOutputCacheValue{
|
||||
output: out,
|
||||
glyphs: gs,
|
||||
atime: now(),
|
||||
}
|
||||
|
||||
const cacheSoftLimit = 512
|
||||
if len(g.outputCache) > cacheSoftLimit {
|
||||
for key, e := range g.outputCache {
|
||||
// 60 is an arbitrary number.
|
||||
if e.atime >= now()-60 {
|
||||
continue
|
||||
}
|
||||
delete(g.outputCache, key)
|
||||
}
|
||||
}
|
||||
|
||||
return out, gs
|
||||
}
|
122
text/v2/gotextseg.go
Normal file
122
text/v2/gotextseg.go
Normal file
@ -0,0 +1,122 @@
|
||||
// Copyright 2023 The Ebitengine Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package text
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/draw"
|
||||
"math"
|
||||
|
||||
"github.com/go-text/typesetting/opentype/api"
|
||||
"golang.org/x/image/math/fixed"
|
||||
"golang.org/x/image/vector"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
func segmentsToBounds(segs []api.Segment) fixed.Rectangle26_6 {
|
||||
if len(segs) == 0 {
|
||||
return fixed.Rectangle26_6{}
|
||||
}
|
||||
|
||||
minX := float32(math.Inf(1))
|
||||
minY := float32(math.Inf(1))
|
||||
maxX := float32(math.Inf(-1))
|
||||
maxY := float32(math.Inf(-1))
|
||||
|
||||
for _, seg := range segs {
|
||||
n := 1
|
||||
switch seg.Op {
|
||||
case api.SegmentOpQuadTo:
|
||||
n = 2
|
||||
case api.SegmentOpCubeTo:
|
||||
n = 3
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
x := seg.Args[i].X
|
||||
y := seg.Args[i].Y
|
||||
if minX > x {
|
||||
minX = x
|
||||
}
|
||||
if minY > y {
|
||||
minY = y
|
||||
}
|
||||
if maxX < x {
|
||||
maxX = x
|
||||
}
|
||||
if maxY < y {
|
||||
maxY = y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fixed.Rectangle26_6{
|
||||
Min: fixed.Point26_6{
|
||||
X: float32ToFixed26_6(minX),
|
||||
Y: float32ToFixed26_6(minY),
|
||||
},
|
||||
Max: fixed.Point26_6{
|
||||
X: float32ToFixed26_6(maxX),
|
||||
Y: float32ToFixed26_6(maxY),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func segmentsToImage(segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *ebiten.Image {
|
||||
if len(segs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
w, h := (glyphBounds.Max.X - glyphBounds.Min.X).Ceil(), (glyphBounds.Max.Y - glyphBounds.Min.Y).Ceil()
|
||||
if w == 0 || h == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if glyphBounds.Min.X&((1<<6)-1) != 0 {
|
||||
w++
|
||||
}
|
||||
if glyphBounds.Min.Y&((1<<6)-1) != 0 {
|
||||
h++
|
||||
}
|
||||
|
||||
biasX := fixed26_6ToFloat32(-glyphBounds.Min.X + subpixelOffset.X)
|
||||
biasY := fixed26_6ToFloat32(-glyphBounds.Min.Y + subpixelOffset.Y)
|
||||
|
||||
rast := vector.NewRasterizer(w, h)
|
||||
rast.DrawOp = draw.Src
|
||||
for _, seg := range segs {
|
||||
switch seg.Op {
|
||||
case api.SegmentOpMoveTo:
|
||||
rast.MoveTo(seg.Args[0].X+biasX, seg.Args[0].Y+biasY)
|
||||
case api.SegmentOpLineTo:
|
||||
rast.LineTo(seg.Args[0].X+biasX, seg.Args[0].Y+biasY)
|
||||
case api.SegmentOpQuadTo:
|
||||
rast.QuadTo(
|
||||
seg.Args[0].X+biasX, seg.Args[0].Y+biasY,
|
||||
seg.Args[1].X+biasX, seg.Args[1].Y+biasY,
|
||||
)
|
||||
case api.SegmentOpCubeTo:
|
||||
rast.CubeTo(
|
||||
seg.Args[0].X+biasX, seg.Args[0].Y+biasY,
|
||||
seg.Args[1].X+biasX, seg.Args[1].Y+biasY,
|
||||
seg.Args[2].X+biasX, seg.Args[2].Y+biasY,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dst := image.NewAlpha(image.Rect(0, 0, w, h))
|
||||
rast.Draw(dst, dst.Bounds(), image.Opaque, image.Point{})
|
||||
return ebiten.NewImageFromImage(dst)
|
||||
}
|
@ -17,7 +17,6 @@ package text
|
||||
import (
|
||||
"image"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
@ -26,12 +25,6 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
var currentStdFaceID uint64
|
||||
|
||||
func nextStdFaceID() uint64 {
|
||||
return atomic.AddUint64(¤tStdFaceID, 1)
|
||||
}
|
||||
|
||||
var _ Face = (*StdFace)(nil)
|
||||
|
||||
// StdFace is a Face implementation for a semi-standard font.Face (golang.org/x/image/font).
|
||||
@ -51,13 +44,20 @@ func NewStdFace(face font.Face) *StdFace {
|
||||
f: &faceWithCache{
|
||||
f: face,
|
||||
},
|
||||
id: nextStdFaceID(),
|
||||
id: nextUniqueID(),
|
||||
}
|
||||
s.addr = s
|
||||
runtime.SetFinalizer(s, theGlyphImageCache.clear)
|
||||
runtime.SetFinalizer(s, finalizeStdFace)
|
||||
return s
|
||||
}
|
||||
|
||||
func finalizeStdFace(face *StdFace) {
|
||||
runtime.SetFinalizer(face, nil)
|
||||
theGlyphImageCache.clear(func(key faceCacheKey) bool {
|
||||
return key.stdFaceID == face.id
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StdFace) copyCheck() {
|
||||
if s.addr != s {
|
||||
panic("text: illegal use of non-zero StdFace copied by value")
|
||||
@ -84,7 +84,9 @@ func (s *StdFace) UnsafeInternal() any {
|
||||
|
||||
// faceCacheKey implements Face.
|
||||
func (s *StdFace) faceCacheKey() faceCacheKey {
|
||||
return faceCacheKey(s.id)
|
||||
return faceCacheKey{
|
||||
stdFaceID: s.id,
|
||||
}
|
||||
}
|
||||
|
||||
// advance implements Face.
|
||||
@ -137,7 +139,7 @@ func (s *StdFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int
|
||||
Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1),
|
||||
}
|
||||
key := glyphImageCacheKey{
|
||||
rune: r,
|
||||
id: uint32(r),
|
||||
xoffset: subpixelOffset.X,
|
||||
// yoffset is always the same if the rune is the same, so this doesn't have to be a key.
|
||||
}
|
||||
|
@ -20,14 +20,13 @@ package text
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type faceCacheKey uint64
|
||||
|
||||
// Face is an interface representing a font face. The implementations are only GoTextFace and StdFace.
|
||||
type Face interface {
|
||||
// Metrics returns the metrics for this Face.
|
||||
@ -63,6 +62,10 @@ type Metrics struct {
|
||||
// The value is typically positive, even though a descender goes below the baseline.
|
||||
HDescent float64
|
||||
|
||||
// Width is the recommended amount of horizontal space between two lines of text in pixels.
|
||||
// If the face is StdFace or the font dosen't support a vertical direction, Width is 0.
|
||||
Width float64
|
||||
|
||||
// VAscent is the distance in pixels from the top of a line to its baseline for vertical lines.
|
||||
// If the face is StdFace or the font dosen't support a vertical direction, VAscent is 0.
|
||||
VAscent float64
|
||||
@ -72,10 +75,20 @@ type Metrics struct {
|
||||
VDescent float64
|
||||
}
|
||||
|
||||
func fixed26_6ToFloat32(x fixed.Int26_6) float32 {
|
||||
return float32(x>>6) + float32(x&((1<<6)-1))/float32(1<<6)
|
||||
}
|
||||
|
||||
func fixed26_6ToFloat64(x fixed.Int26_6) float64 {
|
||||
return float64(x>>6) + float64(x&((1<<6)-1))/float64(1<<6)
|
||||
}
|
||||
|
||||
func float32ToFixed26_6(x float32) fixed.Int26_6 {
|
||||
i := float32(math.Floor(float64(x)))
|
||||
frac := x - i
|
||||
return fixed.Int26_6(i)<<6 + fixed.Int26_6(frac*(1<<6))
|
||||
}
|
||||
|
||||
func float64ToFixed26_6(x float64) fixed.Int26_6 {
|
||||
i := math.Floor(x)
|
||||
frac := x - i
|
||||
@ -227,3 +240,9 @@ func CacheGlyphs(text string, face Face) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentUniqueID uint64
|
||||
|
||||
func nextUniqueID() uint64 {
|
||||
return atomic.AddUint64(¤tUniqueID, 1)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user