mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2025-01-24 01:42:05 +01:00
parent
46600b42f9
commit
fe35180b78
@ -22,115 +22,62 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/go-text/typesetting/di"
|
"golang.org/x/text/language"
|
||||||
"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"
|
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
|
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed NotoSansArabic-Regular.ttf
|
//go:embed NotoSansArabic-Regular.ttf
|
||||||
var arabicTTF []byte
|
var arabicTTF []byte
|
||||||
|
|
||||||
var arabicOut shaping.Output
|
var arabicFaceSource *text.GoTextFaceSource
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
face, err := font.ParseTTF(bytes.NewReader(arabicTTF))
|
s, err := text.NewGoTextFaceSource(bytes.NewReader(arabicTTF))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
runes := []rune("لمّا كان الاعتراف بالكرامة المتأصلة في جميع")
|
arabicFaceSource = s
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed NotoSansDevanagari-Regular.ttf
|
//go:embed NotoSansDevanagari-Regular.ttf
|
||||||
var devanagariTTF []byte
|
var devanagariTTF []byte
|
||||||
|
|
||||||
var devanagariOut shaping.Output
|
var devanagariFaceSource *text.GoTextFaceSource
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
face, err := font.ParseTTF(bytes.NewReader(devanagariTTF))
|
s, err := text.NewGoTextFaceSource(bytes.NewReader(devanagariTTF))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
runes := []rune("चूंकि मानव परिवार के सभी सदस्यों के जन्मजात गौरव और समान")
|
devanagariFaceSource = s
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed NotoSansThai-Regular.ttf
|
//go:embed NotoSansThai-Regular.ttf
|
||||||
var thaiTTF []byte
|
var thaiTTF []byte
|
||||||
|
|
||||||
var thaiOut shaping.Output
|
var thaiFaceSource *text.GoTextFaceSource
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
face, err := font.ParseTTF(bytes.NewReader(thaiTTF))
|
s, err := text.NewGoTextFaceSource(bytes.NewReader(thaiTTF))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
runes := []rune("โดยที่การไม่นำพาและการหมิ่นในคุณค่าของสิทธิมนุษยชน")
|
thaiFaceSource = s
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var japaneseOut shaping.Output
|
var japaneseFaceSource *text.GoTextFaceSource
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
const japanese = language.Script(('j' << 24) | ('p' << 16) | ('a' << 8) | 'n')
|
s, err := text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf))
|
||||||
|
|
||||||
face, err := font.ParseTTF(bytes.NewReader(fonts.MPlus1pRegular_ttf))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
runes := []rune("ラーメン。")
|
japaneseFaceSource = s
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -151,21 +98,6 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Game struct {
|
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 {
|
func (g *Game) Update() error {
|
||||||
@ -173,67 +105,47 @@ func (g *Game) Update() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Draw(screen *ebiten.Image) {
|
func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
g.drawGlyphs(screen, &arabicOut, 20, 100)
|
const arabicText = "لمّا كان الاعتراف بالكرامة المتأصلة في جميع"
|
||||||
g.drawGlyphs(screen, &devanagariOut, 20, 150)
|
|
||||||
g.drawGlyphs(screen, &thaiOut, 20, 200)
|
|
||||||
g.drawGlyphs(screen, &japaneseOut, 20, 250)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fixed26_6ToFloat32(x fixed.Int26_6) float32 {
|
op := &text.DrawOptions{}
|
||||||
return float32(x>>6) + (float32(x&(1<<6-1)) / (1 << 6))
|
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 {
|
const hindiText = "चूंकि मानव परिवार के सभी सदस्यों के जन्मजात गौरव और समान"
|
||||||
i := float32(math.Floor(float64(x)))
|
|
||||||
return (fixed.Int26_6(i) << 6) + fixed.Int26_6((x-i)*(1<<6))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Game) drawGlyphs(dst *ebiten.Image, output *shaping.Output, originX, originY float32) {
|
op.GeoM.Reset()
|
||||||
g.vertices = g.vertices[:0]
|
op.GeoM.Translate(20, 150)
|
||||||
g.indices = g.indices[:0]
|
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{
|
op.GeoM.Reset()
|
||||||
X: float32ToFixed26_6(originX),
|
op.GeoM.Translate(20, 200)
|
||||||
Y: float32ToFixed26_6(originY),
|
text.Draw(screen, thaiText, &text.GoTextFace{
|
||||||
}
|
Source: thaiFaceSource,
|
||||||
for _, glyph := range output.Glyphs {
|
SizeInPoints: 24,
|
||||||
key := glyphCacheKey{
|
Language: language.Thai,
|
||||||
output: output,
|
}, op)
|
||||||
glyphID: glyph.GlyphID,
|
|
||||||
origin: orig,
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := g.glyphCache[key]
|
const japaneseText = "ラーメン。"
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.image != nil {
|
op.GeoM.Reset()
|
||||||
op := &ebiten.DrawImageOptions{}
|
op.GeoM.Translate(20, 250)
|
||||||
op.GeoM.Translate(float64(v.point.X), float64(v.point.Y))
|
text.Draw(screen, japaneseText, &text.GoTextFace{
|
||||||
dst.DrawImage(v.image, op)
|
Source: japaneseFaceSource,
|
||||||
}
|
Direction: text.DirectionTopToBottomAndRightToLeft,
|
||||||
|
SizeInPoints: 24,
|
||||||
orig = orig.Add(fixed.Point26_6{X: glyph.XAdvance, Y: glyph.YAdvance * -1})
|
Language: language.Thai,
|
||||||
}
|
}, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||||
@ -247,94 +159,3 @@ func main() {
|
|||||||
log.Fatal(err)
|
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 (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"golang.org/x/image/math/fixed"
|
"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 {
|
type glyphImageCacheKey struct {
|
||||||
// For StdFace
|
// id is rune for StdFace, and GID for GoTextFace.
|
||||||
rune rune
|
id uint32
|
||||||
|
|
||||||
xoffset fixed.Int26_6
|
xoffset fixed.Int26_6
|
||||||
|
yoffset fixed.Int26_6
|
||||||
}
|
}
|
||||||
|
|
||||||
type glyphImageCacheEntry struct {
|
type glyphImageCacheEntry struct {
|
||||||
@ -109,10 +122,13 @@ func (g *glyphImageCache) getOrCreate(face Face, key glyphImageCacheKey, create
|
|||||||
return img
|
return img
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *glyphImageCache) clear(face Face) {
|
func (g *glyphImageCache) clear(comp func(key faceCacheKey) bool) {
|
||||||
runtime.SetFinalizer(face, nil)
|
|
||||||
|
|
||||||
g.m.Lock()
|
g.m.Lock()
|
||||||
defer g.m.Unlock()
|
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 (
|
import (
|
||||||
"image"
|
"image"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync/atomic"
|
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"golang.org/x/image/font"
|
"golang.org/x/image/font"
|
||||||
@ -26,12 +25,6 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var currentStdFaceID uint64
|
|
||||||
|
|
||||||
func nextStdFaceID() uint64 {
|
|
||||||
return atomic.AddUint64(¤tStdFaceID, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Face = (*StdFace)(nil)
|
var _ Face = (*StdFace)(nil)
|
||||||
|
|
||||||
// StdFace is a Face implementation for a semi-standard font.Face (golang.org/x/image/font).
|
// 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: &faceWithCache{
|
||||||
f: face,
|
f: face,
|
||||||
},
|
},
|
||||||
id: nextStdFaceID(),
|
id: nextUniqueID(),
|
||||||
}
|
}
|
||||||
s.addr = s
|
s.addr = s
|
||||||
runtime.SetFinalizer(s, theGlyphImageCache.clear)
|
runtime.SetFinalizer(s, finalizeStdFace)
|
||||||
return s
|
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() {
|
func (s *StdFace) copyCheck() {
|
||||||
if s.addr != s {
|
if s.addr != s {
|
||||||
panic("text: illegal use of non-zero StdFace copied by value")
|
panic("text: illegal use of non-zero StdFace copied by value")
|
||||||
@ -84,7 +84,9 @@ func (s *StdFace) UnsafeInternal() any {
|
|||||||
|
|
||||||
// faceCacheKey implements Face.
|
// faceCacheKey implements Face.
|
||||||
func (s *StdFace) faceCacheKey() faceCacheKey {
|
func (s *StdFace) faceCacheKey() faceCacheKey {
|
||||||
return faceCacheKey(s.id)
|
return faceCacheKey{
|
||||||
|
stdFaceID: s.id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// advance implements Face.
|
// 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),
|
Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1),
|
||||||
}
|
}
|
||||||
key := glyphImageCacheKey{
|
key := glyphImageCacheKey{
|
||||||
rune: r,
|
id: uint32(r),
|
||||||
xoffset: subpixelOffset.X,
|
xoffset: subpixelOffset.X,
|
||||||
// yoffset is always the same if the rune is the same, so this doesn't have to be a key.
|
// 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 (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"golang.org/x/image/math/fixed"
|
"golang.org/x/image/math/fixed"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type faceCacheKey uint64
|
|
||||||
|
|
||||||
// Face is an interface representing a font face. The implementations are only GoTextFace and StdFace.
|
// Face is an interface representing a font face. The implementations are only GoTextFace and StdFace.
|
||||||
type Face interface {
|
type Face interface {
|
||||||
// Metrics returns the metrics for this Face.
|
// 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.
|
// The value is typically positive, even though a descender goes below the baseline.
|
||||||
HDescent float64
|
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.
|
// 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.
|
// If the face is StdFace or the font dosen't support a vertical direction, VAscent is 0.
|
||||||
VAscent float64
|
VAscent float64
|
||||||
@ -72,10 +75,20 @@ type Metrics struct {
|
|||||||
VDescent float64
|
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 {
|
func fixed26_6ToFloat64(x fixed.Int26_6) float64 {
|
||||||
return float64(x>>6) + float64(x&((1<<6)-1))/float64(1<<6)
|
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 {
|
func float64ToFixed26_6(x float64) fixed.Int26_6 {
|
||||||
i := math.Floor(x)
|
i := math.Floor(x)
|
||||||
frac := x - i
|
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