├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── ack_setup_test.go ├── aux_sizer_test.go ├── cache ├── cache_entry.go ├── default_cache.go ├── default_cache_test.go ├── default_handler.go ├── default_handler_test.go ├── doc.go ├── ebiten_no.go ├── ebiten_yes.go └── glyph_cache_handler.go ├── doc.go ├── docs ├── README.md ├── display-scaling.md ├── fixed-26-6.md ├── fixed-ops.md ├── img │ ├── glyph_edges.png │ ├── glyph_filled.png │ ├── glyph_sign.png │ ├── gtxt_aligns.png │ ├── gtxt_mirror.png │ ├── gtxt_outline_cheap.png │ ├── gtxt_quantization.png │ └── outline_vs_raster.png ├── panorama.md ├── pixel-tips.md ├── px-size.md ├── quantization.md ├── rasterize-outlines.md ├── renderer.md └── shaping.md ├── ebiten_no.go ├── ebiten_yes.go ├── examples ├── README.md ├── ebiten │ ├── aligns │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── all_glyphs │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── color_markup │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── colorful │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── cutout │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── debug_glyph │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── elastic_sizer │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── faux_styles │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── hover_shadow │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── measure │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── miss_handler │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── shaking │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── typewriter │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ └── words │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go └── gtxt │ ├── aligns │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── blend_modes │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── debug_glyph │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── direction_bidi │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── each_font │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── font_library │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── hello_world │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── measure │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── mirror │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── outline_cheap │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── pattern │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── properties │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── quantization │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── rainbow │ ├── go.mod │ ├── go.sum │ └── main.go │ └── sizer_expand │ ├── go.mod │ ├── go.sum │ └── main.go ├── feed.go ├── font ├── ack_setup_test.go ├── doc.go ├── library.go ├── library_test.go ├── parse.go ├── parse_test.go ├── properties.go ├── properties_test.go └── test │ └── .blank ├── fract ├── constants.go ├── convert.go ├── convert_test.go ├── doc.go ├── ebiten_yes.go ├── point.go ├── point_test.go ├── rect.go ├── rect_test.go ├── unit.go └── unit_test.go ├── go.mod ├── go.sum ├── mask ├── buffer.go ├── cmp_test.go ├── curve_segmenter.go ├── default_rasterizer.go ├── doc.go ├── edge_marker.go ├── edge_marker_rast.go ├── edge_marker_test.go ├── faux_rasterizer.go ├── faux_rasterizer_bold_test.go ├── faux_rasterizer_oblique_test.go ├── faux_rasterizer_test.go ├── helper_funcs.go ├── helpers_test.go ├── rasterizer.go ├── sharp_rasterizer.go └── sharper_rasterizer.go ├── misc.go ├── renderer.go ├── renderer_align.go ├── renderer_draw.go ├── renderer_draw_helpers.go ├── renderer_draw_with_wrap.go ├── renderer_gtw_fract.go ├── renderer_gtw_glyph.go ├── renderer_gtw_utils.go ├── renderer_internals.go ├── renderer_measure.go ├── renderer_measure_helpers.go ├── renderer_measure_test.go ├── renderer_restorable_state.go ├── sizer ├── default_sizer.go ├── doc.go ├── helpers.go ├── padded_advance_sizer.go ├── padded_kern_sizer.go ├── padded_scalable_kern_sizer.go ├── sizer.go └── vertical_sizer.go ├── string_iterator.go ├── string_iterator_test.go ├── test ├── README.md ├── generate │ └── blend_rand │ │ ├── ebiten.go │ │ ├── ebiten_gtxt.go │ │ └── gtxt.go └── scripts │ ├── run_benchmarks.bat │ ├── run_benchmarks.sh │ ├── run_coverhtml.bat │ ├── run_coverhtml.sh │ ├── run_tests.bat │ └── run_tests.sh ├── test_utils_test.go ├── testdata_generate.go └── testdata_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # make .kage shaders be highlighted in Github like Go code 2 | *.kage linguist-language=Go 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore images except the ones from the docs 2 | *.png 3 | !docs/img/*.png 4 | 5 | # ignore go workspaces 6 | *.work 7 | *.work.sum 8 | 9 | # ignore fonts for testing 10 | /fonts/ 11 | *.ttf 12 | *.otf 13 | 14 | # ignore generated testdata files 15 | testdata_*.go 16 | !testdata_test.go 17 | !testdata_generate.go 18 | 19 | # ignore root copies of scripts if anyone uses them 20 | /*.sh 21 | /*.bat 22 | 23 | # don't share the dirty secrets 24 | TODO.txt 25 | 26 | # ignore some work directories 27 | unused/ 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 tinne26 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /ack_setup_test.go: -------------------------------------------------------------------------------- 1 | package etxt 2 | 3 | // This file contains a fake test ensuring that test assets are available, 4 | // setups a few important variables and provides some helper methods. 5 | 6 | import ( 7 | "embed" 8 | "fmt" 9 | "os" 10 | "sort" 11 | "sync" 12 | "testing" 13 | 14 | "github.com/tinne26/etxt/font" 15 | "golang.org/x/image/font/sfnt" 16 | ) 17 | 18 | //go:embed font/test/* 19 | var testfs embed.FS 20 | 21 | var testFontsDir string = "font/test" 22 | var testFontA *sfnt.Font 23 | var testFontB *sfnt.Font 24 | var assetsLoadMutex sync.Mutex 25 | var testAssetsLoaded bool 26 | 27 | func TestAssetAvailability(t *testing.T) { 28 | ensureTestAssetsLoaded() 29 | if len(testWarnings) > 0 { 30 | t.Fatalf("missing test assets\n%s", testWarnings) 31 | } 32 | } 33 | 34 | var testWarnings string 35 | 36 | func ensureTestAssetsLoaded() { 37 | // assets load access control 38 | assetsLoadMutex.Lock() 39 | defer assetsLoadMutex.Unlock() 40 | if testAssetsLoaded { 41 | return 42 | } 43 | testAssetsLoaded = true 44 | 45 | // load library from embedded folder and check fonts 46 | lib := font.NewLibrary() 47 | _, _, err := lib.ParseAllFromFS(testfs, testFontsDir) 48 | if err != nil { 49 | fmt.Printf("TESTS INIT: %s", err.Error()) 50 | os.Exit(1) 51 | } 52 | 53 | type FontInfo struct { 54 | font *sfnt.Font 55 | name string 56 | } 57 | fonts := make([]FontInfo, 0, 2) 58 | lib.EachFont(func(name string, sfntFont *sfnt.Font) error { 59 | fonts = append(fonts, FontInfo{sfntFont, name}) 60 | return nil 61 | }) 62 | sort.Slice(fonts, func(i, j int) bool { 63 | return fonts[i].name < fonts[j].name 64 | }) 65 | 66 | // set fonts and/or warnings for missing fonts 67 | switch len(fonts) { 68 | case 0: 69 | testWarnings = "WARNING: Expected at least 2 .ttf fonts in " + testFontsDir + "/ (found 0)\n" + 70 | "WARNING: Most tests will be skipped\n" 71 | case 1: 72 | testFontA = fonts[0].font 73 | testWarnings = "WARNING: Expected at least 2 .ttf fonts in " + testFontsDir + "/ (found 1)\n" + 74 | "WARNING: Some tests will be skipped\n" 75 | default: 76 | testFontA = fonts[0].font 77 | testFontB = fonts[1].font 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /aux_sizer_test.go: -------------------------------------------------------------------------------- 1 | package etxt 2 | 3 | // TODO: add tests for the sizer package, which can't have them on its own 4 | // subpackage due to the lack of available fonts. 5 | -------------------------------------------------------------------------------- /cache/cache_entry.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "sync/atomic" 4 | 5 | // A cached mask with additional information to estimate how 6 | // much the entry is being used. 7 | type cachedMaskEntry struct { 8 | Mask GlyphMask // Read-only. 9 | lastAccess uint64 10 | byteSize uint32 // Read-only. 11 | } 12 | 13 | func (self *cachedMaskEntry) UpdateAccess(accessTick uint64) { 14 | atomic.StoreUint64(&self.lastAccess, accessTick) 15 | } 16 | 17 | func (self *cachedMaskEntry) LastAccess() uint64 { 18 | return atomic.LoadUint64(&self.lastAccess) 19 | } 20 | 21 | func (self *cachedMaskEntry) ByteSize() uint32 { 22 | return atomic.LoadUint32(&self.byteSize) 23 | } 24 | 25 | // Creates a new cached mask entry for the given GlyphMask. 26 | func newCachedMaskEntry(mask GlyphMask, accessTick uint64) *cachedMaskEntry { 27 | return &cachedMaskEntry{ 28 | Mask: mask, 29 | lastAccess: accessTick, 30 | byteSize: GlyphMaskByteSize(mask), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cache/default_handler.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "unsafe" 4 | 5 | import "golang.org/x/image/font/sfnt" 6 | 7 | import "github.com/tinne26/etxt/fract" 8 | import "github.com/tinne26/etxt/mask" 9 | 10 | var _ GlyphCacheHandler = (*DefaultCacheHandler)(nil) 11 | 12 | // A default implementation of [GlyphCacheHandler]. 13 | type DefaultCacheHandler struct { 14 | cache *DefaultCache 15 | activeKey [3]uint64 16 | } 17 | 18 | // Implements [GlyphCacheHandler].NotifyFontChange(...) 19 | func (self *DefaultCacheHandler) NotifyFontChange(font *sfnt.Font) { 20 | self.activeKey[0] = uint64(uintptr(unsafe.Pointer(font))) 21 | } 22 | 23 | // Implements [GlyphCacheHandler].NotifyRasterizerChange(...) 24 | func (self *DefaultCacheHandler) NotifyRasterizerChange(rasterizer mask.Rasterizer) { 25 | self.activeKey[1] = rasterizer.Signature() 26 | } 27 | 28 | // Implements [GlyphCacheHandler].NotifySizeChange(...) 29 | func (self *DefaultCacheHandler) NotifySizeChange(size fract.Unit) { 30 | self.activeKey[2] = (self.activeKey[2] & ^uint64(0xFFFFFFFF00000000)) | (uint64(size) << 32) 31 | } 32 | 33 | // Implements [GlyphCacheHandler].NotifyFractChange(...) 34 | func (self *DefaultCacheHandler) NotifyFractChange(fract fract.Point) { 35 | bits := uint64(fract.Y.FractShift()) << 16 36 | bits |= uint64(fract.X.FractShift()) << 22 37 | self.activeKey[2] = (self.activeKey[2] & ^uint64(0x000000000FFF0000)) | bits 38 | } 39 | 40 | // This is not a thing nowadays, but if sfnt ever implemented proper hinting 41 | // and you could detect whether a glyph mask has hinting instructions applied 42 | // or not, or if you implemented some other hinting mechanism yourself, you 43 | // could use this "variant" change to differentiate the glyphs. This code 44 | // only allows 4 bits to encode variants, but since etxt.Renderer doesn't 45 | // use all the bits from the size, we could easily shave ~12 bits more from 46 | // the size key encoding and go up to 16 bits for variants. 47 | // 48 | // For rasterizer-based hinting it doesn't matter much, though, as the 64 49 | // bits from their cache signature can also do the job. 50 | // func (self *DefaultCacheHandler) NotifyVariantChange(variant uint8) { 51 | // self.activeKey[2] = (self.activeKey[2] & ^uint64(0x00000000F0000000)) | (uint64(variant ^ 0x0F) << 28) 52 | // } 53 | 54 | // Implements [GlyphCacheHandler].GetMask(...) 55 | func (self *DefaultCacheHandler) GetMask(index sfnt.GlyphIndex) (GlyphMask, bool) { 56 | self.activeKey[2] = (self.activeKey[2] & ^uint64(0x000000000000FFFF)) | uint64(index) 57 | return self.cache.GetMask(self.activeKey) 58 | } 59 | 60 | // Implements [GlyphCacheHandler].PassMask(...) 61 | func (self *DefaultCacheHandler) PassMask(index sfnt.GlyphIndex, mask GlyphMask) { 62 | self.activeKey[2] = (self.activeKey[2] & ^uint64(0x000000000000FFFF)) | uint64(index) 63 | self.cache.PassMask(self.activeKey, mask) 64 | } 65 | 66 | // Provides access to the underlying [DefaultCache]. 67 | func (self *DefaultCacheHandler) Cache() *DefaultCache { 68 | return self.cache 69 | } 70 | -------------------------------------------------------------------------------- /cache/default_handler_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | import "github.com/tinne26/etxt/mask" 6 | import "github.com/tinne26/etxt/fract" 7 | 8 | func TestDefaultHandler(t *testing.T) { 9 | rast := mask.DefaultRasterizer{} 10 | cache := NewDefaultCache(16 * 1024 * 1024) 11 | handler := cache.NewHandler() 12 | handler.NotifyFontChange(nil) 13 | handler.NotifyRasterizerChange(&rast) 14 | handler.NotifySizeChange(12 << 6) 15 | handler.NotifyFractChange(fract.Point{1, 1}) 16 | 17 | if handler.Cache().CurrentSize() != 0 { 18 | t.Fatal("no mask yet size != 0") 19 | } 20 | 21 | if GlyphMaskByteSize(nil) != constMaskSizeFactor { 22 | t.Fatal("assumptions") 23 | } 24 | 25 | _, found := handler.GetMask(9) 26 | if found { 27 | t.Fatal("no mask in the cache") 28 | } 29 | handler.PassMask(9, nil) 30 | mask, found := handler.GetMask(9) 31 | if !found { 32 | t.Fatal("expected mask in cache") 33 | } 34 | if mask != nil { 35 | t.Fatal("expected nil mask") 36 | } 37 | 38 | gotSize := handler.Cache().PeakSize() 39 | if gotSize != constMaskSizeFactor { 40 | t.Fatalf("expected %d bytes, got %d", constMaskSizeFactor, gotSize) 41 | } 42 | 43 | mask, found = cache.GetMask([3]uint64{0, 0x0000000000000000, 0x0000030000410009}) 44 | if !found { 45 | t.Fatal("expected mask at the given key") 46 | } 47 | if mask != nil { 48 | t.Fatal("expected nil mask") 49 | } 50 | 51 | preSize := cache.CurrentSize() 52 | cache.removeRandOldEntry() 53 | freed := preSize - cache.CurrentSize() 54 | if freed != constMaskSizeFactor { 55 | t.Fatalf("expected %d freed bytes, got %d", constMaskSizeFactor, freed) 56 | } 57 | 58 | preSize = cache.CurrentSize() 59 | cache.removeRandOldEntry() 60 | freed = preSize - cache.CurrentSize() 61 | if freed != 0 { 62 | t.Fatalf("expected 0 freed bytes, got %d", freed) 63 | } 64 | 65 | gotSize = handler.Cache().CurrentSize() 66 | if gotSize != 0 { 67 | t.Fatalf("expected 0 bytes, got %d", gotSize) 68 | } 69 | 70 | gotSize = handler.Cache().PeakSize() 71 | if gotSize != constMaskSizeFactor { 72 | t.Fatalf("expected %d bytes, got %d", constMaskSizeFactor, gotSize) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cache/doc.go: -------------------------------------------------------------------------------- 1 | // The cache subpackage defines the [GlyphCacheHandler] interface used 2 | // within etxt and provides a default cache implementation. 3 | // 4 | // Since glyph rasterization is an expensive CPU process, caches are a 5 | // vital part of any real-time text rendering pipeline. 6 | // 7 | // As far as practical advice goes, "how to determine the size of my cache" 8 | // would be the main topic of discussion. Sadly, there's no good rule of 9 | // thumb to say "set your cache size to half its peak memory usage" or 10 | // similar. Cache sizes really depend on your use-case: sometimes you have 11 | // only a couple fonts at a few fixed sizes and you want your cache to fit 12 | // everything. Sometimes you determine your font sizes based on the current 13 | // screen size and can absolutely not pretend to cache all the masks that 14 | // the renderers may generate. The [DefaultCache.PeakSize]() function is 15 | // a good tool to assist you, but you will have to figure out your requirements 16 | // by yourself. Of course, you can also just use Renderer.Utils().SetCache8MiB() 17 | // and see how far does that get you. 18 | // 19 | // To give a more concrete size reference, though, let's assume a normal or 20 | // small reading font size, where each glyph mask is around 11x11 on average 21 | // (many glyphs don't have ascenders or descenders). That's about 676 bytes per 22 | // mask on Ebitengine. Then say we will have around 64 different glyphs (there 23 | // may only be 26 letters in english, but we also need to account for uppercase, 24 | // numbers, punctuation, variants with diacritic marks, etc.). We would already 25 | // be around 42KiB of data. If you account for a couple different fonts being 26 | // used in an app, bigger sizes and maybe variants with italics or bold, you get 27 | // closer to be working with MiBs of data, not KiBs. If you also disable full 28 | // quantization, each glyph mask will need to be rendered for different subpixel 29 | // positions. This can range from anywhere between x2 to x64 memory usage in most 30 | // common scenarios. 31 | // 32 | // The summary would be that anything below 64KiB of cache is almost sure to fall 33 | // short in many scenarios, with a few MiBs of capacity probably being a much 34 | // better ballpark estimate for what many games and applications will end up using 35 | // on their UI screens. 36 | package cache 37 | -------------------------------------------------------------------------------- /cache/ebiten_no.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package cache 4 | 5 | import "image" 6 | 7 | // Alias for etxt.GlyphMask. 8 | type GlyphMask = *image.Alpha 9 | 10 | const constMaskSizeFactor = 56 11 | 12 | func GlyphMaskByteSize(mask GlyphMask) uint32 { 13 | if mask == nil { 14 | return constMaskSizeFactor 15 | } 16 | w, h := mask.Rect.Dx(), mask.Rect.Dy() 17 | return maskDimsByteSize(w, h) 18 | } 19 | 20 | func maskDimsByteSize(width, height int) uint32 { 21 | return uint32(width*height) + constMaskSizeFactor 22 | } 23 | 24 | // used for testing purposes 25 | func newEmptyGlyphMask(width, height int) GlyphMask { 26 | return GlyphMask(image.NewAlpha(image.Rect(0, 0, width, height))) 27 | } 28 | -------------------------------------------------------------------------------- /cache/ebiten_yes.go: -------------------------------------------------------------------------------- 1 | //go:build !gtxt 2 | 3 | package cache 4 | 5 | import "github.com/hajimehoshi/ebiten/v2" 6 | 7 | // Same as [etxt.GlyphMask], redefined locally for improved clarity 8 | // and consistency with the etxt parent package when defining caches 9 | // and the [GlyphCacheHandler] interface. 10 | // 11 | // [etxt.GlyphMask]: https://pkg.go.dev/github.com/tinne26/etxt@v0.0.9#GlyphMask 12 | type GlyphMask = *ebiten.Image 13 | 14 | // Based on Ebitengine internals. 15 | const constMaskSizeFactor = 192 16 | 17 | // Returns an approximation of a [GlyphMask] size in bytes. 18 | // 19 | // With Ebitengine, the exact amount of mipmaps and helper fields is 20 | // not known, so the values may not be completely accurate, and should 21 | // be treated as a lower bound. With gtxt, the returned values are 22 | // exact. 23 | func GlyphMaskByteSize(mask GlyphMask) uint32 { 24 | if mask == nil { 25 | return constMaskSizeFactor 26 | } 27 | w, h := mask.Size() 28 | return maskDimsByteSize(w, h) 29 | } 30 | 31 | func maskDimsByteSize(width, height int) uint32 { 32 | return uint32(width*height)*4 + constMaskSizeFactor 33 | } 34 | 35 | // used for testing purposes 36 | func newEmptyGlyphMask(width, height int) GlyphMask { 37 | return GlyphMask(ebiten.NewImage(width, height)) 38 | } 39 | -------------------------------------------------------------------------------- /cache/glyph_cache_handler.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/tinne26/etxt/fract" 5 | "github.com/tinne26/etxt/mask" 6 | "golang.org/x/image/font/sfnt" 7 | ) 8 | 9 | // A [GlyphCacheHandler] acts as an intermediator between a glyph cache 10 | // and another object, typically a [Renderer], to give the later a clear 11 | // target interface to conform to while abstracting the details of an 12 | // underlying cache, which might be finickier to deal with directly 13 | // in a performant way. 14 | // 15 | // Glyph cache handlers can't be used concurrently unless the concrete 16 | // implementation explicitly says otherwise. 17 | // 18 | // [Renderer]: https://pkg.go.dev/github.com/tinne26/etxt@v0.0.9#Renderer 19 | type GlyphCacheHandler interface { 20 | 21 | // --- configuration notification methods --- 22 | // Update methods (called only if required so overhead can be low). 23 | // Passed values must always be non-nil, except for NotifyOtherChange(). 24 | 25 | // Notifies that the font in use has changed. 26 | NotifyFontChange(*sfnt.Font) 27 | 28 | // Notifies that the text size (in pixels) has changed. 29 | NotifySizeChange(fract.Unit) 30 | 31 | // Notifies that the rasterizer has changed. Typically, the 32 | // rasterizer's CacheSignature() will be used to tell them apart. 33 | NotifyRasterizerChange(mask.Rasterizer) // called on config changes too 34 | 35 | // Notifies that the fractional drawing position has changed. 36 | // Only the 6 bits corresponding to the non-integer part of each 37 | // coordinate are considered. 38 | NotifyFractChange(fract.Point) 39 | 40 | //NotifyOtherChange(any) // more methods like this could be added 41 | 42 | // --- cache access methods --- 43 | 44 | // Gets the mask image for the given glyph index and current configuration. 45 | // The bool indicates whether the mask has been found (as it may be nil). 46 | GetMask(sfnt.GlyphIndex) (GlyphMask, bool) 47 | 48 | // Passes a mask image for the given glyph index and current 49 | // configuration to the underlying cache. PassMask should only 50 | // be called after GetMask() fails. 51 | // 52 | // Given a specific configuration, the contents of the mask image 53 | // must always be consistent. This implies that passed masks may be 54 | // ignored if a mask is already cached under that configuration, as 55 | // it will be considered superfluous. In other words: passing different 56 | // masks for the same configuration may cause inconsistent results. 57 | PassMask(sfnt.GlyphIndex, GlyphMask) 58 | 59 | // Notice that many more methods could be provided, like Get/Pass 60 | // for Advance, Kern, Bounds, etc., and other methods like Clear() 61 | // or ReleaseFont(), but since etxt doesn't need that, the interface 62 | // is limited to masks. You can expand whatever you want with your 63 | // own interfaces and type assertions. 64 | // 65 | // Hinting is also another interesting topic, but since sfnt doesn't 66 | // apply hinting instructions, there's not much to do here. Even if sfnt 67 | // did, managing glyph "variants" would be wiser, as hinting instructions 68 | // often exist only for a few characters at a few specific sizes only, 69 | // and you may not want to keep lots of superfluous duplicated masks for 70 | // hinted and unhinted configs. 71 | } 72 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // etxt is a package for text rendering designed to be used with 2 | // [Ebitengine], a 2D game engine made by Hajime Hoshi for Golang. 3 | // 4 | // To get started, you should create a [Renderer] and set up 5 | // a font and a cache: 6 | // 7 | // text := etxt.NewRenderer() 8 | // text.SetFont(font) // e.g. lbrtsans.Font() from github.com/tinne26/fonts 9 | // text.Utils().SetCache8MiB() 10 | // 11 | // Then, you can further adjust the renderer properties with functions 12 | // like [Renderer.SetColor](), [Renderer.SetSize](), [Renderer.SetAlign](), 13 | // [Renderer.SetScale]() and many others. 14 | // 15 | // Once you have everything configured to your liking, drawing is quite 16 | // straightforward: 17 | // 18 | // text.Draw(canvas, "Hello world!", x, y) 19 | // 20 | // To learn more, make sure to check the [examples]! 21 | // 22 | // [examples]: https://github.com/tinne26/etxt/tree/v0.0.9/examples 23 | // [Ebitengine]: https://ebitengine.org 24 | package etxt 25 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Additional documentation 2 | A collection of additional documents covering topics related to font management and rendering, with both general context and specific advice for **etxt** and even a bit of game development. 3 | 4 | All the documents are linked directly from the code documentation, so... unless you are very curious about any specific topic, don't bother exploring this folder. You will be linked to the appropriate documents when relevant. 5 | -------------------------------------------------------------------------------- /docs/fixed-26-6.md: -------------------------------------------------------------------------------- 1 | # Fixed.Int26_6 2 | When working with fonts we often have to deal with **fixed precision** numbers. 3 | 4 | You are probably already familiar with *floating* precision numbers (`float32`, `float64`), so fixed precision numbers should be fairly easier to understand: instead of having a mantissa and an exponent, we simply have a certain amount of bits reserved for the **whole part** of the number, and the remaining bits being used for the **decimal part**. 5 | 6 | For example, with a fixed precision `int` that has 26 bits for the whole number and 6 bits for the decimal part, we can represent integers between `2^25 - 1 = 33554431` and `-2^25 = -33554432`, with up to `2^6 = 64` different decimal values for each whole number. The representable decimal magnitudes are all multiples of `1/64 = 0.015625`. We can store an `int26.6` in an `int32`. 7 | 8 | To make it easier to interpret: 9 | - If you had an `int32` representing milliseconds and wanted to know how many seconds you have, you would automatically know to divide by 1000. You know your `int32` represents thousandths of seconds, or 1/1000th parts of a second. 10 | - If you have a fixed point `int26.6`, your `int32` represents 1/64th parts (remember that 64 comes from 2^6) of whatever you are measuring. Pixels in our case. 11 | 12 | ## So, why do we have to work with fixed precision numbers? 13 | Since font outlines are scalable, sometimes we need to work with coordinates that do not exactly match the pixel grid, and fixed point numbers have been traditionally chosen to take care of this. Modern processors fare much better with floating point operations, but historically, the speedup of using fixed point vs floating point was critical in getting the whole process to be fast. Both 16.16 and 26.6 fixed point types are common when working with fonts, but only 26.6 is used within **etxt**. 14 | 15 | ## In which situations do we need fixed precision numbers? 16 | - After drawing a glyph, the amount of space we need to advance to prepare for drawing the next glyph may leave us at a fractional pixel coordinate. 17 | - From the previous point, if you are not quantizing fractional coordinates, you may have to start drawing text at a fractional pixel position. That's why `DrawFract()` exists and why `Traverse*` functions use `fixed.Int26_6` values. 18 | - Glyph rasterizers need to be able to deal with fractional pixel positions. 19 | 20 | ## Practical advice for operating with fixed precision numbers 21 | There are two key packages to be aware of when dealing with fixed precision numbers: 22 | - The Golang package where they are defined, [x/image/math/fixed](https://pkg.go.dev/golang.org/x/image/math/fixed). 23 | - The [etxt/efixed](https://pkg.go.dev/github.com/tinne26/etxt/efixed) subpackage, which contains a few additional helpful functions. 24 | 25 | Most of the time, to operate with fixed precision numbers you only need to do one of the following: 26 | - Use the right rounding function, like [`Ceil()`](https://pkg.go.dev/golang.org/x/image/math/fixed#Int26_6.Ceil), [`Floor()`](https://pkg.go.dev/golang.org/x/image/math/fixed#Int26_6.Floor) and [`efixed.ToIntHalfUp()`](https://pkg.go.dev/github.com/tinne26/etxt/efixed#ToIntHalfUp) and its variants. 27 | - Convert from/to integer coordinates: 28 | - To convert from `int` to `fixed.Int26_6` you can use [`efixed.FromInt()`](https://pkg.go.dev/github.com/tinne26/etxt/efixed#FromInt). 29 | - To convert from `fixed.Int26_6` to `int`, you generally round the `fixed.Int26_6` variable itself with [`Ceil()`](https://pkg.go.dev/golang.org/x/image/math/fixed#Int26_6.Ceil). 30 | - Convert from/to actual `float64` coordinates: 31 | - To convert from `float64` to `fixed.Int26_6` you use [`efixed.FromFloat64()`](https://pkg.go.dev/github.com/tinne26/etxt/efixed#FromFloat64) and its variants. 32 | - To convert from `fixed.Int26_6` to `float64` you use [`efixed.ToFloat64()`](https://pkg.go.dev/github.com/tinne26/etxt/efixed#ToFloat64). 33 | 34 | Quick sample snippet: 35 | ```Golang 36 | // convert from int to fixed26.6 37 | myInt := 100 38 | fixedValue := fract.FromInt(myInt) // == fixed.Int26_6(myInt << 6) 39 | 40 | // add 0.5 to the fixed value 41 | fixedValue += 32 // 64 would add "1", so 32 is half that, 0.5 42 | 43 | // convert to float64 and display 44 | floatValue := fixedValue.ToFloat64() // == float64(fixedValue)/64.0 45 | fmt.Printf("value = %f\n", floatValue) // prints "value = 100.50000" 46 | ``` 47 | 48 | -------------------------------------------------------------------------------- /docs/img/glyph_edges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinne26/etxt/2cb49951efa0d330dd41d61a118b7cd280071722/docs/img/glyph_edges.png -------------------------------------------------------------------------------- /docs/img/glyph_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinne26/etxt/2cb49951efa0d330dd41d61a118b7cd280071722/docs/img/glyph_filled.png -------------------------------------------------------------------------------- /docs/img/glyph_sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinne26/etxt/2cb49951efa0d330dd41d61a118b7cd280071722/docs/img/glyph_sign.png -------------------------------------------------------------------------------- /docs/img/gtxt_aligns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinne26/etxt/2cb49951efa0d330dd41d61a118b7cd280071722/docs/img/gtxt_aligns.png -------------------------------------------------------------------------------- /docs/img/gtxt_mirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinne26/etxt/2cb49951efa0d330dd41d61a118b7cd280071722/docs/img/gtxt_mirror.png -------------------------------------------------------------------------------- /docs/img/gtxt_outline_cheap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinne26/etxt/2cb49951efa0d330dd41d61a118b7cd280071722/docs/img/gtxt_outline_cheap.png -------------------------------------------------------------------------------- /docs/img/gtxt_quantization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinne26/etxt/2cb49951efa0d330dd41d61a118b7cd280071722/docs/img/gtxt_quantization.png -------------------------------------------------------------------------------- /docs/img/outline_vs_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinne26/etxt/2cb49951efa0d330dd41d61a118b7cd280071722/docs/img/outline_vs_raster.png -------------------------------------------------------------------------------- /docs/pixel-tips.md: -------------------------------------------------------------------------------- 1 | # Tips for pixel-art-like vectorial fonts 2 | 3 | Pixel-art-like fonts can be used with etxt as long as they also contain glyph outlines (as opposed to fonts with only glyph bitmaps). This document is focused on etxt, but some of the advice is general and also applicable to `ebiten/text/v2`. 4 | 5 | Quick practical advice: 6 | - Enable full horizontal quantization with [`Renderer.Fract().SetHorzQuantization(etxt.QtFull)`](https://pkg.go.dev/github.com/tinne26/etxt@v0.0.9#RendererFract.SetHorzQuantization). Vertical quantization is already set to full pixels by default. Ebitengine text packages determine ["glyph variations" automatically](https://github.com/hajimehoshi/ebiten/blob/v2.8.5/text/v2/text.go#L96-L114), and this isn't directly customizable there. 7 | - Pixel-art-like vectorial fonts are designed for a single size (or their multiples). 16px is common... but single sizes have some implications: 8 | - You don't want to use [Renderer.SetScale](). Leave the scale to 1. You want to be rendering on your logical, fixed-size canvas, and only doing scaling later on the projection from the logical canvas to the full resolution screen. 9 | - If your font doesn't look sharp at the intended size, DPI may be to blame. There are two common DPI values used in the wild: 72DPI and 96DPI. On etxt, 72DPI is implicitly used. If a font is designed for 96DPI, you may need to multiply its size by 4/3 or similar conversions. 10 | - If you still want to use pixel art fonts at arbitrary sizes, you might consider using the [`SharpRasterizer`](https://pkg.go.dev/github.com/tinne26/etxt@v0.0.9/mask#SharpRasterizer) to avoid blurriness (`rasterizer.Glyph().SetRasterizer(&mask.SharpRasterizer{})`). 11 | 12 | In general, etxt is not optimized or oriented to pixel art fonts, and sfnt, the underlying library used to parse the fonts, doesn't have support for glyph bitmaps. This doesn't mean that using etxt is crazy if you are working with such fonts; etxt still provides many useful features no matter the type of font you are using. That being said, if a specialized package existed for dealing with this kind of fonts on Ebitengine, that could easily become a better alternative. I'm working on [ptxt](), but it still has a long way to go. For a simpler approach, you might also be interested in [ingenten](https://github.com/Frabjous-Studios/ingenten)). 13 | -------------------------------------------------------------------------------- /docs/quantization.md: -------------------------------------------------------------------------------- 1 | # Quantization 2 | 3 | Quantization in the context of **etxt** refers to the process of adjusting glyph coordinates to the pixel grid. 4 | 5 | Whenever a glyph is drawn, we need to update the drawing position for the next glyph. Quite often, the new position won't fall perfectly at the start of a pixel, but rather at a fractional pixel position. What should we do then? Jump directly to the next whole pixel? Try to draw the glyph at this fractional position? 6 | 7 | Before deciding, we need to explain the trade-offs: 8 | - If we try to draw glyphs at their exact fractional positions, we will be respecting the flow of the text as much as possible... but we will potentially have to cache each glyph in a variety of fractional positions. The maximum fractional precision per axis is 1/64th of a pixel. This means that if we don't quantize text at all in the horizontal axis, we may have to store each glyph up to 64 times in our cache, all with very slightly different positions. If we are also not quantizing in the vertical axis, the possibilities are multiplied by 64 again, resulting in a maximum of 4096 sub-positions per glyph. That's... not ideal. 9 | - If we fully quantize glyph positions to the pixel grid, we only need to store "one variation" of each glyph, which is great, but the flow of the text may be slightly off. 10 | 11 | ## So what do we do? Quantize or not quantize? 12 | 13 | By default, **etxt** does full glyph quantization (aligns glyphs to the pixel grid), but you can modify this behavior through `Renderer.SetQuantizerStep()`. The best choice, though, will depend on the situation: 14 | - When you are working with big text (e.g. >=32px), you rarely want to consider fractional pixel positions. Leave quantization on, let the renderer align glyphs to the pixel grid and call it a day, no one will be the wiser. 15 | - When you are working with small text, you generally want to increase the precision of the text's horizontal positioning if you want high quality results. Not everyone can tell, and the font being used and other variables can make this more or less necessary, but I can tell and it's more pleasant to have *some respect* for the fractional positions. Setting `step = 22` (1/3rd of a pixel) or `step = 16` (1/4th of a pixel) with `Renderer.SetQuantizerStep(16, 64)` is virtually always enough. 16 | - You almost always want to keep the vertical positions quantized to the pixel grid. Text flows horizontally, not vertically, so there's no need to be precise in the vertical axis (it will waste space in the glyphs cache and it can even look worse in many cases). 17 | - The main exception to the previous rules is when you want to animate text to give it movement. If you start moving your text but keep it quantized to the pixel grid, the movement will look jittery and jumpy. So, if you need high quality text movement animations, specially when they are very slow, you will have to respect the fractional positions, even if you are working with big text. I'd use at least `step = 16` on animations that aren't too slow. 18 | - Another exception is if you aren't using a cache. I don't know when or why would you do that, but if you are not using a cache you may as well go with the maximum precision (`Renderer.SetQuantizerStep(1, 1)`), because you are fully recomputing glyph masks each time anyway. 19 | 20 | ## Visual comparison 21 | 22 | Here's an example from [examples/gtxt/quantization](https://github.com/tinne26/etxt/blob/v0.0.9/examples/gtxt/quantization/main.go): 23 | ![](https://github.com/tinne26/etxt/blob/v0.0.9/docs/img/gtxt_quantization.png?raw=true) 24 | 25 | The differences are visible if you start comparing the lines letter by letter, but they are also not major enough to be obvious if you aren't focusing on them. Different sizes and fonts may produce different results. For example, monospaced fonts and fonts in a pixelated style may look more consistent with quantization, while fonts with more natural or hand drawn styles will almost always flow better without full horizontal quantization. 26 | -------------------------------------------------------------------------------- /docs/renderer.md: -------------------------------------------------------------------------------- 1 | # Renderers 2 | Random bits of trivia and advice: 3 | - Many small games can do all their text rendering with a single `etxt.Renderer`, by simply changing fonts and sizes as needed. No need for an *army* of renderers. 4 | - You might still need more than one renderer if you need to draw text concurrently, want to use different caches, want to wrap renderers on custom types for advanced use-cases, or simply to organize your code more naturally. 5 | - While renderers aren't too heavy or slow to initialize, do not create new ones on each frame. If you ever get in the business of pooling them (which should be a last resource, but you do you), I recommend setting the font(s) to nil first. 6 | - Even if drawing text is reasonably performant once glyph masks are cached, it's always good to remember that sometimes you can draw to an offscreen image to avoid doing so much work for text rendering on each frame. That said, drawing to an offscreen also has some downsides when the screen size changes, as you might need to re-render. 7 | - If you have a complex UI system, it's advisable to work with color palettes, font sets and sizes at an abstract level (e.g: main, background and highlight colors, main and title font, heading, normal and detail sizes, etc.) instead of passing all that information manually to the renderer. While the `etxt.Renderer` is easy to use directly, in many cases you will want to use it as building block, not as the "definitive" abstraction. It's not and it doesn't try to be. 8 | 9 | ## Drawing UI at full resolution 10 | To get crisp text at big sizes, it's important that you keep in mind what's your game screen size. When working with Ebitengine, it's very common to use a fixed, small screen size, draw your pixel art there, and then forget that if you also draw your text and UI at that small size it will look terrible when it's scaled up. See the [display scaling](https://github.com/tinne26/etxt/blob/v0.0.9/docs/display-scaling.md) document for further advice. 11 | 12 | ## Drawing UI at small sizes 13 | To get crisp text at small sizes, I'm sorry, but since this package depends on [**sfnt**](https://pkg.go.dev/golang.org/x/image/font/sfnt) and **sfnt** doesn't have support for hinting instructions, small text is not going to look as good as it can. Maybe some day. 14 | 15 | ...or you can try to implement [subpixel rendering](https://en.wikipedia.org/wiki/Subpixel_rendering) in a custom rasterizer... 16 | -------------------------------------------------------------------------------- /docs/shaping.md: -------------------------------------------------------------------------------- 1 | # Text shaping 2 | In Arabic, letters are written in different forms based on their position within a word. In Devaganari (Indic), groups of consonants may require ligatures or different glyph forms. Khmer (Combodian script) has diacritic and syllable-modifying marks... These scripts, among many others, are known as [**complex scripts**](https://en.wikipedia.org/wiki/Complex_text_layout), scripts where the shape or positioning of graphemes can vary based on their relation to other graphemes. 3 | 4 | In contrast, Latin, Cyrillic, Greek, Hiragana and many others are examples of non-complex scripts. 5 | 6 | When it comes to complex scripts, Unicode doesn't include code points for all the possible ligatures, clusters and glyph variations. This means the basic process used for non-complex scripts of mapping Unicode code points to font glyphs is not enough. Instead, a process known as **text shaping** is required to convert an input text to an output sequence of glyphs that takes into account the specific scripts and fonts being used. 7 | 8 | This is a complex process that can vary significantly for each script, requiring lots of specific knowledge and individualized handling. HarfBuzz is one of the most mature text shaping libraries in use nowadays. You can read their own definition of text shaping at https://harfbuzz.github.io/what-is-harfbuzz.html. 9 | 10 | ## etxt support for text shaping 11 | **etxt** doesn't offer any tools to do text shaping. A concept of `Twine` was developed to help improve the situation and allow direct use of glyph indices... but the implementation, although functional, was too complex both for the maintainer and the users. 12 | 13 | Sadly, there's a hole in Go's landscape when it comes to text shaping: the most official package for font manipulation in Golang, [**sfnt**](https://pkg.go.dev/golang.org/x/image/font/sfnt), does not expose the GSUB and GPOS font tables required to implement text shaping on your own. This forces Golang programmers to either: 14 | - Fork or reimplement **sfnt** functionality before being able to work on text shaping (or directly contribute to move https://github.com/golang/go/issues/45325 forward). 15 | - Use CGO bindings to bigger libraries like HarfBuzz. See https://pkg.go.dev/github.com/npillmayer/gotype/engine/text/textshaping. 16 | - Reimplement bigger libraries like HarfBuzz in pure Go. See https://github.com/go-text/typesetting. This is what Hajime started using in [`ebiten/v2/text/v2`](https://pkg.go.dev/github.com/hajimehoshi/ebiten/v2/text/v2), so this is your best choice if you need to support complex scripts at the moment. 17 | 18 | This is a sad situation because while universal text shaping is a gigantic ~~mess~~ problem and it would be quite insane to attempt to roll your own solution when HarfBuzz already exists, the truth is that in some contexts like indie game development, doing text shaping for a single language (e.g, your own) and a controlled set of fonts would be perfectly reasonable. Instead, right now you are forced to either go big or go home. 19 | 20 | To be completely honest, I don't believe in Unicode or [SFNT font formats](https://en.wikipedia.org/wiki/SFNT) at all (they are too big and complex for their own or anyone's good), so I see text shaping as just another layer on top of a broken foundation, but that's a story for another day. Hopefully this document gave you some context and helped you understand the relationship between scripts, fonts and Unicode a bit better. 21 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # etxt examples 2 | 3 | This folder contains two subfolders: 4 | - `gtxt` is the basic examples folder. The code uses the generic **etxt** version (`-tags gtxt`), but the techniques showcased have general applicability. This folder contains many simple examples that will help you get started with etxt. 5 | - `ebiten` contains examples on how to use **etxt** with Ebitengine. These tend to be more advanced than `gtxt` examples, but the end results are also more inspiring. If you want to see what etxt is capable of, this is the place. 6 | 7 | As long as you have Golang installed (>=go1.18), you can run the examples directly without any previous step. Almost all the programs expect one argument with a path to the font to use[^1]: 8 | ``` 9 | go run -tags gtxt github.com/tinne26/etxt/examples/gtxt/sizer_expand@latest path/to/your_font.ttf 10 | ``` 11 | 12 | [^1]: If you need a quick font download, you can just pick [Liberation Sans from this link](https://github.com/tinne26/fonts/blob/main/liberation/lbrtsans/LiberationSans-Regular.ttf). It's the sans-serif version of the font embedded for the example on etxt's readme (`examples/ebiten/words`). 13 | 14 | For Ebitengine examples, omit the `gtxt` tag: 15 | ``` 16 | go run github.com/tinne26/etxt/examples/ebiten/colorful@latest path/to/your_font.ttf 17 | ``` 18 | 19 | Alternatively, if you are feeling lazy, you can visit https://tinne26.github.io/etxt-examples/ for some web-based example ports. Or you can check a few `gtxt` results below: 20 | 21 | ### gtxt/aligns 22 | ![](https://raw.githubusercontent.com/tinne26/etxt/v0.0.9/docs/img/gtxt_aligns.png) 23 | 24 | ### gtxt/quantization 25 | ![](https://raw.githubusercontent.com/tinne26/etxt/v0.0.9/docs/img/gtxt_quantization.png) 26 | 27 | ### gtxt/outline_cheap 28 | ![](https://raw.githubusercontent.com/tinne26/etxt/v0.0.9/docs/img/gtxt_outline_cheap.png) 29 | 30 | ### gtxt/mirror 31 | ![](https://raw.githubusercontent.com/tinne26/etxt/v0.0.9/docs/img/gtxt_mirror.png) 32 | -------------------------------------------------------------------------------- /examples/ebiten/aligns/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/aligns 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.10-0.20250320134748-7dafb740a310 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/jezek/xgb v1.1.0 // indirect 14 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 15 | golang.org/x/image v0.9.0 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/all_glyphs/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/all_glyphs 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | golang.org/x/image v0.9.0 9 | ) 10 | 11 | require ( 12 | github.com/ebitengine/purego v0.3.0 // indirect 13 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/color_markup/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/color_markup 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | golang.org/x/image v0.9.0 9 | ) 10 | 11 | require ( 12 | github.com/ebitengine/purego v0.3.0 // indirect 13 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/colorful/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/colorful 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | golang.org/x/image v0.9.0 9 | ) 10 | 11 | require ( 12 | github.com/ebitengine/purego v0.3.0 // indirect 13 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/colorful/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "log" 7 | "math" 8 | "os" 9 | 10 | "github.com/hajimehoshi/ebiten/v2" 11 | "github.com/tinne26/etxt" 12 | "github.com/tinne26/etxt/font" 13 | "github.com/tinne26/etxt/fract" 14 | "golang.org/x/image/font/sfnt" 15 | ) 16 | 17 | // This example draws some text with a color changing effect, 18 | // where each letter changes color progressively. You can 19 | // run it like this: 20 | // go run github.com/tinne26/etxt/examples/ebiten/colorful@latest path/to/font.ttf 21 | // 22 | // This example showcases how to use RendererGlyph.SetDrawFunc(), 23 | // creating a custom drawing function to manually alter the color 24 | // of each letter in the text. For other examples of SetDrawFunc(), 25 | // see also examples/ebiten/shaking. 26 | // 27 | // Notice that changing text color through SetDrawFunc() is rather 28 | // unusual, and in most cases you will prefer using a Feed or creating 29 | // a complex Text object with color changing indications. In this 30 | // case, though, since we want to change the color *of each letter* 31 | // in a dynamic and continuous way, customizing the glyph drawing 32 | // function directly feels more natural. 33 | 34 | type Game struct { 35 | text *etxt.Renderer 36 | 37 | // text color variables 38 | red float64 39 | green float64 40 | blue float64 41 | shift float64 42 | } 43 | 44 | func (self *Game) Layout(winWidth int, winHeight int) (int, int) { 45 | scale := ebiten.DeviceScaleFactor() 46 | self.text.SetScale(scale) // relevant for HiDPI 47 | canvasWidth := int(math.Ceil(float64(winWidth) * scale)) 48 | canvasHeight := int(math.Ceil(float64(winHeight) * scale)) 49 | return canvasWidth, canvasHeight 50 | } 51 | 52 | func (self *Game) Update() error { 53 | // progressively change the values used in Draw to 54 | // determine letter colors, using different speeds 55 | self.red -= 0.0202 56 | self.green -= 0.0168 57 | self.blue -= 0.0227 58 | return nil 59 | } 60 | 61 | func (self *Game) Draw(screen *ebiten.Image) { 62 | // dark background 63 | screen.Fill(color.RGBA{0, 0, 0, 255}) 64 | 65 | // draw text 66 | bounds := screen.Bounds() 67 | self.shift = 0.0 // reset color shift factor 68 | self.text.Draw(screen, "Colorful!\nWonderful!", bounds.Dx()/2, bounds.Dy()/2) 69 | } 70 | 71 | // This is the function that we use to override the text renderer's default draw 72 | // function. It's set on the main through renderer.Glyph().SetDrawFunc(). 73 | func (self *Game) drawColorfulGlyph(target etxt.Target, glyphIndex sfnt.GlyphIndex, origin fract.Point) { 74 | // derive the color for the current letter from the initial/ values on 75 | // each color channel, the current offset, and the sine function 76 | r := (math.Sin(self.red+self.shift) + 1.0) / 2.0 77 | g := (math.Sin(self.green+self.shift) + 1.0) / 2.0 78 | b := (math.Sin(self.blue+self.shift) + 1.0) / 2.0 79 | textColor := color.RGBA{uint8(r * 255), uint8(g * 255), uint8(b * 255), 255} 80 | self.text.SetColor(textColor) // * 81 | // * Not all renderer properties are safe to change while drawing, 82 | // but color is one of the exceptions. 83 | 84 | // draw the glyph mask 85 | mask := self.text.Glyph().LoadMask(glyphIndex, origin) 86 | self.text.Glyph().DrawMask(target, mask, origin) 87 | 88 | // increase offset to apply to the next letters 89 | self.shift += 0.15 90 | } 91 | 92 | func main() { 93 | // get font path 94 | if len(os.Args) != 2 { 95 | msg := "Usage: expects one argument with the path to the font to be used\n" 96 | fmt.Fprint(os.Stderr, msg) 97 | os.Exit(1) 98 | } 99 | 100 | // parse font 101 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | fmt.Printf("Font loaded: %s\n", fontName) 106 | 107 | // create and configure renderer 108 | renderer := etxt.NewRenderer() 109 | renderer.Utils().SetCache8MiB() 110 | renderer.SetSize(64) 111 | renderer.SetFont(sfntFont) 112 | renderer.SetAlign(etxt.Center) 113 | 114 | // create game struct 115 | game := &Game{ 116 | text: renderer, 117 | red: -5.54, 118 | green: -4.3, 119 | blue: -6.4, 120 | } 121 | 122 | // override default text renderer draw function 123 | renderer.Glyph().SetDrawFunc(game.drawColorfulGlyph) 124 | 125 | // run the game 126 | ebiten.SetWindowTitle("etxt/examples/ebiten/colorful") 127 | ebiten.SetWindowSize(640, 480) 128 | err = ebiten.RunGame(game) 129 | if err != nil { 130 | log.Fatal(err) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /examples/ebiten/cutout/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/cutout 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/jezek/xgb v1.1.0 // indirect 14 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 15 | golang.org/x/image v0.9.0 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/debug_glyph/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/debug_glyph 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | golang.org/x/image v0.10.0 9 | ) 10 | 11 | require ( 12 | github.com/ebitengine/purego v0.4.0 // indirect 13 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.7.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/debug_glyph/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/hajimehoshi/ebiten/v2" 9 | "github.com/tinne26/etxt" 10 | "github.com/tinne26/etxt/font" 11 | "github.com/tinne26/etxt/fract" 12 | "github.com/tinne26/etxt/mask" 13 | "golang.org/x/image/font/sfnt" 14 | ) 15 | 16 | // This is the Ebitengine version of gtxt/debug_glyph. Not a true example, but 17 | // rather a debug program for when developing custom rasterizers and wanting 18 | // to check the results manually. In the case of Ebitengine, the values have 19 | // to pass through the GPU, and values may vary slightly compared to the gtxt 20 | // version (CPU rendering). 21 | 22 | const GlyphToDebug = 'A' 23 | 24 | func main() { 25 | // get font path 26 | if len(os.Args) != 2 { 27 | msg := "Usage: expects one argument with the path to the font to be used\n" 28 | fmt.Fprint(os.Stderr, msg) 29 | os.Exit(1) 30 | } 31 | 32 | // parse font 33 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | fmt.Printf("Font loaded: %s\n", fontName) 38 | 39 | // create and configure renderer 40 | // (notice that we don't set a cache, no need for a single glyph) 41 | renderer := etxt.NewRenderer() 42 | renderer.SetSize(18) 43 | renderer.SetFont(sfntFont) 44 | renderer.SetAlign(etxt.Center) 45 | renderer.Fract().SetHorzQuantization(etxt.QtFull) 46 | renderer.Fract().SetVertQuantization(etxt.QtFull) 47 | 48 | // set a custom rasterizer that we want to debug 49 | fauxRast := mask.FauxRasterizer{} 50 | //fauxRast.SetSkewFactor(-0.3) 51 | fauxRast.SetExtraWidth(+0.0) 52 | renderer.Glyph().SetRasterizer(&fauxRast) 53 | 54 | // set the debugging draw function 55 | renderer.Glyph().SetDrawFunc( 56 | func(_ etxt.Target, glyphIndex sfnt.GlyphIndex, position fract.Point) { 57 | mask := renderer.Glyph().LoadMask(glyphIndex, position) 58 | bounds := mask.Bounds() 59 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 60 | fmt.Printf("%04d: [ ", y) 61 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 62 | _, _, _, a := mask.At(x, y).RGBA() 63 | fmt.Printf("%03d ", a>>8) 64 | } 65 | fmt.Printf("]\n") 66 | } 67 | }) 68 | 69 | // draw the glyph to debug to hit the custom debug draw function 70 | err = ebiten.RunGame(&Game{text: renderer}) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | fmt.Print("Program exited successfully.\n") 75 | } 76 | 77 | // The game struct is required for ebitengine to initialize the graphical 78 | // command queue, but we are only trying to invoke our custom text rendering 79 | // debug function once and then we terminate right away. 80 | type Game struct { 81 | text *etxt.Renderer 82 | done bool 83 | } 84 | 85 | func (self *Game) Layout(w, h int) (int, int) { return w, h } 86 | func (self *Game) Update() error { 87 | if self.done { 88 | return ebiten.Termination 89 | } 90 | return nil 91 | } 92 | func (self *Game) Draw(canvas *ebiten.Image) { 93 | if self.done { 94 | return 95 | } 96 | bounds := canvas.Bounds() 97 | w, h := bounds.Dx(), bounds.Dy() 98 | self.text.Draw(canvas, string(GlyphToDebug), w/2, h/2) 99 | self.done = true 100 | } 101 | -------------------------------------------------------------------------------- /examples/ebiten/elastic_sizer/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/elastic_sizer 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/jezek/xgb v1.1.0 // indirect 14 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 15 | golang.org/x/image v0.9.0 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/faux_styles/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/faux_styles 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/jezek/xgb v1.1.0 // indirect 14 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 15 | golang.org/x/image v0.9.0 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/hover_shadow/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/hover_shadow 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/jezek/xgb v1.1.0 // indirect 14 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 15 | golang.org/x/image v0.9.0 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/hover_shadow/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "log" 7 | "math" 8 | "os" 9 | 10 | "github.com/hajimehoshi/ebiten/v2" 11 | "github.com/tinne26/etxt" 12 | "github.com/tinne26/etxt/font" 13 | "github.com/tinne26/etxt/fract" 14 | ) 15 | 16 | // This example shows how to combine a couple draws and some 17 | // very basic logic in order to create a simple effect when 18 | // hovering text with the mouse. There are still a few interesting 19 | // details here and there if you are still getting started with 20 | // etxt and Ebitengine, like measuring the text and manipulating 21 | // its fract.Rect, or adjusting the animation based on the display 22 | // scaling for consistent results across different setups. 23 | // You can run this example with: 24 | // go run github.com/tinne26/etxt/examples/ebiten/hover_shadow@latest path/to/font.ttf 25 | 26 | const HoverText = "Hover me please!" 27 | 28 | type Game struct { 29 | text *etxt.Renderer 30 | focus float64 31 | canvasWidth int 32 | canvasHeight int 33 | } 34 | 35 | func (self *Game) Layout(winWidth, winHeight int) (int, int) { 36 | scale := ebiten.DeviceScaleFactor() 37 | self.text.SetScale(scale) // relevant for HiDPI 38 | self.canvasWidth = int(math.Ceil(float64(winWidth) * scale)) 39 | self.canvasHeight = int(math.Ceil(float64(winHeight) * scale)) 40 | return self.canvasWidth, self.canvasHeight 41 | } 42 | 43 | func (self *Game) Update() error { 44 | // calculate target area. you could easily optimize this, 45 | // but we are being lazy and wasteful... and it's still ok 46 | targetRect := self.text.Measure(HoverText) 47 | ox, oy := self.canvasWidth/2, self.canvasHeight/2 48 | targetRect = targetRect.CenteredAtIntCoords(ox, oy) 49 | 50 | // determine if we are inside or outside the 51 | // hover area and adjust the "focus" level 52 | cursorPt := fract.IntsToPoint(ebiten.CursorPosition()) 53 | if targetRect.Contains(cursorPt) { 54 | self.focus += 0.05 55 | if self.focus > 1.0 { 56 | self.focus = 1.0 57 | } 58 | } else { 59 | self.focus -= 0.05 60 | if self.focus < 0.0 { 61 | self.focus = 0.0 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (self *Game) Draw(canvas *ebiten.Image) { 69 | const MaxOffsetX = 4 // max shadow x offset 70 | const MaxOffsetY = 4 // max shadow y offset 71 | 72 | // dark background 73 | canvas.Fill(color.RGBA{0, 0, 0, 255}) 74 | 75 | // draw text 76 | if self.focus > 0 { 77 | self.text.SetColor(color.RGBA{200, 0, 200, 200}) // sharp shadow 78 | scale := ebiten.DeviceScaleFactor() 79 | hx := self.canvasWidth/2 + int(self.focus*MaxOffsetX*scale) 80 | hy := self.canvasHeight/2 + int(self.focus*MaxOffsetY*scale) 81 | self.text.Draw(canvas, HoverText, hx, hy) 82 | } 83 | 84 | self.text.SetColor(color.RGBA{255, 255, 255, 255}) // main color 85 | self.text.Draw(canvas, HoverText, self.canvasWidth/2, self.canvasHeight/2) 86 | } 87 | 88 | func main() { 89 | // get font path 90 | if len(os.Args) != 2 { 91 | msg := "Usage: expects one argument with the path to the font to be used\n" 92 | fmt.Fprint(os.Stderr, msg) 93 | os.Exit(1) 94 | } 95 | 96 | // parse font 97 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | fmt.Printf("Font loaded: %s\n", fontName) 102 | 103 | // create and configure renderer 104 | renderer := etxt.NewRenderer() 105 | renderer.Utils().SetCache8MiB() 106 | renderer.SetSize(64) 107 | renderer.SetFont(sfntFont) 108 | renderer.SetAlign(etxt.Center) 109 | 110 | // run the game 111 | ebiten.SetWindowTitle("etxt/examples/ebiten/hover_shadow") 112 | ebiten.SetWindowSize(640, 480) 113 | err = ebiten.RunGame(&Game{text: renderer}) 114 | if err != nil { 115 | log.Fatal(err) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /examples/ebiten/measure/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/measure 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/jezek/xgb v1.1.0 // indirect 14 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 15 | golang.org/x/image v0.9.0 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/measure/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "log" 7 | "math" 8 | "os" 9 | 10 | "github.com/hajimehoshi/ebiten/v2" 11 | "github.com/hajimehoshi/ebiten/v2/inpututil" 12 | "github.com/tinne26/etxt" 13 | "github.com/tinne26/etxt/font" 14 | ) 15 | 16 | // This example allows you to interactively write text in an Ebitengine 17 | // program and see how the measurement rect for the text changes. You 18 | // can use backspace to remove characters and enter to create line 19 | // breaks. You can run it like this: 20 | // go run github.com/tinne26/etxt/examples/ebiten/measure@latest path/to/font.ttf 21 | 22 | type Game struct { 23 | text *etxt.Renderer 24 | content []rune // not very efficient, but AppendInputChars uses runes 25 | wrapMode bool 26 | } 27 | 28 | func (self *Game) Layout(winWidth, winHeight int) (int, int) { 29 | scale := ebiten.DeviceScaleFactor() 30 | self.text.SetScale(scale) // relevant for HiDPI 31 | canvasWidth := int(math.Ceil(float64(winWidth) * scale)) 32 | canvasHeight := int(math.Ceil(float64(winHeight) * scale)) 33 | return canvasWidth, canvasHeight 34 | } 35 | 36 | func (self *Game) Update() error { 37 | var keyRepeat = func(key ebiten.Key) bool { 38 | ticks := inpututil.KeyPressDuration(key) 39 | return ticks == 1 || (ticks > 14 && (ticks-14)%9 == 0) 40 | } 41 | 42 | if inpututil.IsKeyJustPressed(ebiten.KeyControlLeft) || inpututil.IsKeyJustPressed(ebiten.KeyControlRight) { 43 | self.wrapMode = !self.wrapMode 44 | } 45 | 46 | if keyRepeat(ebiten.KeyBackspace) && len(self.content) >= 1 { 47 | self.content = self.content[0 : len(self.content)-1] 48 | } else if keyRepeat(ebiten.KeyEnter) { 49 | self.content = append(self.content, '\n') 50 | } else { 51 | self.content = ebiten.AppendInputChars(self.content) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (self *Game) Draw(canvas *ebiten.Image) { 58 | // dark background 59 | canvas.Fill(color.RGBA{2, 1, 0, 255}) 60 | 61 | // get canvas size and basic coords 62 | bounds := canvas.Bounds() 63 | w, h := bounds.Dx(), bounds.Dy() 64 | pad := int((ebiten.DeviceScaleFactor() * float64(h)) / 64) 65 | x, y := pad*2, pad*2 66 | 67 | // highlight text's area rectangle and draw text 68 | areaColor := color.RGBA{8, 72, 88, 255} 69 | content := string(self.content) 70 | if self.wrapMode { // measure and draw 71 | maxLineWidth := w - 2*x 72 | textArea := self.text.MeasureWithWrap(content, maxLineWidth) 73 | textArea.AddInts(x, y).Clip(canvas).Fill(areaColor) 74 | self.text.DrawWithWrap(canvas, content, x, y, maxLineWidth) 75 | } else { 76 | textArea := self.text.Measure(content) 77 | textArea.AddInts(x, y).Clip(canvas).Fill(areaColor) 78 | self.text.Draw(canvas, content, x, y) 79 | } 80 | 81 | // draw instructions, fps and other info for fun 82 | self.text.Utils().StoreState() 83 | defer self.text.Utils().RestoreState() 84 | self.text.SetSize(14) 85 | self.text.SetAlign(etxt.Right | etxt.Baseline) 86 | var info string 87 | fps := ebiten.ActualFPS() 88 | if self.wrapMode { 89 | info = fmt.Sprintf("%d glyphs - %.2fFPS | Line Wrap On [CTRL]", len(self.content), fps) 90 | } else { 91 | info = fmt.Sprintf("%d glyphs - %.2fFPS | Line Wrap Off [CTRL]", len(self.content), fps) 92 | } 93 | self.text.Draw(canvas, info, w-pad, h-pad) 94 | } 95 | 96 | func main() { 97 | // get font path 98 | if len(os.Args) != 2 { 99 | msg := "Usage: expects one argument with the path to the font to be used\n" 100 | fmt.Fprint(os.Stderr, msg) 101 | os.Exit(1) 102 | } 103 | 104 | // parse font 105 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | fmt.Printf("Font loaded: %s\n", fontName) 110 | 111 | // create and configure renderer 112 | renderer := etxt.NewRenderer() 113 | renderer.Utils().SetCache8MiB() 114 | renderer.SetColor(color.RGBA{255, 255, 255, 255}) // white 115 | renderer.SetFont(sfntFont) 116 | renderer.SetSize(18) 117 | renderer.SetAlign(etxt.Top | etxt.Left) 118 | 119 | // run the game 120 | ebiten.SetWindowTitle("etxt/examples/ebiten/measure") 121 | ebiten.SetWindowSize(640, 480) 122 | ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) 123 | err = ebiten.RunGame(&Game{ 124 | text: renderer, 125 | content: []rune("Interactive text"), 126 | }) 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /examples/ebiten/miss_handler/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/miss_handler 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | golang.org/x/image v0.9.0 9 | ) 10 | 11 | require ( 12 | github.com/ebitengine/purego v0.3.0 // indirect 13 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/miss_handler/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "log" 7 | "math" 8 | "os" 9 | 10 | "github.com/hajimehoshi/ebiten/v2" 11 | "github.com/tinne26/etxt" 12 | "github.com/tinne26/etxt/font" 13 | ) 14 | 15 | // This example is only for testing what happens when glyphs are 16 | // missing if a suitable fallback handler is set. 17 | // 18 | // You can run it like this: 19 | // go run github.com/tinne26/etxt/examples/ebiten/miss_handler@latest path/to/font.ttf 20 | 21 | const Content = "We have àccëntš, we have the ру́сский алфави́т, we have japanese 漢字." 22 | 23 | type Game struct { 24 | text *etxt.Renderer 25 | } 26 | 27 | func (self *Game) Layout(winWidth, winHeight int) (int, int) { 28 | scale := ebiten.DeviceScaleFactor() 29 | self.text.SetScale(scale) // relevant for HiDPI 30 | canvasWidth := int(math.Ceil(float64(winWidth) * scale)) 31 | canvasHeight := int(math.Ceil(float64(winHeight) * scale)) 32 | return canvasWidth, canvasHeight 33 | } 34 | 35 | func (self *Game) Update() error { 36 | return nil 37 | } 38 | 39 | const NumContentTypes = 5 40 | 41 | func (self *Game) Draw(canvas *ebiten.Image) { 42 | canvas.Fill(color.RGBA{3, 2, 0, 255}) 43 | bounds := canvas.Bounds() 44 | w := bounds.Dx() 45 | x, y := bounds.Min.X+w/2, bounds.Min.Y+bounds.Dy()/2 46 | 47 | self.text.SetSize(18) 48 | self.text.SetColor(color.RGBA{255, 255, 255, 255}) 49 | self.text.SetAlign(etxt.Center) 50 | self.text.DrawWithWrap(canvas, Content, x, y, w-w/4) 51 | } 52 | 53 | func main() { 54 | // get font path 55 | if len(os.Args) != 2 { 56 | msg := "Usage: expects one argument with the path to the font to be used\n" 57 | fmt.Fprint(os.Stderr, msg) 58 | os.Exit(1) 59 | } 60 | 61 | // parse font 62 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | fmt.Printf("Font loaded: %s\n", fontName) 67 | 68 | // create and configure renderer 69 | renderer := etxt.NewRenderer() 70 | renderer.Utils().SetCache8MiB() 71 | renderer.SetFont(sfntFont) 72 | renderer.SetColor(color.RGBA{128, 128, 128, 255}) 73 | renderer.SetAlign(etxt.LastBaseline | etxt.Left) 74 | renderer.SetSize(16) 75 | 76 | // miss handler 77 | renderer.Glyph().SetMissHandler(etxt.OnMissNotdef) 78 | // glyph := renderer.Glyph().GetRuneIndex('?') 79 | // renderer.Glyph().SetMissHandler(func(*sfnt.Font, rune) (sfnt.GlyphIndex, bool) { 80 | // return glyph, false 81 | // }) 82 | 83 | // run the game 84 | ebiten.SetWindowTitle("etxt/examples/ebiten/miss_handler") 85 | ebiten.SetWindowSize(640, 480) 86 | err = ebiten.RunGame(&Game{ 87 | text: renderer, 88 | }) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /examples/ebiten/shaking/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/shaking 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | github.com/tinne26/sfntshape v0.0.0-20230731165804-e02366c93d9d 9 | golang.org/x/image v0.9.0 10 | ) 11 | 12 | require ( 13 | github.com/ebitengine/purego v0.3.0 // indirect 14 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 15 | github.com/jezek/xgb v1.1.0 // indirect 16 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 17 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 18 | golang.org/x/sync v0.1.0 // indirect 19 | golang.org/x/sys v0.6.0 // indirect 20 | golang.org/x/text v0.11.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /examples/ebiten/typewriter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/typewriter 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/jezek/xgb v1.1.0 // indirect 14 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 15 | golang.org/x/image v0.9.0 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/ebiten/words/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/ebiten/words 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | github.com/tinne26/etxt v0.0.9-alpha.8 8 | github.com/tinne26/fonts/liberation/lbrtserif v0.0.0-20230317183620-0b634734e4ec 9 | ) 10 | 11 | require ( 12 | github.com/ebitengine/purego v0.3.0 // indirect 13 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/image v0.9.0 // indirect 17 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 18 | golang.org/x/sync v0.1.0 // indirect 19 | golang.org/x/sys v0.6.0 // indirect 20 | golang.org/x/text v0.11.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /examples/ebiten/words/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | 7 | "github.com/hajimehoshi/ebiten/v2" 8 | "github.com/tinne26/etxt" 9 | "github.com/tinne26/fonts/liberation/lbrtserif" 10 | ) 11 | 12 | const WordsPerSec = 2.71828 13 | 14 | var Words = []string{ 15 | "solitude", "joy", "ride", "whisper", "leaves", "cookie", 16 | "hearts", "disdain", "simple", "death", "sea", "shallow", 17 | "self", "rhyme", "childish", "sky", "tic", "tac", "boom", 18 | } 19 | 20 | // ---- Ebitengine's Game interface implementation ---- 21 | 22 | type Game struct { 23 | text *etxt.Renderer 24 | wordIndex float64 25 | } 26 | 27 | func (self *Game) Layout(winWidth int, winHeight int) (int, int) { 28 | scale := ebiten.DeviceScaleFactor() 29 | self.text.SetScale(scale) // relevant for HiDPI 30 | canvasWidth := int(math.Ceil(float64(winWidth) * scale)) 31 | canvasHeight := int(math.Ceil(float64(winHeight) * scale)) 32 | return canvasWidth, canvasHeight 33 | } 34 | 35 | func (self *Game) Update() error { 36 | newIndex := (self.wordIndex + WordsPerSec/60.0) 37 | self.wordIndex = math.Mod(newIndex, float64(len(Words))) 38 | return nil 39 | } 40 | 41 | func (self *Game) Draw(canvas *ebiten.Image) { 42 | // background color 43 | canvas.Fill(color.RGBA{229, 255, 222, 255}) 44 | 45 | // get screen center position and text content 46 | bounds := canvas.Bounds() // assumes origin (0, 0) 47 | x, y := bounds.Dx()/2, bounds.Dy()/2 48 | text := Words[int(self.wordIndex)] 49 | 50 | // draw the text 51 | self.text.Draw(canvas, text, x, y) 52 | } 53 | 54 | // ---- main function ---- 55 | 56 | func main() { 57 | // create text renderer, set the font and cache 58 | renderer := etxt.NewRenderer() 59 | renderer.SetFont(lbrtserif.Font()) 60 | renderer.Utils().SetCache8MiB() 61 | 62 | // adjust main text style properties 63 | renderer.SetColor(color.RGBA{239, 91, 91, 255}) 64 | renderer.SetAlign(etxt.Center) 65 | renderer.SetSize(72) 66 | 67 | // set up Ebitengine and start the game 68 | ebiten.SetWindowTitle("etxt/examples/ebiten/words") 69 | err := ebiten.RunGame(&Game{text: renderer}) 70 | if err != nil { 71 | panic(err) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/gtxt/aligns/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/aligns 2 | 3 | go 1.18 4 | 5 | require github.com/tinne26/etxt v0.0.9-alpha.8 6 | 7 | require ( 8 | github.com/ebitengine/purego v0.3.0 // indirect 9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 10 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 11 | github.com/jezek/xgb v1.1.0 // indirect 12 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 13 | golang.org/x/image v0.9.0 // indirect 14 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 15 | golang.org/x/sync v0.1.0 // indirect 16 | golang.org/x/sys v0.6.0 // indirect 17 | golang.org/x/text v0.11.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /examples/gtxt/blend_modes/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/blend_modes 2 | 3 | go 1.18 4 | 5 | require github.com/tinne26/etxt v0.0.9-alpha.8 6 | 7 | require ( 8 | github.com/ebitengine/purego v0.3.0 // indirect 9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 10 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 11 | github.com/jezek/xgb v1.1.0 // indirect 12 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 13 | golang.org/x/image v0.9.0 // indirect 14 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 15 | golang.org/x/sync v0.1.0 // indirect 16 | golang.org/x/sys v0.6.0 // indirect 17 | golang.org/x/text v0.11.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /examples/gtxt/blend_modes/main.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/png" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | 14 | "github.com/tinne26/etxt" 15 | "github.com/tinne26/etxt/font" 16 | ) 17 | 18 | // Must be compiled with '-tags gtxt' 19 | 20 | const Alpha = 255 // can be changed (e.g. 144) if you want to see how 21 | // color modes work with semi-transparency too 22 | 23 | func main() { 24 | // get font path 25 | if len(os.Args) != 2 { 26 | msg := "Usage: expects one argument with the path to the font to be used\n" 27 | fmt.Fprint(os.Stderr, msg) 28 | os.Exit(1) 29 | } 30 | 31 | // parse font 32 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | fmt.Printf("Font loaded: %s\n", fontName) 37 | 38 | // create and configure renderer 39 | renderer := etxt.NewRenderer() 40 | renderer.Utils().SetCache8MiB() 41 | renderer.SetSize(24) 42 | renderer.SetFont(sfntFont) 43 | renderer.SetAlign(etxt.Center) 44 | 45 | // create target image and fill it with different colors 46 | target := image.NewRGBA(image.Rect(0, 0, 720, 300)) 47 | for i := 0; i < 720*100*4; i += 4 { // first 100 lines cyan 48 | //target.Pix[i + 0] = 0 49 | target.Pix[i+1] = 255 50 | target.Pix[i+2] = 255 51 | target.Pix[i+3] = 255 52 | } 53 | for i := 720 * 100 * 4; i < 720*200*4; i += 4 { // next 100 lines magenta 54 | target.Pix[i+0] = 255 55 | //target.Pix[i + 1] = 0 56 | target.Pix[i+2] = 255 57 | target.Pix[i+3] = 255 58 | } 59 | for i := 720 * 200 * 4; i < 720*300*4; i += 4 { // next 100 lines yellow 60 | target.Pix[i+0] = 255 61 | target.Pix[i+1] = 255 62 | //target.Pix[i + 2] = 0 63 | target.Pix[i+3] = 255 64 | } 65 | 66 | // draw first row of blend modes 67 | offX, offY := 180, 100 68 | x := offX / 2 69 | y := offY / 2 70 | renderer.SetColor(color.RGBA{0, 0, 0, Alpha}) 71 | renderer.Draw(target, "over", x, y) 72 | 73 | x += offX 74 | renderer.SetBlendMode(etxt.BlendCut) 75 | renderer.Draw(target, "cut", x, y) 76 | 77 | x += offX 78 | renderer.SetBlendMode(etxt.BlendReplace) 79 | renderer.Draw(target, "replace", x, y) 80 | 81 | x += offX 82 | renderer.SetBlendMode(etxt.BlendHue) 83 | renderer.Draw(target, "hue", x, y) 84 | 85 | // draw second row of blend modes 86 | y += offY 87 | x = offX / 2 88 | renderer.SetColor(color.RGBA{0, Alpha, Alpha, Alpha}) 89 | renderer.SetBlendMode(etxt.BlendSub) 90 | renderer.Draw(target, "subtract", x, y) 91 | 92 | x += offX 93 | renderer.SetBlendMode(etxt.BlendAdd) 94 | renderer.Draw(target, "add", x, y) 95 | 96 | x += offX 97 | renderer.SetBlendMode(etxt.BlendMultiply) 98 | renderer.Draw(target, "multiply", x, y) 99 | 100 | x += offX 101 | renderer.SetBlendMode(etxt.BlendOver) 102 | renderer.Draw(target, "over", x, y) 103 | 104 | // draw third row of blend modes 105 | y += offY 106 | x = offX / 2 107 | renderer.SetColor(color.RGBA{Alpha, 0, 0, Alpha}) 108 | renderer.SetBlendMode(etxt.BlendOver) 109 | renderer.Draw(target, "over", x, y) 110 | 111 | x += offX 112 | renderer.SetBlendMode(etxt.BlendHue) 113 | renderer.Draw(target, "hue", x, y) 114 | 115 | x += offX 116 | renderer.SetBlendMode(etxt.BlendSub) 117 | renderer.Draw(target, "subtract", x, y) 118 | 119 | x += offX 120 | renderer.SetBlendMode(etxt.BlendMultiply) 121 | renderer.Draw(target, "multiply", x, y) 122 | 123 | // store image as png 124 | filename, err := filepath.Abs("gtxt_blend_modes.png") 125 | if err != nil { 126 | log.Fatal(err) 127 | } 128 | fmt.Printf("Output image: %s\n", filename) 129 | file, err := os.Create(filename) 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | err = png.Encode(file, target) 134 | if err != nil { 135 | log.Fatal(err) 136 | } 137 | err = file.Close() 138 | if err != nil { 139 | log.Fatal(err) 140 | } 141 | fmt.Print("Program exited successfully.\n") 142 | } 143 | -------------------------------------------------------------------------------- /examples/gtxt/debug_glyph/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/debug_glyph 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/tinne26/etxt v0.0.9-alpha.8 7 | golang.org/x/image v0.9.0 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/gtxt/debug_glyph/main.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "log" 9 | "os" 10 | 11 | "github.com/tinne26/etxt" 12 | "github.com/tinne26/etxt/font" 13 | "github.com/tinne26/etxt/fract" 14 | "github.com/tinne26/etxt/mask" 15 | "golang.org/x/image/font/sfnt" 16 | ) 17 | 18 | // More than an example, this is something I use when debugging effects 19 | // and rasterizers to print mask glyph data directly and be able to 20 | // see it and analyze it. 21 | 22 | const GlyphToDebug = 'Q' 23 | 24 | func main() { 25 | // get font path 26 | if len(os.Args) != 2 { 27 | msg := "Usage: expects one argument with the path to the font to be used\n" 28 | fmt.Fprint(os.Stderr, msg) 29 | os.Exit(1) 30 | } 31 | 32 | // parse font 33 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | fmt.Printf("Font loaded: %s\n", fontName) 38 | 39 | // create and configure renderer 40 | // (notice that we don't set a cache, no need for a single glyph) 41 | const FontSize = 10 42 | renderer := etxt.NewRenderer() 43 | renderer.SetSize(FontSize) 44 | renderer.SetFont(sfntFont) 45 | renderer.SetAlign(etxt.Center) 46 | renderer.Fract().SetHorzQuantization(etxt.QtFull) 47 | renderer.Fract().SetVertQuantization(etxt.QtFull) 48 | 49 | // set a custom rasterizer that we want to debug 50 | fauxRast := mask.FauxRasterizer{} 51 | //fauxRast.SetSkewFactor(-0.3) 52 | fauxRast.SetExtraWidth(+0.0) 53 | renderer.Glyph().SetRasterizer(&fauxRast) 54 | 55 | // set the debugging draw function 56 | renderer.Glyph().SetDrawFunc( 57 | func(_ etxt.Target, glyphIndex sfnt.GlyphIndex, position fract.Point) { 58 | mask := renderer.Glyph().LoadMask(glyphIndex, position) 59 | bounds := mask.Bounds() 60 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 61 | fmt.Printf("%04d: [ ", y) 62 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 63 | _, _, _, a := mask.At(x, y).RGBA() 64 | fmt.Printf("%03d ", a>>8) 65 | } 66 | fmt.Printf("]\n") 67 | } 68 | }) 69 | 70 | // create a target image big enough. while it's not technically used on 71 | // our custom debugging function, etxt would panic on a nil image or may 72 | // optimize the draw away if the target is empty or non-intersecting 73 | target := image.NewRGBA(image.Rect(0, 0, FontSize*2, FontSize*2)) 74 | 75 | // draw the glyph to debug to hit the custom debug draw function 76 | renderer.Draw(target, string(GlyphToDebug), FontSize, FontSize) 77 | fmt.Print("Program exited successfully.\n") 78 | } 79 | -------------------------------------------------------------------------------- /examples/gtxt/direction_bidi/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/direction_bidi 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/tinne26/etxt v0.0.9-alpha.8 7 | golang.org/x/text v0.11.0 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/image v0.9.0 // indirect 17 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 18 | golang.org/x/sync v0.1.0 // indirect 19 | golang.org/x/sys v0.6.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/gtxt/each_font/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/each_font 2 | 3 | go 1.18 4 | 5 | require github.com/tinne26/etxt v0.0.9-alpha.8 6 | 7 | require ( 8 | github.com/ebitengine/purego v0.3.0 // indirect 9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 10 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 11 | github.com/jezek/xgb v1.1.0 // indirect 12 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 13 | golang.org/x/image v0.9.0 // indirect 14 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 15 | golang.org/x/sync v0.1.0 // indirect 16 | golang.org/x/sys v0.6.0 // indirect 17 | golang.org/x/text v0.11.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /examples/gtxt/each_font/main.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/png" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "sort" 14 | 15 | "github.com/tinne26/etxt" 16 | "github.com/tinne26/etxt/font" 17 | ) 18 | 19 | // Must be compiled with '-tags gtxt'. 20 | // This example expects a path to a font directory as the first 21 | // argument, reads the fonts in it and creates an image where each 22 | // font name is drawn with its own font. 23 | 24 | func main() { 25 | // get font directory path 26 | if len(os.Args) != 2 { 27 | msg := "Usage: expects one argument with the path to the font directory\n" 28 | fmt.Fprint(os.Stderr, msg) 29 | os.Exit(1) 30 | } 31 | 32 | // print given font directory 33 | fontDir, err := filepath.Abs(os.Args[1]) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | fmt.Printf("Reading font directory: %s\n", fontDir) 38 | 39 | // create font library, parsing fonts in the given directory 40 | fontLib := font.NewLibrary() 41 | added, skipped, err := fontLib.ParseAllFromPath(fontDir) 42 | if err != nil { 43 | log.Fatalf("Added %d fonts, skipped %d, failed with '%s'", added, skipped, err.Error()) 44 | } 45 | 46 | // create renderer (uncached in this example) 47 | renderer := etxt.NewRenderer() 48 | renderer.SetSize(24) 49 | renderer.SetAlign(etxt.Center) 50 | renderer.SetColor(color.RGBA{0, 0, 0, 255}) // black 51 | 52 | // determine how much space we will need to draw all 53 | // the fonts while also collecting their names 54 | width, height := 0, 0 55 | names := make([]string, 0, fontLib.Size()) 56 | err = fontLib.EachFont( 57 | func(fontName string, font *etxt.Font) error { 58 | renderer.SetFont(font) 59 | rect := renderer.Measure(fontName) 60 | height += rect.IntHeight() 61 | if rect.IntWidth() > width { 62 | width = rect.IntWidth() 63 | } 64 | names = append(names, fontName) 65 | return nil 66 | }) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | // add some padding to the computed width and height 72 | width += 16 73 | height += 12 74 | 75 | // create a target image and fill it with white 76 | outImage := image.NewRGBA(image.Rect(0, 0, width, height)) 77 | for i := 0; i < width*height*4; i++ { 78 | outImage.Pix[i] = 255 79 | } 80 | 81 | // draw each font name in order 82 | sort.Strings(names) 83 | y := 6 84 | for _, name := range names { 85 | renderer.SetFont(fontLib.GetFont(name)) // select the proper font 86 | h := renderer.Measure(name).IntHeight() 87 | y += h / 2 // advance half of the line height 88 | renderer.Draw(outImage, name, width/2, y) // draw font centered 89 | y += h - h/2 // advance remaining line height 90 | } 91 | 92 | // store image as png 93 | filename, err := filepath.Abs("gtxt_each_font.png") 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | fmt.Printf("Output image: %s\n", filename) 98 | file, err := os.Create(filename) 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | err = png.Encode(file, outImage) 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | err = file.Close() 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | fmt.Print("Program exited successfully.\n") 111 | } 112 | -------------------------------------------------------------------------------- /examples/gtxt/font_library/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/font_library 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/tinne26/etxt v0.0.9-alpha.8 7 | golang.org/x/image v0.9.0 8 | ) 9 | 10 | require golang.org/x/text v0.11.0 // indirect 11 | -------------------------------------------------------------------------------- /examples/gtxt/font_library/go.sum: -------------------------------------------------------------------------------- 1 | github.com/tinne26/etxt v0.0.9-alpha.8 h1:VFqVFGxnhqFxSAFgG9tWtVE/Pdj37aR9XADt0jv4AXo= 2 | github.com/tinne26/etxt v0.0.9-alpha.8/go.mod h1:Icbd4bDjrXag1oYIhB51CrkMYqRb7YMv0AsrOSfNKfU= 3 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 4 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 5 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 6 | golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g= 7 | golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= 8 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 9 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 10 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 11 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 12 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 13 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 14 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 16 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 17 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 18 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 24 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 25 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 26 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 27 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 28 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 29 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 30 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 31 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 32 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 33 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 34 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 35 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 36 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 37 | -------------------------------------------------------------------------------- /examples/gtxt/font_library/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/tinne26/etxt/font" 10 | "golang.org/x/image/font/sfnt" 11 | ) 12 | 13 | // Must be compiled with '-tags gtxt'. 14 | // This example expects a path to a font directory as the first 15 | // argument, reads the fonts in it and prints their names to the 16 | // terminal. 17 | 18 | func main() { 19 | // get font directory path 20 | if len(os.Args) != 2 { 21 | msg := "Usage: expects one argument with the path to the font directory\n" 22 | fmt.Fprint(os.Stderr, msg) 23 | os.Exit(1) 24 | } 25 | 26 | // print given font directory 27 | fontDir, err := filepath.Abs(os.Args[1]) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | fmt.Printf("Reading font directory: %s\n", fontDir) 32 | 33 | // create font library 34 | fontLib := font.NewLibrary() 35 | added, skipped, err := fontLib.ParseAllFromPath(fontDir) 36 | if err != nil { 37 | log.Fatalf("Added %d fonts, skipped %d, failed with '%s'", added, skipped, err.Error()) 38 | } 39 | fmt.Printf("Added %d fonts, skipped %d\n", added, skipped) 40 | 41 | // print, for each font parsed, its name, family and subfamily 42 | err = fontLib.EachFont( 43 | func(fontName string, sfntFont *sfnt.Font) error { 44 | family, err := font.GetFamily(sfntFont) 45 | if err != nil { 46 | log.Printf("(failed to load family for font %s: %s)", fontName, err.Error()) 47 | family = "unknown" 48 | } 49 | subfamily, err := font.GetSubfamily(sfntFont) 50 | if err != nil { 51 | log.Printf("(failed to load subfamily for font %s: %s)", fontName, err.Error()) 52 | subfamily = "unknown" 53 | } 54 | fmt.Printf("* %s (%s | %s)\n", fontName, family, subfamily) 55 | return nil 56 | }) 57 | if err != nil { 58 | log.Fatal("FontLibrary.EachFont error!: " + err.Error()) 59 | } 60 | fmt.Print("Program exited successfully.\n") 61 | } 62 | -------------------------------------------------------------------------------- /examples/gtxt/hello_world/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/hello_world 2 | 3 | go 1.18 4 | 5 | require github.com/tinne26/etxt v0.0.9-alpha.8 6 | 7 | require ( 8 | github.com/ebitengine/purego v0.3.0 // indirect 9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 10 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 11 | github.com/jezek/xgb v1.1.0 // indirect 12 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 13 | golang.org/x/image v0.9.0 // indirect 14 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 15 | golang.org/x/sync v0.1.0 // indirect 16 | golang.org/x/sys v0.6.0 // indirect 17 | golang.org/x/text v0.11.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /examples/gtxt/hello_world/main.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/png" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | 14 | "github.com/tinne26/etxt" 15 | "github.com/tinne26/etxt/font" 16 | ) 17 | 18 | // Must be compiled with '-tags gtxt' 19 | 20 | func main() { 21 | const OutImgWidth = 256 22 | const OutImgHeight = 64 23 | const TextSize = 32 24 | 25 | // get font path 26 | if len(os.Args) != 2 { 27 | msg := "Usage: expects one argument with the path to the font to be used\n" 28 | fmt.Fprint(os.Stderr, msg) 29 | os.Exit(1) 30 | } 31 | 32 | // parse font 33 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | fmt.Printf("Font loaded: %s\n", fontName) 38 | 39 | // create and configure renderer 40 | renderer := etxt.NewRenderer() 41 | renderer.Utils().SetCache8MiB() 42 | renderer.SetSize(TextSize) 43 | renderer.SetFont(sfntFont) 44 | renderer.SetAlign(etxt.Center) 45 | renderer.SetColor(color.RGBA{0, 0, 0, 255}) // black 46 | 47 | // create target image and fill it with white 48 | outImage := image.NewRGBA(image.Rect(0, 0, OutImgWidth, OutImgHeight)) 49 | for i := 0; i < OutImgWidth*OutImgHeight*4; i++ { 50 | outImage.Pix[i] = 255 51 | } 52 | 53 | // draw the text 54 | renderer.Draw(outImage, "Hello World!", OutImgWidth/2, OutImgHeight/2) 55 | 56 | // store image as png 57 | filename, err := filepath.Abs("gtxt_hello_world.png") 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | fmt.Printf("Output image: %s\n", filename) 62 | file, err := os.Create(filename) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | err = png.Encode(file, outImage) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | err = file.Close() 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | fmt.Print("Program exited successfully.\n") 75 | } 76 | -------------------------------------------------------------------------------- /examples/gtxt/measure/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/measure 2 | 3 | go 1.18 4 | 5 | require github.com/tinne26/etxt v0.0.9-alpha.8 6 | 7 | require ( 8 | github.com/ebitengine/purego v0.3.0 // indirect 9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 10 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 11 | github.com/jezek/xgb v1.1.0 // indirect 12 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 13 | golang.org/x/image v0.9.0 // indirect 14 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 15 | golang.org/x/sync v0.1.0 // indirect 16 | golang.org/x/sys v0.6.0 // indirect 17 | golang.org/x/text v0.11.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /examples/gtxt/measure/main.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/png" 10 | "log" 11 | "math/rand" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | "time" 16 | 17 | "github.com/tinne26/etxt" 18 | "github.com/tinne26/etxt/font" 19 | ) 20 | 21 | // Must be compiled with '-tags gtxt' 22 | 23 | func main() { 24 | // we want random sentences in order to find the text size dynamically, 25 | // so we start declaring different text fragments to combine later 26 | who := []string{ 27 | "my doggy", "methuselah", "the king", "the queen", "mr. skywalker", 28 | "your little pony", "my banana", "gopher", "jigglypuff", "evil jin", 29 | "the genius programmer", "your boyfriend", "the last samurai", 30 | "the cute robot", "your ancestor's ghost", 31 | } 32 | what := []string{ 33 | "climbs a tree", "writes a book", "stares at you", "commissions naughty art", 34 | "smiles", "takes scenery pics", "pays the bill", "practices times tables", 35 | "prays", "runs to take cover", "joins the chat", "downvotes your post", 36 | "discovers the moon", "poops", "questions your sense of humor", 37 | "re-opens the github issue", "talks to its clone", "arrives at the disco", 38 | "spies the neighbours", "solves the hardest equation", "discusses geopolitics", 39 | "gets mad at you for crossing the street", 40 | } 41 | how := []string{ 42 | "while dancing", "in style", "while undressing", "while getting high", 43 | "maniacally", "early in the morning", "right at the last moment", 44 | "as the world ends", "without much fuss", "bare-chested", "periodically", 45 | "every day", "with the gang", "without using the hands", 46 | "with the eyes closed", "bored as hell", "while remembering the past", 47 | } 48 | 49 | // get font path 50 | if len(os.Args) != 2 { 51 | msg := "Usage: expects one argument with the path to the font to be used\n" 52 | fmt.Fprint(os.Stderr, msg) 53 | os.Exit(1) 54 | } 55 | 56 | // parse font 57 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | fmt.Printf("Font loaded: %s\n", fontName) 62 | 63 | // create and configure renderer 64 | renderer := etxt.NewRenderer() 65 | renderer.Utils().SetCache8MiB() 66 | renderer.SetSize(16) 67 | renderer.SetFont(sfntFont) 68 | renderer.SetAlign(etxt.Center) 69 | renderer.SetColor(color.RGBA{0, 0, 0, 255}) // black 70 | 71 | // generate the random sentences 72 | rand.Seed(time.Now().UnixNano()) 73 | sentences := make([]string, 2+rand.Intn(6)) 74 | fmt.Printf("Generating %d sentences...\n", len(sentences)) 75 | for i := 0; i < len(sentences); i++ { 76 | sentence := who[rand.Intn(len(who))] + " " 77 | sentence += what[rand.Intn(len(what))] + " " 78 | sentence += how[rand.Intn(len(how))] 79 | sentences[i] = sentence 80 | } 81 | fullText := strings.Join(sentences, "\n") 82 | 83 | // determine how much space should it take to draw the 84 | // sentences, plus a bit of vertical and horizontal padding 85 | w, h := renderer.Measure(fullText).PadInts(8, 6).IntSize() 86 | 87 | // create target image and fill it with white 88 | outImage := image.NewRGBA(image.Rect(0, 0, w, h)) 89 | for i := 0; i < w*h*4; i++ { 90 | outImage.Pix[i] = 255 91 | } 92 | 93 | // draw the sentences 94 | renderer.Draw(outImage, fullText, w/2, h/2) 95 | 96 | // store image as png 97 | filename, err := filepath.Abs("gtxt_measure.png") 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | fmt.Printf("Output image: %s\n", filename) 102 | file, err := os.Create(filename) 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | err = png.Encode(file, outImage) 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | err = file.Close() 111 | if err != nil { 112 | log.Fatal(err) 113 | } 114 | fmt.Print("Program exited successfully.\n") 115 | } 116 | -------------------------------------------------------------------------------- /examples/gtxt/mirror/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/mirror 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/tinne26/etxt v0.0.9-alpha.8 7 | golang.org/x/image v0.9.0 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/gtxt/outline_cheap/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/outline_cheap 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/tinne26/etxt v0.0.9-alpha.8 7 | golang.org/x/image v0.9.0 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/gtxt/outline_cheap/main.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/png" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | 14 | "github.com/tinne26/etxt" 15 | "github.com/tinne26/etxt/font" 16 | "github.com/tinne26/etxt/fract" 17 | "golang.org/x/image/font/sfnt" 18 | ) 19 | 20 | // Must be compiled with '-tags gtxt' 21 | 22 | // This example draws text with a cheap and simple outline, made by 23 | // repeatedly drawing text slightly shifted to the left, right, up 24 | // and down. For higher quality outlines, see the OutlineRasterizer 25 | // instead and the gtxt/outline example. 26 | // 27 | // If you want a more advanced example on how to draw glyphs individually, 28 | // check gtxt/mirror instead. This example uses the renderer's DefaultDrawFunc, 29 | // so it doesn't get into the grittiest details. 30 | 31 | func main() { 32 | // get font path 33 | if len(os.Args) != 2 { 34 | msg := "Usage: expects one argument with the path to the font to be used\n" 35 | fmt.Fprint(os.Stderr, msg) 36 | os.Exit(1) 37 | } 38 | 39 | // parse font 40 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | fmt.Printf("Font loaded: %s\n", fontName) 45 | 46 | // create and configure renderer 47 | renderer := etxt.NewRenderer() 48 | renderer.Utils().SetCache8MiB() 49 | renderer.SetSize(36) 50 | renderer.SetFont(sfntFont) 51 | renderer.SetAlign(etxt.Center) 52 | renderer.SetColor(color.RGBA{0, 0, 0, 255}) // black 53 | 54 | // create target image and fill it with white 55 | w, h := 312, 64 56 | outImage := image.NewRGBA(image.Rect(0, 0, w, h)) 57 | for i := 0; i < w*h*4; i++ { 58 | outImage.Pix[i] = 255 59 | } 60 | 61 | // The key idea is to draw text repeatedly, slightly shifted 62 | // to the left, right, up, down... and finally draw the middle. 63 | // We could also do this with separate Draw() calls, but using 64 | // a custom function is a more general and tweakable approach. 65 | // 66 | // We will still draw the main text on a separate call afterwards 67 | // in order to avoid the background of a letter being overlayed 68 | // on top of a previously drawn letter (won't happen on most fonts 69 | // or sizes or glyph sequences, but it's possible in some cases). 70 | renderer.Glyph().SetDrawFunc( 71 | func(target etxt.Target, glyphIndex sfnt.GlyphIndex, origin fract.Point) { 72 | mask := renderer.Glyph().LoadMask(glyphIndex, origin) 73 | origin.X -= fract.One // shift left 74 | renderer.Glyph().DrawMask(target, mask, origin) 75 | origin.X += fract.One * 2 // shift right 76 | renderer.Glyph().DrawMask(target, mask, origin) 77 | origin.X -= fract.One // restore X to center 78 | origin.Y -= fract.One // shift up 79 | renderer.Glyph().DrawMask(target, mask, origin) 80 | origin.Y += fract.One * 2 // shift down 81 | renderer.Glyph().DrawMask(target, mask, origin) 82 | }) 83 | renderer.Draw(outImage, "Cheap Outline!", w/2, h/2) 84 | 85 | // finally draw the main text. you can try different colors, but 86 | // white makes it look like there's only outline, so that's cool. 87 | renderer.SetColor(color.RGBA{255, 255, 255, 255}) 88 | renderer.Glyph().SetDrawFunc(nil) // restore default draw function 89 | renderer.Draw(outImage, "Cheap Outline!", 156, 32) 90 | 91 | // store result as png 92 | filename, err := filepath.Abs("gtxt_outline_cheap.png") 93 | if err != nil { 94 | log.Fatal(err) 95 | } 96 | fmt.Printf("Output image: %s\n", filename) 97 | file, err := os.Create(filename) 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | err = png.Encode(file, outImage) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | err = file.Close() 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | fmt.Print("Program exited successfully.\n") 110 | } 111 | -------------------------------------------------------------------------------- /examples/gtxt/pattern/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/pattern 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/tinne26/etxt v0.0.9-alpha.8 7 | golang.org/x/image v0.9.0 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/gtxt/pattern/main.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/png" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | 14 | "github.com/tinne26/etxt" 15 | "github.com/tinne26/etxt/font" 16 | "github.com/tinne26/etxt/fract" 17 | "golang.org/x/image/font/sfnt" 18 | ) 19 | 20 | // Must be compiled with '-tags gtxt' 21 | 22 | // An example showcasing how to draw glyphs manually and applying a 23 | // specific pattern effect. The manual glyph drawing part is similar to 24 | // examples/gtxt/mirror. 25 | 26 | func main() { 27 | // get font path 28 | if len(os.Args) != 2 { 29 | msg := "Usage: expects one argument with the path to the font to be used\n" 30 | fmt.Fprint(os.Stderr, msg) 31 | os.Exit(1) 32 | } 33 | 34 | // parse font 35 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | fmt.Printf("Font loaded: %s\n", fontName) 40 | 41 | // create and configure renderer 42 | renderer := etxt.NewRenderer() 43 | renderer.Utils().SetCache8MiB() 44 | renderer.SetSize(64) 45 | renderer.SetFont(sfntFont) 46 | renderer.SetAlign(etxt.Center) 47 | renderer.SetColor(color.RGBA{255, 255, 255, 255}) // white 48 | 49 | // create target image and fill it with black 50 | w, h := 360, 64 51 | outImage := image.NewRGBA(image.Rect(0, 0, w, h)) 52 | for i := 3; i < w*h*4; i += 4 { 53 | outImage.Pix[i] = 255 54 | } 55 | 56 | // set custom draw func and draw 57 | renderer.Glyph().SetDrawFunc( 58 | func(target etxt.Target, glyphIndex sfnt.GlyphIndex, origin fract.Point) { 59 | mask := renderer.Glyph().LoadMask(glyphIndex, origin) 60 | drawAsPattern(outImage, mask, origin) 61 | }) 62 | renderer.Draw(outImage, "PATTERN", 180, 32) 63 | 64 | // store result as png 65 | filename, err := filepath.Abs("gtxt_pattern.png") 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | fmt.Printf("Output image: %s\n", filename) 70 | file, err := os.Create(filename) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | err = png.Encode(file, outImage) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | err = file.Close() 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | fmt.Print("Program exited successfully.\n") 83 | } 84 | 85 | func drawAsPattern(target *image.RGBA, mask etxt.GlyphMask, origin fract.Point) { 86 | // to draw a mask into a target, we need to displace it by the 87 | // current drawing position and be careful with clipping 88 | srcRect, destRect := getDrawBounds(mask.Rect, target.Bounds(), origin) 89 | if destRect.Empty() { 90 | return 91 | } // nothing to draw 92 | 93 | // we now have two rects that are the same size but identify 94 | // different regions of the mask and target images. we can use 95 | // them to read from one and draw on the other. yay. 96 | 97 | // we start by creating some helper variables to make iteration 98 | // through the rects more pleasant 99 | width := srcRect.Dx() 100 | height := srcRect.Dy() 101 | srcOffX := srcRect.Min.X 102 | srcOffY := srcRect.Min.Y 103 | destOffX := destRect.Min.X 104 | destOffY := destRect.Min.Y 105 | 106 | // iterate the rects and draw! 107 | for y := 0; y < height; y++ { 108 | for x := 0; x < width; x++ { 109 | // pattern filtering, edit and make your own! 110 | // e.g: 111 | // >> (x + y) % 2 == 0 112 | // >> x % 3 != 2 && y % 3 != 2 113 | // >> x % 3 == 2 || y % 3 == 2 114 | // >> x == y 115 | // >> (width - x) % 5 == y % 5 116 | // >> (y > height/2) && (x + y) % 2 == 0 117 | discard := x%2 != 0 || y%2 != 0 118 | if discard { 119 | continue 120 | } 121 | 122 | // get mask alpha level 123 | level := mask.AlphaAt(srcOffX+x, srcOffY+y).A 124 | if level == 0 { 125 | continue 126 | } // non-filled part of the glyph 127 | 128 | // now we finally can draw to the target 129 | target.SetRGBA(destOffX+x, destOffY+y, color.RGBA{255, 255, 255, 255}) 130 | } 131 | } 132 | } 133 | 134 | // same as in gtxt/mirror 135 | func getDrawBounds(srcRect, targetRect image.Rectangle, origin fract.Point) (image.Rectangle, image.Rectangle) { 136 | shift := image.Pt(origin.X.ToIntFloor(), origin.Y.ToIntFloor()) 137 | destRect := targetRect.Intersect(srcRect.Add(shift)) 138 | shift.X, shift.Y = -shift.X, -shift.Y 139 | return destRect.Add(shift), destRect 140 | } 141 | -------------------------------------------------------------------------------- /examples/gtxt/properties/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/properties 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/tinne26/etxt v0.0.9-alpha.8 7 | golang.org/x/image v0.9.0 8 | ) 9 | 10 | require golang.org/x/text v0.11.0 // indirect 11 | -------------------------------------------------------------------------------- /examples/gtxt/properties/go.sum: -------------------------------------------------------------------------------- 1 | github.com/tinne26/etxt v0.0.9-alpha.8 h1:VFqVFGxnhqFxSAFgG9tWtVE/Pdj37aR9XADt0jv4AXo= 2 | github.com/tinne26/etxt v0.0.9-alpha.8/go.mod h1:Icbd4bDjrXag1oYIhB51CrkMYqRb7YMv0AsrOSfNKfU= 3 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 4 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 5 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 6 | golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g= 7 | golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= 8 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 9 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 10 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 11 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 12 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 13 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 14 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 16 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 17 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 18 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 24 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 25 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 26 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 27 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 28 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 29 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 30 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 31 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 32 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 33 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 34 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 35 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 36 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 37 | -------------------------------------------------------------------------------- /examples/gtxt/quantization/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/quantization 2 | 3 | go 1.18 4 | 5 | require github.com/tinne26/etxt v0.0.9-alpha.8 6 | 7 | require ( 8 | github.com/ebitengine/purego v0.3.0 // indirect 9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 10 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 11 | github.com/jezek/xgb v1.1.0 // indirect 12 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 13 | golang.org/x/image v0.9.0 // indirect 14 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 15 | golang.org/x/sync v0.1.0 // indirect 16 | golang.org/x/sys v0.6.0 // indirect 17 | golang.org/x/text v0.11.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /examples/gtxt/quantization/main.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/png" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | 14 | "github.com/tinne26/etxt" 15 | "github.com/tinne26/etxt/font" 16 | ) 17 | 18 | // Must be compiled with '-tags gtxt' 19 | 20 | func main() { 21 | // get font path 22 | if len(os.Args) != 2 { 23 | msg := "Usage: expects one argument with the path to the font to be used\n" 24 | fmt.Fprint(os.Stderr, msg) 25 | os.Exit(1) 26 | } 27 | 28 | // parse font 29 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | fmt.Printf("Font loaded: %s\n", fontName) 34 | 35 | // create and configure renderer 36 | renderer := etxt.NewRenderer() 37 | renderer.Utils().SetCache8MiB() 38 | renderer.SetSize(20) 39 | renderer.SetFont(sfntFont) 40 | renderer.SetAlign(etxt.Top | etxt.Left) 41 | renderer.SetColor(color.RGBA{0, 0, 0, 255}) // black 42 | 43 | // create target image and fill it with white 44 | // (you could measure text and use its proper line height 45 | // to determine a more precise size for the output image) 46 | outImage := image.NewRGBA(image.Rect(0, 0, 640, 64)) 47 | for i := 0; i < 640*64*4; i++ { 48 | outImage.Pix[i] = 255 49 | } 50 | 51 | // draw quantized text 52 | TextSample := "Horizontally quantized vs unquantized text." 53 | renderer.Fract().SetHorzQuantization(etxt.QtFull) 54 | renderer.Draw(outImage, TextSample+" [quantized]", 8, 8) 55 | 56 | // disable horizontal quantization and draw again 57 | renderer.Fract().SetHorzQuantization(etxt.QtNone) 58 | renderer.Draw(outImage, TextSample+" [unquantized]", 8, 32) 59 | 60 | // store image as png 61 | filename, err := filepath.Abs("gtxt_quantization.png") 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | fmt.Printf("Output image: %s\n", filename) 66 | file, err := os.Create(filename) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | err = png.Encode(file, outImage) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | err = file.Close() 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | fmt.Print("Program exited successfully.\n") 79 | } 80 | -------------------------------------------------------------------------------- /examples/gtxt/rainbow/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/rainbow 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/tinne26/etxt v0.0.9-alpha.8 7 | golang.org/x/image v0.9.0 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 14 | github.com/jezek/xgb v1.1.0 // indirect 15 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 16 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 17 | golang.org/x/sync v0.1.0 // indirect 18 | golang.org/x/sys v0.6.0 // indirect 19 | golang.org/x/text v0.11.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /examples/gtxt/rainbow/main.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package main 4 | 5 | import "os" 6 | import "image" 7 | import "image/color" 8 | import "image/png" 9 | import "path/filepath" 10 | import "log" 11 | import "fmt" 12 | 13 | import "golang.org/x/image/font/sfnt" 14 | 15 | import "github.com/tinne26/etxt" 16 | import "github.com/tinne26/etxt/font" 17 | import "github.com/tinne26/etxt/fract" 18 | 19 | // Must be compiled with '-tags gtxt' 20 | 21 | // NOTE: see gtxt/mirror if you want a more advanced example of drawing each 22 | // glyph mask in a custom way. This one uses the default glyphs masks, 23 | // so all the heavy lifting is already done. 24 | 25 | const Text = "RAINBOW" // colors will repeat every 7 letters 26 | 27 | func main() { 28 | // get font path 29 | if len(os.Args) != 2 { 30 | msg := "Usage: expects one argument with the path to the font to be used\n" 31 | fmt.Fprint(os.Stderr, msg) 32 | os.Exit(1) 33 | } 34 | 35 | // parse font 36 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | fmt.Printf("Font loaded: %s\n", fontName) 41 | 42 | // create and configure renderer 43 | // (we omit the cache as we don't reuse any letters...) 44 | renderer := etxt.NewRenderer() 45 | renderer.SetSize(48) 46 | renderer.SetFont(sfntFont) 47 | renderer.SetAlign(etxt.Center) 48 | 49 | // create target image and fill it with a white to black gradient 50 | width := renderer.Measure(Text).IntWidth() + 24 51 | outImage := image.NewRGBA(image.Rect(0, 0, width, 64)) 52 | for y := 0; y < 64; y++ { 53 | lvl := 255 - uint8(y*8) 54 | if y >= 32 { 55 | lvl = 255 - lvl 56 | } 57 | for x := 0; x < width; x++ { 58 | outImage.Set(x, y, color.RGBA{lvl, lvl, lvl, 255}) 59 | } 60 | } 61 | 62 | // prepare rainbow colors 63 | colors := []color.RGBA{ 64 | {R: 255, G: 0, B: 0, A: 255}, // red 65 | {R: 255, G: 165, B: 0, A: 255}, // orange 66 | {R: 255, G: 255, B: 0, A: 255}, // yellow 67 | {R: 0, G: 255, B: 0, A: 255}, // green 68 | {R: 0, G: 0, B: 255, A: 255}, // blue 69 | {R: 75, G: 0, B: 130, A: 255}, // indigo 70 | {R: 238, G: 130, B: 238, A: 255}, // violet 71 | } 72 | 73 | // set custom rendering function 74 | colorIndex := 0 75 | renderer.Glyph().SetDrawFunc( 76 | func(target etxt.Target, glyphIndex sfnt.GlyphIndex, origin fract.Point) { 77 | renderer.SetColor(colors[colorIndex%7]) 78 | mask := renderer.Glyph().LoadMask(glyphIndex, origin) 79 | renderer.Glyph().DrawMask(target, mask, origin) 80 | colorIndex += 1 81 | }) 82 | 83 | // draw the text 84 | renderer.Draw(outImage, Text, width/2, 32) 85 | 86 | // store result as png 87 | filename, err := filepath.Abs("gtxt_rainbow.png") 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | fmt.Printf("Output image: %s\n", filename) 92 | file, err := os.Create(filename) 93 | if err != nil { 94 | log.Fatal(err) 95 | } 96 | err = png.Encode(file, outImage) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | err = file.Close() 101 | if err != nil { 102 | log.Fatal(err) 103 | } 104 | fmt.Print("Program exited successfully.\n") 105 | } 106 | -------------------------------------------------------------------------------- /examples/gtxt/sizer_expand/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt/examples/gtxt/sizer_expand 2 | 3 | go 1.18 4 | 5 | require github.com/tinne26/etxt v0.0.9-alpha.8 6 | 7 | require ( 8 | github.com/ebitengine/purego v0.3.0 // indirect 9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 10 | github.com/hajimehoshi/ebiten/v2 v2.5.0 // indirect 11 | github.com/jezek/xgb v1.1.0 // indirect 12 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 13 | golang.org/x/image v0.9.0 // indirect 14 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 15 | golang.org/x/sync v0.1.0 // indirect 16 | golang.org/x/sys v0.6.0 // indirect 17 | golang.org/x/text v0.11.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /examples/gtxt/sizer_expand/main.go: -------------------------------------------------------------------------------- 1 | //go:build gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/png" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | 14 | "github.com/tinne26/etxt" 15 | "github.com/tinne26/etxt/font" 16 | "github.com/tinne26/etxt/fract" 17 | "github.com/tinne26/etxt/sizer" 18 | ) 19 | 20 | // Must be compiled with '-tags gtxt' 21 | 22 | func main() { 23 | // get font path 24 | if len(os.Args) != 2 { 25 | msg := "Usage: expects one argument with the path to the font to be used\n" 26 | fmt.Fprint(os.Stderr, msg) 27 | os.Exit(1) 28 | } 29 | 30 | // parse font 31 | sfntFont, fontName, err := font.ParseFromPath(os.Args[1]) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | fmt.Printf("Font loaded: %s\n", fontName) 36 | 37 | // create and configure renderer 38 | renderer := etxt.NewRenderer() 39 | renderer.Utils().SetCache8MiB() 40 | renderer.SetSize(32) 41 | renderer.SetFont(sfntFont) 42 | renderer.SetAlign(etxt.Center) 43 | renderer.SetColor(color.RGBA{255, 255, 255, 255}) // white 44 | 45 | // create sizer and set it too 46 | var padSizer sizer.PaddedKernSizer 47 | renderer.SetSizer(&padSizer) 48 | 49 | // create target image and fill it with black 50 | outImage := image.NewRGBA(image.Rect(0, 0, 600, 230)) 51 | for i := 3; i < 600*230*4; i += 4 { 52 | outImage.Pix[i] = 255 53 | } 54 | 55 | // set target and draw each line expanding more and more 56 | for i := 0; i < 6; i++ { 57 | padSizer.SetPadding(fract.FromInt(i * 12)) 58 | renderer.Draw(outImage, "pyramid", 300, (i+1)*32) 59 | 60 | // note: if we didn't have the sizer available in the scope, 61 | // we would simply have to retrieve it first: 62 | // >> sizer := renderer.GetSizer().(*sizer.PaddedKernSizer) 63 | // >> sizer.SetPadding(fract.FromInt(i*12)) 64 | } 65 | 66 | // store image as png 67 | filename, err := filepath.Abs("gtxt_sizer_expand.png") 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | fmt.Printf("Output image: %s\n", filename) 72 | file, err := os.Create(filename) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | err = png.Encode(file, outImage) 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | err = file.Close() 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | fmt.Print("Program exited successfully.\n") 85 | } 86 | -------------------------------------------------------------------------------- /font/ack_setup_test.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | // This file contains a fake test ensuring that test assets are available, 4 | // setups a few important variables and provides some helper methods. 5 | 6 | import ( 7 | "embed" 8 | "fmt" 9 | "os" 10 | "sync" 11 | "testing" 12 | 13 | "golang.org/x/image/font/sfnt" 14 | ) 15 | 16 | //go:embed test/* 17 | var testfs embed.FS 18 | 19 | var testFontsDir string = "test" 20 | var testPathA string 21 | var testFontA *sfnt.Font 22 | var testFontB *sfnt.Font 23 | var assetsLoadMutex sync.Mutex 24 | var testAssetsLoaded bool 25 | 26 | func TestCompleteness(t *testing.T) { 27 | ensureTestAssetsLoaded() 28 | if len(testWarnings) > 0 { 29 | t.Fatalf("missing test assets\n%s", testWarnings) 30 | } 31 | } 32 | 33 | var testWarnings string 34 | 35 | func ensureTestAssetsLoaded() { 36 | // assets load access control 37 | assetsLoadMutex.Lock() 38 | defer assetsLoadMutex.Unlock() 39 | if testAssetsLoaded { 40 | return 41 | } 42 | testAssetsLoaded = true 43 | 44 | // parse embedded directory and check for useful fonts 45 | entries, err := testfs.ReadDir(testFontsDir) 46 | if err != nil { 47 | fmt.Printf("TESTS INIT: %s", err) 48 | os.Exit(1) 49 | } 50 | 51 | // manual loading to avoid depending on font library here 52 | var mainFontName string 53 | for _, entry := range entries { 54 | entryName := entry.Name() 55 | if !hasValidFontExtension(entryName) { 56 | continue 57 | } 58 | path := testFontsDir + "/" + entryName 59 | font, fontName, err := ParseFromFS(testfs, path) 60 | if err != nil { 61 | fmt.Printf("TESTS INIT: %s", err) 62 | os.Exit(1) 63 | } 64 | 65 | if testFontA == nil { 66 | testFontA = font 67 | testPathA = entryName 68 | mainFontName = fontName 69 | } else { 70 | if mainFontName == fontName { 71 | continue 72 | } 73 | testFontB = font 74 | break 75 | } 76 | } 77 | 78 | // test missing data warnings 79 | if testFontA == nil { 80 | testWarnings = "WARNING: Expected at least 2 .ttf fonts in " + testFontsDir + "/ (found 0)\n" + 81 | "WARNING: Most tests will be skipped\n" 82 | } else if testFontB == nil { 83 | testWarnings = "WARNING: Expected at least 2 .ttf fonts in " + testFontsDir + "/ (found 1)\n" + 84 | "WARNING: Some tests will be skipped\n" 85 | } 86 | } 87 | 88 | func doesNotPanic(function func()) (didNotPanic bool) { 89 | didNotPanic = true 90 | defer func() { didNotPanic = (recover() == nil) }() 91 | function() 92 | return 93 | } 94 | -------------------------------------------------------------------------------- /font/doc.go: -------------------------------------------------------------------------------- 1 | // The font subpackage contains helper methods to parse fonts and 2 | // obtain information from them (id, name, family, etc.), alongisde 3 | // a [Library] type to assist with their management if necessary. 4 | // 5 | // Using a [Library] is actually rather uncommon, as most small games 6 | // do not use more than a couple fonts and will generally be better off 7 | // avoiding the abstraction. 8 | package font 9 | -------------------------------------------------------------------------------- /font/library_test.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "testing" 7 | 8 | "golang.org/x/image/font/sfnt" 9 | ) 10 | 11 | func TestLibrary(t *testing.T) { 12 | lib := NewLibrary() 13 | if lib.Size() != 0 { 14 | t.Fatal("really?") 15 | } 16 | 17 | ensureTestAssetsLoaded() 18 | if testFontA == nil { 19 | t.SkipNow() 20 | } 21 | 22 | added, skipped, err := lib.ParseAllFromPath(testFontsDir + "/" + testPathA) 23 | if err != nil { 24 | t.Fatalf("unexpected error: %s", err.Error()) 25 | } 26 | if added != 1 { 27 | t.Fatal("expected 1 added font") 28 | } 29 | if skipped != 0 { 30 | t.Fatal("expected 0 skipped fonts") 31 | } 32 | 33 | font, name, err := ParseFromPath(testFontsDir + "/" + testPathA) 34 | if !lib.HasFont(name) { 35 | t.Fatalf("expected Library to include %s", name) 36 | } 37 | 38 | if lib.GetFont(name) == nil { 39 | t.Fatal("expected Library to allow access to the font") 40 | } 41 | 42 | if lib.GetFont("SurelyYouDontNameYourFontsLikeThis_") != nil { 43 | t.Fatal("well, well, well...") 44 | } 45 | 46 | lib.EachFont(func(fname string, _ *sfnt.Font) error { 47 | if fname != name { 48 | t.Fatalf("unexpected font %s", fname) 49 | } 50 | return nil 51 | }) 52 | if lib.RemoveFont("totally-not-fake-yay") { 53 | t.Fatal("unexpected remove") 54 | } 55 | if !lib.RemoveFont(name) { 56 | t.Fatal("unexpected remove failure") 57 | } 58 | lib.EachFont(func(fname string, _ *sfnt.Font) error { 59 | t.Fatalf("unexpected font %s", fname) 60 | return nil 61 | }) 62 | 63 | _, err = lib.ParseFromBytes([]byte{1, 2, 3, 4, 5, 6, 7, 8}) 64 | if err == nil { 65 | t.Fatal("expected error to be non-nil") 66 | } 67 | 68 | added, skipped, err = lib.ParseAllFromFS(testfs, testFontsDir) 69 | if err != nil { 70 | panic(err) 71 | } 72 | switch added { 73 | case 0: 74 | t.Fatal("expected at least 1 added font") 75 | case 1: 76 | if testFontB != nil { 77 | t.Fatal("expected at least 2 added fonts") 78 | } 79 | default: 80 | if testFontB == nil { 81 | t.Fatal("expected at most 1 added font, internal test init parsing mismatch") 82 | // ^ see init_test.go 83 | } 84 | } 85 | if skipped != 0 { 86 | t.Logf("WARNING: skipped %d fonts during embed parsing. Do you have dup fonts on %s?", skipped, testFontsDir) 87 | } 88 | 89 | fname, err := lib.ParseFromFS(testfs, testFontsDir+"/"+testPathA) 90 | if err != ErrAlreadyPresent { 91 | t.Fatalf("expected ErrAlreadyPresent, got '%s'", err.Error()) 92 | } 93 | if fname != name { 94 | t.Fatalf("expected '%s', got '%s'", name, fname) 95 | } 96 | 97 | if !lib.RemoveFont(name) { 98 | t.Fatalf("expected font %s to be present and possible to remove", name) 99 | } 100 | file, err := testfs.Open(testFontsDir + "/" + testPathA) 101 | if err != nil { 102 | file.Close() 103 | panic(err) 104 | } 105 | bytes, err := io.ReadAll(file) 106 | file.Close() 107 | if err != nil { 108 | panic(err) 109 | } 110 | fname, err = lib.ParseFromBytes(bytes) 111 | if err != nil { 112 | t.Fatalf("unexpected error '%s'", err) 113 | } 114 | if fname != name { 115 | t.Fatalf("unexpected name '%s' (expected '%s')", fname, name) 116 | } 117 | lib.RemoveFont(fname) 118 | 119 | lname, err := lib.AddFont(font) 120 | if err != nil { 121 | t.Fatalf("unexpected error on AddFont(): %s", err.Error()) 122 | } 123 | if lname != name { 124 | t.Fatalf("expected AddFont() name return to be '%s', but got '%s' instead", name, lname) 125 | } 126 | 127 | mustErr := true 128 | err = lib.EachFont(func(string, *sfnt.Font) error { 129 | if mustErr { 130 | mustErr = false 131 | return errors.New("manual error test") 132 | } 133 | t.Fatal("EachFont failed to stop on the first iteration") 134 | return nil 135 | }) 136 | if err == nil || err.Error() != "manual error test" { 137 | t.Fatalf("expected \"manual error test\" error, but got \"%s\" instead", err) 138 | } 139 | 140 | if doesNotPanic(func() { lib.AddFont(nil) }) { 141 | t.Fatalf("lib.AddFont(nil) should have panicked") 142 | } 143 | releaseSfntBuffer(sfntBuffer) // critical cleanup after the panic 144 | 145 | added, skipped, err = lib.ParseAllFromPath("unexistent/path/ffs/dont-tell-me") 146 | if added != 0 { 147 | t.Fatalf("added != 0") 148 | } 149 | if skipped != 0 { 150 | t.Fatalf("skipped != 0") 151 | } 152 | if err == nil { 153 | t.Fatalf("seriously?") 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /font/parse.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/fs" 7 | "os" 8 | 9 | "golang.org/x/image/font/sfnt" 10 | ) 11 | 12 | // Similar to [sfnt.Parse](), but also including the font name 13 | // in the returned values. The bytes must not be modified while 14 | // the font is in use. 15 | // 16 | // This is a low level function; you may prefer to use a 17 | // [Library] instead. 18 | // 19 | // [sfnt.Parse]: https://pkg.go.dev/golang.org/x/image/font/sfnt#Parse. 20 | func ParseFromBytes(fontBytes []byte) (*sfnt.Font, string, error) { 21 | newFont, err := sfnt.Parse(fontBytes) 22 | if err != nil { 23 | return nil, "", err 24 | } 25 | fontName, err := GetName(newFont) 26 | return newFont, fontName, err 27 | } 28 | 29 | // Attempts to parse a font located the given filepath and returns it 30 | // along its name and any possible error. Supported formats are .ttf 31 | // and .otf. 32 | // 33 | // This is a low level function; you may prefer to use a 34 | // [Library] instead. 35 | func ParseFromPath(path string) (*sfnt.Font, string, error) { 36 | // check font path validity 37 | ok := hasValidFontExtension(path) 38 | if !ok { 39 | return nil, "", errors.New("invalid font path '" + path + "'") 40 | } 41 | 42 | // open font file 43 | file, err := os.Open(path) 44 | if err != nil { 45 | return nil, "", err 46 | } 47 | return parseFontFileAndClose(file) 48 | } 49 | 50 | // Same as [ParseFromPath](), but for embedded filesystems. 51 | // 52 | // This is a low level function; you may prefer to use a 53 | // [Library] instead. 54 | func ParseFromFS(filesys fs.FS, path string) (*sfnt.Font, string, error) { 55 | // check font path validity 56 | ok := hasValidFontExtension(path) 57 | if !ok { 58 | return nil, "", errors.New("invalid font path '" + path + "'") 59 | } 60 | 61 | // open font file 62 | file, err := filesys.Open(path) 63 | if err != nil { 64 | return nil, "", err 65 | } 66 | return parseFontFileAndClose(file) 67 | } 68 | 69 | // ---- helpers ---- 70 | 71 | func parseFontFileAndClose(file io.ReadCloser) (*sfnt.Font, string, error) { 72 | fontBytes, err := io.ReadAll(file) 73 | if err != nil { 74 | _ = file.Close() 75 | return nil, "", err 76 | } 77 | err = file.Close() 78 | if err != nil { 79 | return nil, "", err 80 | } 81 | return ParseFromBytes(fontBytes) 82 | } 83 | 84 | // Whether font path ends in .ttf or .otf. 85 | func hasValidFontExtension(path string) bool { 86 | if len(path) < 4 { 87 | return false 88 | } 89 | if path[len(path)-1] != 'f' { 90 | return false 91 | } 92 | if path[len(path)-2] != 't' { 93 | return false 94 | } 95 | thrd := path[len(path)-3] 96 | if thrd != 't' && thrd != 'o' { 97 | return false 98 | } 99 | if path[len(path)-4] != '.' { 100 | return false 101 | } 102 | return true 103 | } 104 | -------------------------------------------------------------------------------- /font/parse_test.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/fs" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | type fakeFS struct{} 12 | 13 | func (fakeFS) Open(string) (fs.File, error) { 14 | return nil, errors.New("fakeFS") 15 | } 16 | 17 | type fakeReadCloser struct{ errOnRead bool } 18 | 19 | func (self fakeReadCloser) Read(p []byte) (n int, err error) { 20 | if self.errOnRead { 21 | return 0, errors.New("fakeRead") 22 | } 23 | return 0, io.EOF 24 | } 25 | func (self fakeReadCloser) Close() error { 26 | return errors.New("fakeClose") 27 | } 28 | 29 | // Testing the tricky error cases, fundamentally. The main 30 | // code paths are already implicitly tested through the library 31 | // functions and tests. 32 | func TestParse(t *testing.T) { 33 | var err error 34 | 35 | _, _, err = ParseFromBytes([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) 36 | if err == nil { 37 | t.Fatal("expected error") 38 | } 39 | 40 | _, _, err = ParseFromPath("path/with/no/extension") 41 | if err == nil || !strings.Contains(err.Error(), "invalid font path") { 42 | t.Fatal("expected error with 'invalid font path' in its contents") 43 | } 44 | 45 | _, _, err = ParseFromPath("fake/path/must/not/exist/yay.ttf") 46 | if !isPathNotExistErr(err) { 47 | t.Fatalf("expected error of path/file not existing, got '%s'", err) 48 | } 49 | 50 | fakefs := fakeFS{} 51 | _, _, err = ParseFromFS(fakefs, "path/with/no/extension") 52 | if err == nil || !strings.Contains(err.Error(), "invalid font path") { 53 | t.Fatal("expected error with 'invalid font path' in its contents") 54 | } 55 | _, _, err = ParseFromFS(fakefs, "cool.ttf") 56 | if err == nil || err.Error() != "fakeFS" { 57 | t.Fatalf("expected \"fakeFS\" error, but got '%s'", err) 58 | } 59 | 60 | if hasValidFontExtension("") { 61 | t.Fatalf("not a valid font extension") 62 | } 63 | if hasValidFontExtension(".") { 64 | t.Fatalf("not a valid font extension") 65 | } 66 | if hasValidFontExtension(".t") { 67 | t.Fatalf("not a valid font extension") 68 | } 69 | if hasValidFontExtension(".tt") { 70 | t.Fatalf("not a valid font extension") 71 | } 72 | if hasValidFontExtension(".ttx") { 73 | t.Fatalf("not a valid font extension") 74 | } 75 | if hasValidFontExtension("ttf") { 76 | t.Fatalf("not a valid font extension") 77 | } 78 | if hasValidFontExtension("otf") { 79 | t.Fatalf("not a valid font extension") 80 | } 81 | if hasValidFontExtension(".tgf") { 82 | t.Fatalf("not a valid font extension") 83 | } 84 | if hasValidFontExtension(".gtf") { 85 | t.Fatalf("not a valid font extension") 86 | } 87 | if hasValidFontExtension(".mp4a") { 88 | t.Fatalf("not a valid font extension") 89 | } 90 | if hasValidFontExtension(".xttf") { 91 | t.Fatalf("not a valid font extension") 92 | } 93 | if !hasValidFontExtension(".ttf") { 94 | t.Fatalf(".ttf must be a valid font extension") 95 | } 96 | if !hasValidFontExtension(".otf") { 97 | t.Fatalf(".ttf must be a valid font extension") 98 | } 99 | 100 | rc := fakeReadCloser{errOnRead: true} 101 | _, _, err = parseFontFileAndClose(rc) 102 | if err == nil || err.Error() != "fakeRead" { 103 | t.Fatalf("expected err == \"fakeRead\", but got '%s'", err) 104 | } 105 | rc.errOnRead = false 106 | _, _, err = parseFontFileAndClose(rc) 107 | if err == nil || err.Error() != "fakeClose" { 108 | t.Fatalf("expected err == \"fakeClose\", but got '%s'", err) 109 | } 110 | } 111 | 112 | func isPathNotExistErr(err error) bool { 113 | if err == nil { 114 | return false 115 | } 116 | if strings.Contains(err.Error(), "cannot find") { 117 | return true 118 | } // windows 119 | if strings.Contains(err.Error(), "no such file or directory") { 120 | return true 121 | } // linux 122 | // ... (more OSes may be relevant, but I only wrote the ones I have test scripts for) 123 | return false 124 | } 125 | -------------------------------------------------------------------------------- /font/properties.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | "errors" 5 | "sync/atomic" 6 | 7 | "golang.org/x/image/font/sfnt" 8 | ) 9 | 10 | // A common error returned by font property getter functions. 11 | var ErrNotFound = errors.New("font property not found or empty") 12 | 13 | // We allocate one sfnt.Buffer so it can be used in FontProperty() calls. 14 | // These buffers can't be used concurrently though, so sfntBuffer will only 15 | // be used if no one else is using it at the moment. We don't bother creating 16 | // a pool because that would be waaay overkill, and this simple trick already 17 | // makes calls to FontProperty() fast in 99% use-cases for a reasonably small 18 | // price. Only limitation? Panic recovers can leave it locked as being used. 19 | var sfntBuffer *sfnt.Buffer 20 | var usingSfntBuffer uint32 = 0 21 | 22 | func getSfntBuffer() *sfnt.Buffer { 23 | if !atomic.CompareAndSwapUint32(&usingSfntBuffer, 0, 1) { 24 | return nil 25 | } 26 | if sfntBuffer == nil { 27 | sfntBuffer = &sfnt.Buffer{} 28 | } 29 | return sfntBuffer 30 | } 31 | 32 | func releaseSfntBuffer(buffer *sfnt.Buffer) { 33 | if buffer != nil { 34 | atomic.StoreUint32(&usingSfntBuffer, 0) 35 | } 36 | } 37 | 38 | // Returns the requested font property for the given font. 39 | // The returned property string might be empty even when error is nil. 40 | // If the property is missing, [ErrNotFound] will be returned. 41 | func GetProperty(font *sfnt.Font, property sfnt.NameID) (string, error) { 42 | buffer := getSfntBuffer() 43 | str, err := font.Name(buffer, property) 44 | releaseSfntBuffer(buffer) 45 | if err == sfnt.ErrNotFound { 46 | return "", ErrNotFound 47 | } 48 | return str, err 49 | } 50 | 51 | // Returns the family name of the given font. If the information is 52 | // missing, [ErrNotFound] will be returned. Other errors are also 53 | // possible (e.g., if the font naming table is invalid). 54 | func GetFamily(font *sfnt.Font) (string, error) { 55 | return GetProperty(font, sfnt.NameIDFamily) 56 | } 57 | 58 | // Returns the subfamily name of the given font. If the information 59 | // is missing, [ErrNotFound] will be returned. Other errors are also 60 | // possible (e.g., if the font naming table is invalid). 61 | // 62 | // In most cases, the subfamily value will be one of: 63 | // - "Regular", "Italic", "Bold", "Bold Italic" 64 | func GetSubfamily(font *sfnt.Font) (string, error) { 65 | return GetProperty(font, sfnt.NameIDSubfamily) 66 | } 67 | 68 | // Returns the name of the given font. If the information is missing, 69 | // [ErrNotFound] will be returned. Other errors are also possible (e.g., 70 | // if the font naming table is invalid). 71 | func GetName(font *sfnt.Font) (string, error) { 72 | return GetProperty(font, sfnt.NameIDFull) 73 | } 74 | 75 | // Returns the identifier of the given font. If the information is missing, 76 | // [ErrNotFound] will be returned. Other errors are also possible (e.g., 77 | // if the font naming table is invalid). 78 | func GetIdentifier(font *sfnt.Font) (string, error) { 79 | return GetProperty(font, sfnt.NameIDUniqueIdentifier) 80 | } 81 | 82 | // Returns the runes in the given text that can't be represented by the 83 | // font. If runes are repeated in the input text, the returned slice may 84 | // contain them multiple times too. 85 | // 86 | // If you load fonts dynamically, it is good practice to use this function 87 | // or the simpler [IsMissingRunes]() to make sure that the fonts include all 88 | // the glyphs that you require. 89 | func GetMissingRunes(font *sfnt.Font, text string) ([]rune, error) { 90 | buffer := getSfntBuffer() 91 | defer releaseSfntBuffer(buffer) 92 | 93 | missing := make([]rune, 0) 94 | for _, codePoint := range text { 95 | index, err := font.GlyphIndex(buffer, codePoint) 96 | if err != nil { 97 | return missing, err 98 | } 99 | if index == 0 { 100 | missing = append(missing, codePoint) 101 | } 102 | } 103 | return missing, nil 104 | } 105 | 106 | // Returns true iff the font is missing any of the glyphs required to 107 | // render the given text. More casual version of [GetMissingRunes](). 108 | func IsMissingRunes(font *sfnt.Font, text string) (bool, error) { 109 | buffer := getSfntBuffer() 110 | defer releaseSfntBuffer(buffer) 111 | 112 | for _, codePoint := range text { 113 | index, err := font.GlyphIndex(buffer, codePoint) 114 | if err != nil { 115 | return false, err 116 | } 117 | if index == 0 { 118 | return true, nil 119 | } 120 | } 121 | return false, nil 122 | } 123 | -------------------------------------------------------------------------------- /font/properties_test.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestGetProperties(t *testing.T) { 9 | ensureTestAssetsLoaded() 10 | if testFontA == nil { 11 | t.SkipNow() 12 | } 13 | var value string 14 | var err error 15 | 16 | // ensure state sanity 17 | buffer := getSfntBuffer() 18 | if buffer == nil { 19 | panic("unexpected nil") 20 | } 21 | releaseSfntBuffer(buffer) 22 | 23 | // test unexsitent property 24 | value, err = GetProperty(testFontA, 999) 25 | if err != ErrNotFound { 26 | t.Fatalf("GetProperty(testFontA, 999) error: %s", err) 27 | } 28 | if value != "" { 29 | t.Fatalf("GetProperty(nil, 999) value = \"%s\"", value) 30 | } 31 | 32 | name, err := GetName(testFontA) 33 | if err != nil { 34 | panic(err) 35 | } 36 | ident, err := GetIdentifier(testFontA) 37 | if err != nil { 38 | panic(err) 39 | } 40 | family, err := GetFamily(testFontA) 41 | if err != nil { 42 | panic(err) 43 | } 44 | if !strings.Contains(name, family) && !strings.Contains(ident, family) { 45 | holyBible := "expected font name (%s) or identifier (%s) to contain " 46 | holyBible += "font family (%s). Maybe you are using a weird font?" 47 | t.Fatalf(holyBible, name, ident, family) 48 | } 49 | subfamily, err := GetSubfamily(testFontA) 50 | if err != nil { 51 | panic(err) 52 | } 53 | if subfamily != "Regular" && subfamily != "Italic" && 54 | subfamily != "Bold" && subfamily != "Bold Italic" { 55 | t.Fatalf("expected a... normal font subfamily, but got %s", subfamily) 56 | } 57 | 58 | if sfntBuffer == nil { 59 | panic("unexpected nil") 60 | } 61 | buffer = getSfntBuffer() 62 | if buffer == nil { 63 | panic("failed to get shared sfntBuffer") 64 | } 65 | ident2, err := GetIdentifier(testFontA) 66 | if err != nil { 67 | panic(err) 68 | } 69 | if ident2 != ident { 70 | t.Fatalf("ident2 != ident") 71 | } 72 | releaseSfntBuffer(buffer) 73 | } 74 | 75 | func TestGetMissingRunes(t *testing.T) { 76 | ensureTestAssetsLoaded() 77 | if testFontA == nil { 78 | t.SkipNow() 79 | } 80 | var missing []rune 81 | var err error 82 | 83 | missing, err = GetMissingRunes(testFontA, " ") 84 | if err != nil { 85 | t.Fatalf("unexpected error: %s", err) 86 | } 87 | if len(missing) != 0 { 88 | t.Fatalf("unexpected missing runes: %v", missing) 89 | } 90 | missing, err = GetMissingRunes(testFontA, "\uF800") 91 | if err != nil { 92 | t.Fatalf("unexpected error: %s", err) 93 | } 94 | if len(missing) != 1 { 95 | t.Fatal("unexpected rune \"\\uF800\" not missing") 96 | } 97 | 98 | missing, err = GetMissingRunes(testFontA, " \uF800 \uF800\uF800 ") 99 | if err != nil { 100 | t.Fatalf("unexpected error: %s", err) 101 | } 102 | if len(missing) != 3 { 103 | t.Fatalf("unexpected len(missing) == %d", len(missing)) 104 | } 105 | 106 | // NOTE: missing font.GlyphIndex(buffer, codePoint) test, but I can't 107 | // come up with an easy way to test that (without creating a dummy 108 | // font intended to trigger the error) 109 | } 110 | -------------------------------------------------------------------------------- /font/test/.blank: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinne26/etxt/2cb49951efa0d330dd41d61a118b7cd280071722/font/test/.blank -------------------------------------------------------------------------------- /fract/constants.go: -------------------------------------------------------------------------------- 1 | package fract 2 | 3 | // Miscellaneous constants related to [Unit]. 4 | const ( 5 | MaxUnit Unit = +0x7FFFFFFF 6 | MinUnit Unit = -0x7FFFFFFF - 1 7 | One Unit = 64 // fract.One.ToInt() == 1 8 | MaxInt int = +33554431 // max representable int 9 | MinInt int = -33554432 // min representable int 10 | MaxFloat64 float64 = +33554431.984375 // max representable float 11 | MinFloat64 float64 = -33554432 // min representable float 12 | Delta float64 = 0.015625 // float equivalent of Unit(1) => 1.0/64.0 13 | HalfDelta float64 = 0.0078125 // 1.0/128.0 (used for rounding) 14 | ) 15 | -------------------------------------------------------------------------------- /fract/convert.go: -------------------------------------------------------------------------------- 1 | package fract 2 | 3 | // Fast conversion from int to [Unit]. If the int value is not 4 | // representable with a [Unit], the result is undefined. If you 5 | // want to account for overflows, check [MinInt] <= value <= [MaxInt]. 6 | func FromInt(value int) Unit { return Unit(value << 6) } 7 | 8 | // Converts a float64 to the closest [Unit], rounding away from 9 | // zero in case of ties. Doesn't account for NaNs, infinites 10 | // nor overflows. See also [FromFloat64Up]() and [FromFloat64Down](). 11 | func FromFloat64(value float64) Unit { 12 | if value >= 0 { 13 | return FromFloat64Up(value) 14 | } 15 | return FromFloat64Down(value) 16 | } 17 | 18 | // Converts a float64 to the closest [Unit], rounding up in case 19 | // of ties. Doesn't account for NaNs, infinites nor overflows. 20 | func FromFloat64Up(value float64) Unit { 21 | unitApprox := Unit(value * 64) 22 | fp64Approx := unitApprox.ToFloat64() 23 | if fp64Approx == value { 24 | return unitApprox 25 | } 26 | if fp64Approx > value { 27 | unitApprox -= 1 28 | fp64Approx = unitApprox.ToFloat64() 29 | } 30 | 31 | if value-fp64Approx >= HalfDelta { 32 | unitApprox += 1 33 | } 34 | return unitApprox 35 | } 36 | 37 | // Converts a float64 to the closest [Unit], rounding down in case 38 | // of ties. Doesn't account for NaNs, infinites nor overflows. 39 | func FromFloat64Down(value float64) Unit { 40 | unitApprox := Unit(value * 64) 41 | fp64Approx := unitApprox.ToFloat64() 42 | if fp64Approx == value { 43 | return unitApprox 44 | } 45 | if fp64Approx > value { 46 | unitApprox -= 1 47 | fp64Approx = unitApprox.ToFloat64() 48 | } 49 | 50 | if value-fp64Approx > HalfDelta { 51 | unitApprox += 1 52 | } 53 | return unitApprox 54 | } 55 | -------------------------------------------------------------------------------- /fract/convert_test.go: -------------------------------------------------------------------------------- 1 | package fract 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestFromFloat64(t *testing.T) { 9 | nzr := math.Copysign(0, -1) // negative zero 10 | tests := []struct { 11 | in float64 12 | low Unit 13 | high Unit 14 | }{ 15 | {in: 0.0, low: 0, high: 0}, 16 | {in: nzr, low: 0, high: 0}, 17 | {in: 1.0, low: 64, high: 64}, 18 | {in: -1.0, low: -64, high: -64}, 19 | {in: 0.5, low: 32, high: 32}, 20 | {in: 3.14, low: 201, high: 201}, 21 | {in: -3.14, low: -201, high: -201}, 22 | {in: 8.33, low: 533, high: 533}, 23 | {in: 8.3359375, low: 533, high: 534}, 24 | {in: 8.3359374, low: 533, high: 533}, 25 | {in: 8.3359376, low: 534, high: 534}, 26 | {in: -8.3359375, low: -534, high: -533}, 27 | {in: -8.3359374, low: -533, high: -533}, 28 | {in: -8.3359376, low: -534, high: -534}, 29 | {in: MaxFloat64, low: MaxUnit, high: MaxUnit}, 30 | {in: MinFloat64, low: MinUnit, high: MinUnit}, 31 | } 32 | 33 | for i, test := range tests { 34 | low, high := FromFloat64Down(test.in), FromFloat64Up(test.in) 35 | if low != test.low || high != test.high { 36 | str := "test #%d: in (%f), expected outs %d (%f) and %d (%f), but got %d (%f) and %d (%f)" 37 | t.Fatalf(str, i, test.in, test.low, test.low.ToFloat64(), test.high, test.high.ToFloat64(), low, low.ToFloat64(), high, high.ToFloat64()) 38 | } 39 | away := FromFloat64(test.in) 40 | if test.in >= 0 { 41 | if away != test.high { 42 | str := "test #%d: expected FromFloat64(%f) to return %d (%f), but got %d (%f) instead" 43 | t.Fatalf(str, i, test.in, test.high, test.high.ToFloat64(), away, away.ToFloat64()) 44 | } 45 | } else { 46 | if away != test.low { 47 | str := "test #%d: expected FromFloat64(%f) to return %d (%f), but got %d (%f) instead" 48 | t.Fatalf(str, i, test.in, test.low, test.low.ToFloat64(), away, away.ToFloat64()) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /fract/doc.go: -------------------------------------------------------------------------------- 1 | // CPU-based vectorial text rendering has always been a performance 2 | // sensitive task, with glyph rasterization being one of the most 3 | // critical steps in the process. One of the techniques that can be 4 | // used to improve the rasterization speed is using [fixed point] 5 | // arithmetic instead of floating point arithmetic — and that's what 6 | // brings us to this subpackage. 7 | // 8 | // The fract subpackage defines a [Unit] type representing a 26.6 9 | // fixed point value and provides numerous methods to perform fixed 10 | // point operations. Additionally, the subpackage also defines the 11 | // [Point] and [Rect] helper types. 12 | // 13 | // Other font related Golang packages tend to depend on 14 | // [golang.org/x/image/math/fixed] instead, but this subpackage 15 | // offers more methods, more accurate algorithms and is designed 16 | // to integrate directly with etxt. 17 | // 18 | // [fixed point]: https://github.com/tinne26/etxt/blob/v0.0.9/docs/fixed-26-6.md 19 | package fract 20 | -------------------------------------------------------------------------------- /fract/ebiten_yes.go: -------------------------------------------------------------------------------- 1 | //go:build !gtxt 2 | 3 | package fract 4 | 5 | import "github.com/hajimehoshi/ebiten/v2" 6 | 7 | // Ebitengine-related additional utility methods. 8 | 9 | // Utility function to retrieve the subimage corresponding 10 | // to the rect area. 11 | func (self Rect) Clip(image *ebiten.Image) *ebiten.Image { 12 | return image.SubImage(self.ImageRect()).(*ebiten.Image) 13 | } 14 | -------------------------------------------------------------------------------- /fract/point.go: -------------------------------------------------------------------------------- 1 | package fract 2 | 3 | import ( 4 | "image" 5 | "strconv" 6 | ) 7 | 8 | // A pair of [Unit] coordinates. Commonly used during rendering 9 | // processes to keep track of the pen position within the rendering 10 | // target. 11 | type Point struct { 12 | X Unit 13 | Y Unit 14 | } 15 | 16 | // Creates a point from a pair of units. 17 | func UnitsToPoint(x, y Unit) Point { 18 | return Point{X: x, Y: y} 19 | } 20 | 21 | // Creates a point from a pair of ints. 22 | func IntsToPoint(x, y int) Point { 23 | return Point{X: FromInt(x), Y: FromInt(y)} 24 | } 25 | 26 | // Converts the point coordinates to ints and returns 27 | // them as an [image.Point] stdlib value. The conversion 28 | // will round the units if necessary, which could be 29 | // problematic in some contexts. 30 | func (self Point) ImagePoint() image.Point { 31 | x, y := self.ToInts() 32 | return image.Pt(x, y) 33 | } 34 | 35 | // Returns the point coordinates as a pair of ints. 36 | // The conversion will round the units if necessary, which 37 | // could be problematic in some contexts. 38 | func (self Point) ToInts() (int, int) { 39 | return self.X.ToInt(), self.Y.ToInt() 40 | } 41 | 42 | // Returns the point coordinates as a pair of float64s. 43 | func (self Point) ToFloat64s() (x, y float64) { 44 | return self.X.ToFloat64(), self.Y.ToFloat64() 45 | } 46 | 47 | // Returns the point coordinates as a pair of float32s. 48 | func (self Point) ToFloat32s() (x, y float32) { 49 | return self.X.ToFloat32(), self.Y.ToFloat32() 50 | } 51 | 52 | // Returns the result of adding the given pair of units to 53 | // the current point coordinates. 54 | func (self Point) AddUnits(x, y Unit) Point { 55 | self.X += x 56 | self.Y += y 57 | return self 58 | } 59 | 60 | // Returns the result of adding the two points. 61 | func (self Point) AddPoint(point Point) Point { 62 | self.X += point.X 63 | self.Y += point.Y 64 | return self 65 | } 66 | 67 | // Returns whether the current point is inside the given [Rect]. 68 | func (self Point) In(rect Rect) bool { 69 | return self.X >= rect.Min.X && self.X < rect.Max.X && self.Y >= rect.Min.Y && self.Y < rect.Max.Y 70 | } 71 | 72 | // Returns a textual representation of the point (e.g.: "(2.5, -4)"). 73 | func (self Point) String() string { 74 | x := strconv.FormatFloat(self.X.ToFloat64(), 'f', -1, 64) 75 | y := strconv.FormatFloat(self.Y.ToFloat64(), 'f', -1, 64) 76 | return "(" + x + ", " + y + ")" 77 | } 78 | -------------------------------------------------------------------------------- /fract/point_test.go: -------------------------------------------------------------------------------- 1 | package fract 2 | 3 | import "testing" 4 | 5 | func TestPoint(t *testing.T) { 6 | point := UnitsToPoint(64, 31) 7 | imgPt := point.ImagePoint() 8 | if imgPt.X != 1 || imgPt.Y != 0 { 9 | t.Fatalf("expected (X: 1, Y: 0), got %v", imgPt) 10 | } 11 | point = point.AddUnits(0, 1) 12 | imgPt = point.ImagePoint() 13 | if imgPt.X != 1 || imgPt.Y != 1 { 14 | t.Fatalf("expected (X: 1, Y: 1), got %v", imgPt) 15 | } 16 | 17 | if point.String() != "(1, 0.5)" { 18 | t.Fatalf("expected (1, 0.5), got %s", point.String()) 19 | } 20 | point = point.AddPoint(point) 21 | if point.String() != "(2, 1)" { 22 | t.Fatalf("expected (2, 1), got %s", point.String()) 23 | } 24 | x, y := point.ToFloat64s() 25 | if x != 2 || y != 1 { 26 | t.Fatalf("expected (2, 1), got (%f, %f)", x, y) 27 | } 28 | x32, y32 := point.ToFloat32s() 29 | if x32 != 2 || y32 != 1 { 30 | t.Fatalf("expected (2, 1), got (%f, %f)", x32, y32) 31 | } 32 | 33 | if !point.In(UnitsToRect(128, 64, 129, 65)) { 34 | t.Fatalf("point.In(rect) #1: expected inside, got outside") 35 | } 36 | if point.In(UnitsToRect(128, 64, 129, 64)) { 37 | t.Fatalf("point.In(rect) #2: expected outside, got inside") 38 | } 39 | if point.In(UnitsToRect(128, 64, 128, 65)) { 40 | t.Fatalf("point.In(rect) #3: expected outside, got inside") 41 | } 42 | if point.In(UnitsToRect(0, 0, 64, 64)) { 43 | t.Fatalf("point.In(rect) #4: expected outside, got inside") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /fract/rect_test.go: -------------------------------------------------------------------------------- 1 | package fract 2 | 3 | import "testing" 4 | 5 | func TestRectTrivial(t *testing.T) { 6 | rect := UnitsToRect(0, 0, 64, 64) 7 | if rect.Width() != 64 { 8 | t.Fatal("incorrect width") 9 | } 10 | if rect.Height() != 64 { 11 | t.Fatal("incorrect width") 12 | } 13 | 14 | imgRect := rect.ImageRect() 15 | if imgRect.Min.X != 0 || imgRect.Min.Y != 0 || imgRect.Max.X != 1 || imgRect.Max.Y != 1 { 16 | t.Fatal("incorrect ImageRect() conversion") 17 | } 18 | 19 | if imgRect.Dx() != rect.IntWidth() || imgRect.Dy() != rect.IntHeight() { 20 | t.Fatal("discordance between int width/heigth and ImageRect() width/height") 21 | } 22 | 23 | if rect.Empty() { 24 | t.Fatal("should not be empty") 25 | } 26 | 27 | rect = rect.AddUnits(32, 32) 28 | minx, miny, maxx, maxy := rect.ToFloat64s() 29 | if minx != 0.5 || miny != 0.5 || maxx != 1.5 || maxy != 1.5 { 30 | t.Fatal("invalid ToFloat64s() conversion") 31 | } 32 | 33 | imgRect = rect.ImageRect() 34 | if imgRect.Dx() != 2 || imgRect.Dy() != 2 { 35 | t.Fatal("expected width and height to be 2") 36 | } 37 | if rect.IntWidth() != 1 || rect.IntHeight() != 1 { 38 | t.Fatal("expected precise width and height here") 39 | } 40 | ox, oy := rect.IntOrigin() 41 | if ox != 0 || oy != 0 { 42 | t.Fatal("expected (0, 0) origin") 43 | } 44 | } 45 | 46 | func TestRectPoints(t *testing.T) { 47 | pt1, pt2 := UnitsToPoint(33, 33), UnitsToPoint(36, 36) 48 | rect := PointsToRect(pt2, pt1) 49 | if !rect.Empty() { 50 | t.Fatal("expected empty rect") 51 | } 52 | rect = PointsToRect(pt1, pt2) 53 | if rect.String() != pt1.String()+"-"+pt2.String() { 54 | t.Fatalf("unexpected rect.String() value '%s'", rect.String()) 55 | } 56 | 57 | if !rect.Contains(pt1) { 58 | t.Fatal("expected pt1 to be contained") 59 | } 60 | if rect.Contains(pt2) { 61 | t.Fatal("expected pt2 to NOT be contained") 62 | } 63 | 64 | rect = rect.AddPoint(pt1) 65 | if !rect.Contains(pt1.AddPoint(pt1)) { 66 | t.Fatal("expected pt1 + pt1 to be contained") 67 | } 68 | if rect.Contains(pt1.AddPoint(pt2)) { 69 | t.Fatal("expected pt1 + pt2 to NOT be contained") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tinne26/etxt 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.0 7 | golang.org/x/image v0.9.0 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/purego v0.3.0 // indirect 12 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 13 | github.com/jezek/xgb v1.1.0 // indirect 14 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 15 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 16 | golang.org/x/sync v0.1.0 // indirect 17 | golang.org/x/sys v0.6.0 // indirect 18 | golang.org/x/text v0.11.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /mask/buffer.go: -------------------------------------------------------------------------------- 1 | package mask 2 | 3 | // A common buffer implementation that may be used by 4 | // different rasterizers. 5 | type buffer struct { 6 | Width int // canvas width, in pixels 7 | Height int // canvas height, in pixels 8 | Values []float64 9 | // ^ Negative values are used for counter-clockwise segments, 10 | // positive values are used for clockwise segments. 11 | } 12 | 13 | // Sets a new Width and Height and resizes the underlying buffer 14 | // if necessary. The buffer contents are always cleared too. 15 | func (self *buffer) Resize(width, height int) { 16 | if width <= 0 || height <= 0 { 17 | panic("width or height <= 0") 18 | } 19 | self.Width = width 20 | self.Height = height 21 | totalLen := width * height 22 | if len(self.Values) == totalLen { 23 | // nothing 24 | } else if len(self.Values) > totalLen { 25 | self.Values = self.Values[0:totalLen] 26 | } else { // len(self.Values) < totalLen 27 | if cap(self.Values) >= totalLen { 28 | self.Values = self.Values[0:totalLen] 29 | } else { 30 | self.Values = make([]float64, totalLen) 31 | return // stop before ClearBuffer() 32 | } 33 | } 34 | 35 | self.Clear() 36 | } 37 | 38 | // Fills the internal buffer with zeros. 39 | func (self *buffer) Clear() { fastFillFloat64(self.Values, 0) } 40 | 41 | // Performs the boundary change accumulation operation storing 42 | // the results into the given buffer. Relevant to complete the 43 | // rasterization process when using edge marking algorithms. 44 | func (self *buffer) AccumulateUint8(buffer []uint8) { 45 | if len(buffer) != self.Width*self.Height { 46 | panic("uint8 buffer has wrong length") 47 | } 48 | 49 | index := 0 50 | for y := 0; y < self.Height; y++ { 51 | accumulator := float64(0) 52 | accUint8 := uint8(0) 53 | for x := 0; x < self.Width; x++ { 54 | value := self.Values[index] 55 | if value != 0 { // small optimization 56 | accumulator += value 57 | accUint8 = uint8(clampUnit64(abs64(accumulator)) * 255) 58 | } 59 | buffer[index] = accUint8 60 | index += 1 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /mask/default_rasterizer.go: -------------------------------------------------------------------------------- 1 | package mask 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | 7 | "github.com/tinne26/etxt/fract" 8 | "golang.org/x/image/font/sfnt" 9 | "golang.org/x/image/vector" 10 | ) 11 | 12 | var _ Rasterizer = (*DefaultRasterizer)(nil) 13 | 14 | // The DefaultRasterizer is a wrapper to make [golang.org/x/image/vector.Rasterizer] 15 | // conform to the [Rasterizer] interface. 16 | type DefaultRasterizer struct { 17 | rasterizer vector.Rasterizer 18 | normOffset fract.Point // offset to normalize points to the positive 19 | // quadrant starting from the fractional coords 20 | onChange func(Rasterizer) 21 | 22 | // Notice that the x/image/vector rasterizer expects coords in the 23 | // positive quadrant, which is why we need so many offsets here. 24 | } 25 | 26 | // Satisfies the [Rasterizer] interface. 27 | func (self *DefaultRasterizer) SetOnChangeFunc(onChange func(Rasterizer)) { 28 | self.onChange = onChange 29 | } 30 | 31 | // Satisfies the [Rasterizer] interface. The signature for the 32 | // default rasterizer is always zero, but may be customized as 33 | // you want through type embedding and method overriding. 34 | func (self *DefaultRasterizer) Signature() uint64 { return 0 } 35 | 36 | // Moves the current position to the given point. 37 | func (self *DefaultRasterizer) MoveTo(point fract.Point) { 38 | x, y := point.AddPoint(self.normOffset).ToFloat32s() 39 | self.rasterizer.MoveTo(x, y) 40 | } 41 | 42 | // Creates a straight boundary from the current position to the given point. 43 | func (self *DefaultRasterizer) LineTo(point fract.Point) { 44 | x, y := point.AddPoint(self.normOffset).ToFloat32s() 45 | self.rasterizer.LineTo(x, y) 46 | } 47 | 48 | // Creates a quadratic Bézier curve (also known as a conic Bézier curve) 49 | // to the given target passing through the given control point. 50 | func (self *DefaultRasterizer) QuadTo(control, target fract.Point) { 51 | cx, cy := control.AddPoint(self.normOffset).ToFloat32s() 52 | tx, ty := target.AddPoint(self.normOffset).ToFloat32s() 53 | self.rasterizer.QuadTo(cx, cy, tx, ty) 54 | } 55 | 56 | // Creates a cubic Bézier curve to the given target passing through 57 | // the given control points. 58 | func (self *DefaultRasterizer) CubeTo(controlA, controlB, target fract.Point) { 59 | cax, cay := controlA.AddPoint(self.normOffset).ToFloat32s() 60 | cbx, cby := controlB.AddPoint(self.normOffset).ToFloat32s() 61 | tx, ty := target.AddPoint(self.normOffset).ToFloat32s() 62 | self.rasterizer.CubeTo(cax, cay, cbx, cby, tx, ty) 63 | } 64 | 65 | // Satisfies the [Rasterizer] interface. 66 | func (self *DefaultRasterizer) Rasterize(outline sfnt.Segments, origin fract.Point) (*image.Alpha, error) { 67 | // get outline bounds 68 | fbounds := outline.Bounds() 69 | bounds := fract.Rect{ 70 | Min: fract.UnitsToPoint(fract.Unit(fbounds.Min.X), fract.Unit(fbounds.Min.Y)), 71 | Max: fract.UnitsToPoint(fract.Unit(fbounds.Max.X), fract.Unit(fbounds.Max.Y)), 72 | } 73 | 74 | // prepare rasterizer 75 | var width, height int 76 | var rectOffset image.Point 77 | width, height, self.normOffset, rectOffset = figureOutBounds(bounds, origin) 78 | self.rasterizer.Reset(width, height) 79 | self.rasterizer.DrawOp = draw.Src 80 | 81 | // allocate glyph mask 82 | mask := image.NewAlpha(self.rasterizer.Bounds()) 83 | 84 | // process outline 85 | processOutline(self, outline) 86 | 87 | // since the source texture is a uniform (an image that returns the same 88 | // color for any coordinate), the value of the point at which we want to 89 | // start sampling the texture (the fourth parameter) is unimportant. 90 | self.rasterizer.Draw(mask, mask.Bounds(), image.Opaque, image.Point{}) 91 | 92 | // translate the mask to its final position 93 | mask.Rect = mask.Rect.Add(rectOffset) 94 | return mask, nil 95 | } 96 | -------------------------------------------------------------------------------- /mask/doc.go: -------------------------------------------------------------------------------- 1 | // The mask subpackage defines the [Rasterizer] interface used within etxt 2 | // and provides multiple ready-to-use implementations. 3 | // 4 | // In this context, "[Rasterizer]" refers to a "glyph mask rasterizer": 5 | // whenever we want to render text on a screen we first have to rasterize 6 | // the individual font glyphs, extracted from font files as outlines 7 | // (sets of lines and curves), and draw them into a raster image (a grid 8 | // of pixels). 9 | // 10 | // In short, this subpackage allows anyone to pick different rasterizers 11 | // or implement their own by targeting the [Rasterizer] interface. This 12 | // opens the door to the creation of cool effects that may modify the 13 | // glyph outlines (e.g.: glyph expansion), the rasterization algorithms 14 | // (e.g.: hinting), the resulting glyph masks (e.g.: blurring) or any 15 | // combination of the previous. 16 | // 17 | // That said, before you jump into the hype train, notice that some of these 18 | // effects can also be achieved (often more easily) at later stages with 19 | // shaders or custom blitting. 20 | package mask 21 | -------------------------------------------------------------------------------- /mask/faux_rasterizer_bold_test.go: -------------------------------------------------------------------------------- 1 | package mask 2 | 3 | import "testing" 4 | 5 | func TestFauxBoldWhole(t *testing.T) { 6 | tests := []struct { 7 | w int 8 | in []uint8 9 | out []uint8 10 | }{ 11 | {w: 0, in: []uint8{0, 100, 0, 100, 0}, out: []uint8{0, 100, 0, 100, 0}}, 12 | {w: 1, in: []uint8{0, 100, 0, 100, 0}, out: []uint8{0, 100, 100, 100, 100}}, 13 | {w: 1, in: []uint8{0, 100, 50, 100, 0}, out: []uint8{0, 100, 100, 100, 100}}, 14 | {w: 1, in: []uint8{0, 9, 0, 0, 9, 0, 0}, out: []uint8{0, 9, 9, 0, 9, 9, 0}}, 15 | {w: 2, in: []uint8{5, 0, 0, 0, 5, 0, 0}, out: []uint8{5, 5, 5, 0, 5, 5, 5}}, 16 | {w: 0, in: []uint8{0, 100, 0, 100, 0}, out: []uint8{0, 100, 0, 100, 0}}, // consistency check 17 | {w: 2, in: []uint8{0, 5, 6, 4, 0, 0}, out: []uint8{0, 5, 6, 6, 6, 4}}, 18 | {w: 1, in: []uint8{9, 8, 7, 8, 9, 0}, out: []uint8{9, 9, 8, 8, 9, 9}}, 19 | {w: 1, in: []uint8{9, 1, 1, 2, 9, 0}, out: []uint8{9, 9, 1, 2, 9, 9}}, 20 | {w: 3, // real tests be like... 21 | in: []uint8{0, 0, 0, 0, 0, 200, 255, 235, 250, 230, 97, 2, 0, 0, 0, 0, 0, 78, 249, 255, 252, 111, 251, 251, 148, 19, 0, 0, 0, 0, 0, 28, 214, 255, 255, 120, 0, 0, 0, 0, 0}, 22 | out: []uint8{0, 0, 0, 0, 0, 200, 255, 255, 255, 255, 250, 250, 230, 97, 2, 0, 0, 78, 249, 255, 255, 255, 255, 252, 251, 251, 251, 148, 19, 0, 0, 28, 214, 255, 255, 255, 255, 255, 120, 0, 0}, 23 | }, 24 | {w: 8, 25 | in: []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 250, 255, 255, 255, 152, 74, 47, 69, 102, 169, 240, 254, 170, 102, 245, 252, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 26 | out: []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 254, 254, 254, 254, 254, 252, 252, 252, 252, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 27 | }, 28 | } 29 | 30 | rast := FauxRasterizer{} 31 | for i, test := range tests { 32 | rast.SetExtraWidth(float32(test.w)) 33 | out := make([]uint8, len(test.in)) 34 | for n, value := range test.in { 35 | out[n] = value 36 | } 37 | rast.applyRowExtraWidth(out, out, 0, 999) 38 | if !eqSliceUint8(out, test.out) { 39 | t.Fatalf("test#%d: in %v (+%d), expected %v, got %v", i, test.in, test.w, test.out, out) 40 | } 41 | } 42 | } 43 | 44 | func TestFauxBoldFract(t *testing.T) { 45 | tests := []struct { 46 | w float32 47 | in []uint8 48 | out []uint8 49 | }{ 50 | {w: 0.5, in: []uint8{0, 100, 0, 100, 0}, out: []uint8{0, 100, 50, 100, 50}}, 51 | {w: 0.5, in: []uint8{0, 100, 50, 100, 0}, out: []uint8{0, 100, 75, 100, 50}}, 52 | {w: 0.5, in: []uint8{0, 100, 0, 50, 0}, out: []uint8{0, 100, 50, 50, 25}}, 53 | {w: 1.5, in: []uint8{100, 0, 0, 100, 0, 0}, out: []uint8{100, 100, 50, 100, 100, 50}}, 54 | {w: 1.5, in: []uint8{0, 50, 0, 50, 0, 100, 0, 0}, out: []uint8{0, 50, 50, 50, 50, 100, 100, 50}}, 55 | {w: 8.5, in: []uint8{2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, out: []uint8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0}}, 56 | 57 | // rounding dependent tests (rounding up or down will produce slightly different results) 58 | {w: 0.5, in: []uint8{0, 100, 50, 25, 0}, out: []uint8{0, 100, 75, 38, 13}}, 59 | {w: 0.125, in: []uint8{100, 0, 0, 100, 0}, out: []uint8{100, 13, 0, 100, 13}}, 60 | {w: 0.625, in: []uint8{100, 0, 0, 100, 0}, out: []uint8{100, 63, 0, 100, 63}}, 61 | } 62 | 63 | rast := FauxRasterizer{} 64 | for i, test := range tests { 65 | rast.SetExtraWidth(test.w) 66 | out := make([]uint8, len(test.in)) 67 | for n, value := range test.in { 68 | out[n] = value 69 | } 70 | rast.applyRowExtraWidth(out, out, 0, 999) 71 | if !eqSliceUint8(out, test.out) { 72 | t.Fatalf("test#%d: in %v (+%0.1f), expected %v, got %v", i, test.in, test.w, test.out, out) 73 | } 74 | } 75 | } 76 | 77 | func eqSliceUint8(a, b []uint8) bool { 78 | if len(a) != len(b) { 79 | return false 80 | } 81 | for i, valueA := range a { 82 | if valueA != b[i] { 83 | return false 84 | } 85 | } 86 | return true 87 | } 88 | -------------------------------------------------------------------------------- /mask/faux_rasterizer_oblique_test.go: -------------------------------------------------------------------------------- 1 | package mask 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tinne26/etxt/fract" 7 | ) 8 | 9 | func TestFauxMinusOneSkew(t *testing.T) { 10 | // minus one skew is represented with a zero, and it's easy to mess up initialization 11 | rast := FauxRasterizer{} 12 | rast.SetSkewFactor(-1) 13 | skew := rast.GetSkewFactor() 14 | if skew != -1.0 { 15 | t.Fatalf("expected skew to be %f, got %f", -1.0, skew) 16 | } 17 | } 18 | 19 | func TestFauxOblique(t *testing.T) { 20 | tests := []struct { 21 | skew float32 22 | in []float64 23 | out []uint8 24 | }{ 25 | { 26 | skew: 1.0, 27 | in: []float64{0, -1 /**/, 1, -1 /**/, 1, 1 /**/, 0, 1}, 28 | out: []uint8{0, 128, 128 /**/, 128, 128, 0}, 29 | }, 30 | { 31 | skew: -1.0, 32 | in: []float64{0, -1 /**/, 1, -1 /**/, 1, 1 /**/, 0, 1}, 33 | out: []uint8{128, 128, 0 /**/, 0, 128, 128}, 34 | }, 35 | { 36 | skew: 1.0, 37 | in: []float64{0, -2 /**/, 1, -2 /**/, 1, 2 /**/, 0, 2}, 38 | out: []uint8{0, 0, 0, 128, 128 /**/, 0, 0, 128, 128, 0 /**/, 0, 128, 128, 0, 0 /**/, 128, 128, 0, 0, 0}, 39 | }, 40 | } 41 | 42 | rast := FauxRasterizer{} 43 | for i, test := range tests { 44 | rast.SetSkewFactor(test.skew) 45 | skew := rast.GetSkewFactor() 46 | if skew != test.skew { 47 | t.Fatalf("imprecise internal skew, expected %f, got %f", test.skew, skew) 48 | } 49 | segments := polySegments(test.in) 50 | mask, err := rast.Rasterize(segments, fract.Point{}) 51 | if err != nil { 52 | t.Fatalf("unexpected error: %s", err) 53 | } 54 | if !eqSliceUint8(mask.Pix, test.out) { 55 | exportTest("oblique_fail.png", mask) 56 | t.Fatalf("test #%d mistmatch: expected %v, got %v", i, test.out, mask.Pix) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /mask/faux_rasterizer_test.go: -------------------------------------------------------------------------------- 1 | package mask 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // TODO: may want to add tests proving the correct omission of redundant onChange notifications. 10 | 11 | func TestFloat32UnitRangeStability(t *testing.T) { 12 | if unitFP32FromUint16(0) != -1 { 13 | t.Fatal("expected minus one") 14 | } 15 | if unitFP32FromUint16(65535) != 1 { 16 | t.Fatal("expected one") 17 | } 18 | 19 | i := uint16(0) 20 | for { 21 | fp32 := unitFP32FromUint16(i) 22 | if fp32 == 0 { 23 | t.Fatalf("unexpected zero on i = %d", i) 24 | } 25 | u16 := uint16FromUnitFP32(fp32) 26 | if u16 != i { 27 | t.Fatalf("i = %d, fp32 = %f, u16 => %d", i, fp32, u16) 28 | } 29 | 30 | if i == 65535 { 31 | break 32 | } 33 | i += 1 // exhaustive testing 34 | } 35 | } 36 | 37 | func TestFloat32UnitRangeStabilityRng(t *testing.T) { 38 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 39 | for i := 0; i < 4096; i++ { 40 | fp32 := float32(rng.Float64()*2 - 1.0) 41 | if fp32 == 0 { 42 | continue 43 | } 44 | if fp32 < -1 || fp32 > 1 { 45 | panic("incorrect test code") 46 | } 47 | 48 | u16 := uint16FromUnitFP32(fp32) 49 | re32 := unitFP32FromUint16(u16) 50 | if re32 == 0 { 51 | t.Fatalf("got zero from %f", fp32) 52 | } 53 | re16 := uint16FromUnitFP32(re32) 54 | if re16 != u16 { 55 | t.Fatalf("unstability with fp32 = %f, u16 => %d, re32 = %f, re16 = %d", fp32, u16, re32, re16) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /mask/helper_funcs.go: -------------------------------------------------------------------------------- 1 | package mask 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/tinne26/etxt/fract" 7 | ) 8 | 9 | // Given the glyph bounds and an origin position indicating the subpixel 10 | // positioning (only lowest bits will be taken into account), it returns 11 | // the bounding integer width and heights, the normalization offset to be 12 | // applied to keep the coordinates in the positive plane, and the final 13 | // offset to be applied on the final mask to align its bounds to the glyph 14 | // origin. This is used in Rasterize() functions. 15 | func figureOutBounds(bounds fract.Rect, origin fract.Point) (int, int, fract.Point, image.Point) { 16 | floorMinX := bounds.Min.X.Floor() 17 | floorMinY := bounds.Min.Y.Floor() 18 | var maskCorrection image.Point 19 | maskCorrection.X = floorMinX.ToIntFloor() 20 | maskCorrection.Y = floorMinY.ToIntFloor() 21 | 22 | var normOffset fract.Point 23 | normOffset.X = -floorMinX + origin.X.FractShift() 24 | normOffset.Y = -floorMinY + origin.Y.FractShift() 25 | width := (bounds.Max.X + normOffset.X).ToIntCeil() 26 | height := (bounds.Max.Y + normOffset.Y).ToIntCeil() 27 | return width, height, normOffset, maskCorrection 28 | } 29 | 30 | // Around 9 times as fast as using a regular for loop. 31 | // This can trivially be made generic, and can also be adapted 32 | // to fill buffers with patterns (for example to fill 33 | // images with a specific color). 34 | func fastFillFloat64(buffer []float64, value float64) { 35 | if len(buffer) <= 24 { // no-copy case 36 | for i := range buffer { 37 | buffer[i] = value 38 | } 39 | } else { // copy case 40 | for i := range buffer[:16] { 41 | buffer[i] = value 42 | } 43 | for i := 16; i < len(buffer); i *= 2 { 44 | copy(buffer[i:], buffer[:i]) 45 | } 46 | } 47 | } 48 | 49 | // linearly interpolate (ax, ay) and (bx, by) at the given t, which 50 | // must be in [0, 1] 51 | func lerp(ax, ay, bx, by float64, t float64) (float64, float64) { 52 | return interpolateAt(ax, bx, t), interpolateAt(ay, by, t) 53 | } 54 | 55 | // interpolate a and b at the given t, which must be in [0, 1] 56 | func interpolateAt(a, b float64, t float64) float64 { return a + t*(b-a) } 57 | 58 | // Given two points of a line, it returns its A, B and C 59 | // coefficients from the form "Ax + By + C = 0". 60 | func toLinearFormABC(ox, oy, fx, fy float64) (float64, float64, float64) { 61 | a, b, c := fy-oy, -(fx - ox), (fx-ox)*oy-(fy-oy)*ox 62 | return a, b, c 63 | } 64 | 65 | func abs64(value float64) float64 { 66 | if value >= 0 { 67 | return value 68 | } 69 | return -value 70 | } 71 | 72 | func clampUnit64(value float64) float64 { 73 | if value <= 1.0 { 74 | return value 75 | } 76 | return 1.0 77 | } 78 | -------------------------------------------------------------------------------- /mask/helpers_test.go: -------------------------------------------------------------------------------- 1 | package mask 2 | 3 | // Helper functions for testing. 4 | 5 | import ( 6 | "math" 7 | "math/rand" 8 | 9 | "golang.org/x/image/font/sfnt" 10 | "golang.org/x/image/math/fixed" 11 | ) 12 | 13 | func similarFloat64Slices(a []float64, b []float64) bool { 14 | if len(a) != len(b) { 15 | return false 16 | } 17 | for i, valueA := range a { 18 | if valueA != b[i] { 19 | diff := math.Abs(valueA - b[i]) 20 | if diff > 0.001 { 21 | return false 22 | } // allow small precision differences 23 | } 24 | } 25 | return true 26 | } 27 | 28 | func randomTriangle(rng *rand.Rand, w, h int) sfnt.Segments { 29 | fsw, fsh := float64(w)*64, float64(h)*64 30 | segments := make([]sfnt.Segment, 0, 2) 31 | startX, startY := fixed.Int26_6(fsw/2), fixed.Int26_6(fsh/16) 32 | segments = moveTo(segments, startX, startY) 33 | segments = lineTo(segments, startX, fixed.Int26_6(fsh-fsh/16)) 34 | cx, cy := fixed.Int26_6(rng.Float64()*fsw), fixed.Int26_6(rng.Float64()*fsh) 35 | segments = lineTo(segments, cx, cy) 36 | segments = lineTo(segments, startX, startY) 37 | return sfnt.Segments(segments) 38 | } 39 | 40 | func randomQuad(rng *rand.Rand, w, h int) sfnt.Segments { 41 | fsw, fsh := float64(w)*64, float64(h)*64 42 | segments := make([]sfnt.Segment, 0, 2) 43 | startX, startY := fixed.Int26_6(fsw/2), fixed.Int26_6(fsh/16) 44 | segments = moveTo(segments, startX, startY) 45 | segments = lineTo(segments, startX, fixed.Int26_6(fsh-fsh/16)) 46 | cx, cy := fixed.Int26_6(rng.Float64()*fsw), fixed.Int26_6(rng.Float64()*fsh) 47 | segments = quadTo(segments, cx, cy, startX, startY) 48 | return sfnt.Segments(segments) 49 | } 50 | 51 | func randomSegments(rng *rand.Rand, lines, w, h int) sfnt.Segments { 52 | fsw, fsh := float64(w)*64, float64(h)*64 53 | var makeXY = func() (fixed.Int26_6, fixed.Int26_6) { 54 | return fixed.Int26_6(rng.Float64() * fsw), fixed.Int26_6(rng.Float64() * fsh) 55 | } 56 | 57 | // actually generate the segments 58 | startX, startY := makeXY() 59 | segments := make([]sfnt.Segment, 0, lines+1) 60 | segments = moveTo(segments, startX, startY) 61 | for i := 0; i < lines; i++ { 62 | x, y := makeXY() 63 | switch rng.Intn(3) { 64 | case 0: // LineTo 65 | segments = lineTo(segments, x, y) 66 | case 1: // QuadTo 67 | cx, cy := makeXY() 68 | segments = quadTo(segments, cx, cy, x, y) 69 | case 2: // CubeTo 70 | cx1, cy1 := makeXY() 71 | cx2, cy2 := makeXY() 72 | segments = cubeTo(segments, cx1, cy1, cx2, cy2, x, y) 73 | default: 74 | panic("unexpected case") 75 | } 76 | } 77 | segments = lineTo(segments, startX, startY) 78 | return sfnt.Segments(segments) 79 | } 80 | 81 | func polySegments(coords []float64) sfnt.Segments { 82 | if len(coords)%2 != 0 { 83 | panic("number of coordinates must be even") 84 | } 85 | if len(coords) < 6 { 86 | panic("number of coordinates must be at least 6 (three points)") 87 | } 88 | 89 | var tofx = func(x float64) fixed.Int26_6 { return fixed.Int26_6(x * 64) } 90 | segments := make([]sfnt.Segment, 0, len(coords)/2+1) 91 | segments = moveTo(segments, tofx(coords[0]), tofx(coords[1])) 92 | for i := 2; i < len(coords); i += 2 { 93 | x := coords[i+0] 94 | y := coords[i+1] 95 | segments = lineTo(segments, tofx(x), tofx(y)) 96 | } 97 | segments = lineTo(segments, tofx(coords[0]), tofx(coords[1])) 98 | return sfnt.Segments(segments) 99 | } 100 | 101 | func newSegment(op sfnt.SegmentOp, x1, y1, x2, y2, x3, y3 fixed.Int26_6) sfnt.Segment { 102 | return sfnt.Segment{Op: op, Args: [3]fixed.Point26_6{ 103 | {x1, y1}, {x2, y2}, {x3, y3}, 104 | }, 105 | } 106 | } 107 | 108 | func moveTo(segs []sfnt.Segment, x, y fixed.Int26_6) []sfnt.Segment { 109 | return append(segs, newSegment(sfnt.SegmentOpMoveTo, x, y, 0, 0, 0, 0)) 110 | } 111 | 112 | func lineTo(segs []sfnt.Segment, x, y fixed.Int26_6) []sfnt.Segment { 113 | return append(segs, newSegment(sfnt.SegmentOpLineTo, x, y, 0, 0, 0, 0)) 114 | } 115 | 116 | func quadTo(segs []sfnt.Segment, cx, cy, x, y fixed.Int26_6) []sfnt.Segment { 117 | return append(segs, newSegment(sfnt.SegmentOpQuadTo, cx, cy, x, y, 0, 0)) 118 | } 119 | 120 | func cubeTo(segs []sfnt.Segment, cx1, cy1, cx2, cy2, x, y fixed.Int26_6) []sfnt.Segment { 121 | return append(segs, newSegment(sfnt.SegmentOpCubeTo, cx1, cy1, cx2, cy2, x, y)) 122 | } 123 | -------------------------------------------------------------------------------- /mask/sharp_rasterizer.go: -------------------------------------------------------------------------------- 1 | package mask 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/tinne26/etxt/fract" 7 | "golang.org/x/image/font/sfnt" 8 | ) 9 | 10 | var _ Rasterizer = (*SharpRasterizer)(nil) 11 | 12 | // A rasterizer that quantizes all glyph mask values to fully opaque 13 | // or fully transparent. Its primary use-case is to make scaled pixel 14 | // art fonts look sharper through the elimination of blurry edges. 15 | // 16 | // Since the implementation leverages type embedding, the available methods 17 | // are the same as the ones for [DefaultRasterizer], even if they do not 18 | // appear explicitly in the documentation. 19 | type SharpRasterizer struct{ DefaultRasterizer } 20 | 21 | // Satisfies the [Rasterizer] interface. 22 | func (self *SharpRasterizer) Rasterize(outline sfnt.Segments, origin fract.Point) (*image.Alpha, error) { 23 | mask, err := self.DefaultRasterizer.Rasterize(outline, origin) 24 | if err != nil { 25 | return mask, err 26 | } 27 | for i := 0; i < len(mask.Pix); i++ { 28 | // we use 128 as the threshold, but if you want another value, 29 | // just copy paste the extremely short code and set your own 30 | // or make it customizable 31 | if mask.Pix[i] < 128 { 32 | mask.Pix[i] = 0 33 | } else { 34 | mask.Pix[i] = 255 35 | } 36 | } 37 | return mask, err 38 | } 39 | 40 | // Satisfies the [Rasterizer] interface. 41 | func (self *SharpRasterizer) Signature() uint64 { 42 | // the overriding is necessary to prevent glyphs 43 | // rasterized by this being mixed up with the 44 | // embedded default rasterizer 45 | return 0x0099000000000000 46 | } 47 | -------------------------------------------------------------------------------- /mask/sharper_rasterizer.go: -------------------------------------------------------------------------------- 1 | package mask 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/tinne26/etxt/fract" 7 | "golang.org/x/image/font/sfnt" 8 | ) 9 | 10 | var _ Rasterizer = (*SharperRasterizer)(nil) 11 | 12 | // A variant of SharpRasterizer, with more complex evaluations of what 13 | // should be or not be a solid pixel. This process is executed on the 14 | // CPU; an ideal implementation for Ebitengine would perform a similar 15 | // process through a shader instead. As of right now, this is offered 16 | // mostly as a proof of concept. 17 | type SharperRasterizer struct{ DefaultRasterizer } 18 | 19 | func max2(a, b uint8) uint8 { 20 | if a >= b { 21 | return a 22 | } 23 | return b 24 | } 25 | 26 | func max3(a, b, c uint8) uint8 { 27 | return max2(max2(a, b), c) 28 | } 29 | 30 | func stemPick(center, sideA, sideB uint8) uint8 { 31 | if sideA == 0 { 32 | return max2(center, sideB) 33 | } 34 | if sideB == 0 { 35 | return max2(center, sideA) 36 | } 37 | return center 38 | } 39 | 40 | func cornerPick(a, b uint8) uint8 { 41 | if a == 0 && b == 0 { 42 | return 255 43 | } 44 | return max2(a, b) 45 | } 46 | 47 | // Satisfies the [Rasterizer] interface. 48 | func (self *SharperRasterizer) Rasterize(outline sfnt.Segments, origin fract.Point) (*image.Alpha, error) { 49 | mask, err := self.DefaultRasterizer.Rasterize(outline, origin) 50 | if err != nil { 51 | return mask, err 52 | } 53 | self.sharpen(mask) 54 | return mask, nil 55 | } 56 | 57 | func (self *SharperRasterizer) sharpen(mask *image.Alpha) { 58 | // first pass, correcting corners 59 | bounds := mask.Bounds() 60 | width := bounds.Dx() 61 | height := bounds.Dy() 62 | 63 | var i int = 0 64 | for y := 0; y < height; y++ { 65 | for x := 0; x < width; x++ { 66 | value := mask.Pix[i] 67 | if value == 0 || value == 255 { 68 | i += 1 69 | continue 70 | } 71 | 72 | // check neighbours 73 | var up, down, left, right uint8 74 | if y > 0 { 75 | up = mask.Pix[i-width] 76 | } 77 | if y < height-1 { 78 | down = mask.Pix[i+width] 79 | } 80 | if x > 0 { 81 | left = mask.Pix[i-1] 82 | } 83 | if x < width-1 { 84 | right = mask.Pix[i+1] 85 | } 86 | 87 | if up == 255 { 88 | if left == 255 { 89 | mask.Pix[i] = cornerPick(right, down) 90 | } else if right == 255 { 91 | mask.Pix[i] = cornerPick(left, down) 92 | } else { 93 | mask.Pix[i] = stemPick(value, left, right) 94 | } 95 | } else if left == 255 { 96 | if down == 255 { 97 | mask.Pix[i] = cornerPick(up, right) 98 | } else { 99 | mask.Pix[i] = stemPick(value, up, down) 100 | } 101 | } else if right == 255 { 102 | if down == 255 { // corner case 103 | mask.Pix[i] = cornerPick(up, left) 104 | } else { 105 | mask.Pix[i] = stemPick(value, up, down) 106 | } 107 | } else if down == 255 { 108 | mask.Pix[i] = stemPick(value, left, right) 109 | } else { 110 | // isolated fragment cases. I don't know if this is necessary in practice 111 | // ok, this should only be done if a solid interior exists 112 | if up >= 128 { 113 | if left >= 128 && mask.Pix[i-width-1] >= 128 { 114 | mask.Pix[i] = max3(value, up, left) 115 | } else if right >= 128 && mask.Pix[i-width+1] >= 128 { 116 | mask.Pix[i] = max3(value, up, right) 117 | } 118 | } else if left >= 128 { 119 | if down >= 128 && mask.Pix[i+width-1] >= 128 { 120 | mask.Pix[i] = max3(value, left, down) 121 | } 122 | } else if right >= 128 { 123 | if down >= 128 && mask.Pix[i+width+1] >= 128 { 124 | mask.Pix[i] = max3(value, right, down) 125 | } 126 | } 127 | } 128 | 129 | i += 1 130 | } 131 | } 132 | 133 | for i := 0; i < len(mask.Pix); i++ { 134 | if mask.Pix[i] < 128 { 135 | mask.Pix[i] = 0 136 | } else { 137 | mask.Pix[i] = 255 138 | } 139 | } 140 | } 141 | 142 | // Satisfies the [Rasterizer] interface. 143 | func (self *SharperRasterizer) Signature() uint64 { 144 | return 0x009E000000000000 145 | } 146 | -------------------------------------------------------------------------------- /renderer_restorable_state.go: -------------------------------------------------------------------------------- 1 | package etxt 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/tinne26/etxt/fract" 7 | "github.com/tinne26/etxt/mask" 8 | "github.com/tinne26/etxt/sizer" 9 | "golang.org/x/image/font/sfnt" 10 | ) 11 | 12 | type restorableState struct { 13 | fontColor color.Color 14 | fontSizer sizer.Sizer 15 | rasterizer mask.Rasterizer 16 | activeFont *sfnt.Font 17 | 18 | textDirection Direction 19 | horzQuantization uint8 20 | vertQuantization uint8 21 | align Align 22 | 23 | scale fract.Unit 24 | logicalSize fract.Unit 25 | scaledSize fract.Unit 26 | fontIndex fontIndex 27 | blendMode BlendMode 28 | } 29 | -------------------------------------------------------------------------------- /sizer/default_sizer.go: -------------------------------------------------------------------------------- 1 | package sizer 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/tinne26/etxt/fract" 7 | . "golang.org/x/image/font/sfnt" 8 | "golang.org/x/image/math/fixed" 9 | ) 10 | 11 | var _ Sizer = (*DefaultSizer)(nil) 12 | 13 | // The default [Sizer] used by etxt renderers. For more information 14 | // about sizers, see the documentation of the [Sizer] interface. 15 | type DefaultSizer struct { 16 | cachedAscent fract.Unit 17 | cachedDescent fract.Unit 18 | cachedLineHeight fract.Unit 19 | unused fract.Unit 20 | } 21 | 22 | // Satisfies the [Sizer] interface. 23 | func (self *DefaultSizer) Ascent(*Font, *Buffer, fract.Unit) fract.Unit { 24 | return self.cachedAscent 25 | } 26 | 27 | // Satisfies the [Sizer] interface. 28 | func (self *DefaultSizer) Descent(*Font, *Buffer, fract.Unit) fract.Unit { 29 | return self.cachedDescent 30 | } 31 | 32 | // Satisfies the [Sizer] interface. 33 | func (self *DefaultSizer) LineGap(*Font, *Buffer, fract.Unit) fract.Unit { 34 | return self.cachedLineHeight - self.cachedAscent - self.cachedDescent 35 | } 36 | 37 | // Satisfies the [Sizer] interface. 38 | func (self *DefaultSizer) LineHeight(*Font, *Buffer, fract.Unit) fract.Unit { 39 | return self.cachedLineHeight 40 | } 41 | 42 | // Satisfies the [Sizer] interface. 43 | func (self *DefaultSizer) LineAdvance(*Font, *Buffer, fract.Unit, int) fract.Unit { 44 | return self.cachedLineHeight 45 | } 46 | 47 | // Satisfies the [Sizer] interface. 48 | func (self *DefaultSizer) GlyphAdvance(font *Font, buffer *Buffer, size fract.Unit, g GlyphIndex) fract.Unit { 49 | advance, err := font.GlyphAdvance(buffer, g, fixed.Int26_6(size), hintingNone) 50 | if err == nil { 51 | return fract.Unit(advance) 52 | } 53 | panic("font.GlyphAdvance(index = " + strconv.Itoa(int(g)) + ") error: " + err.Error()) 54 | } 55 | 56 | // Satisfies the [Sizer] interface. 57 | func (self *DefaultSizer) Kern(font *Font, buffer *Buffer, size fract.Unit, g1, g2 GlyphIndex) fract.Unit { 58 | kern, err := font.Kern(buffer, g1, g2, fixed.Int26_6(size), hintingNone) 59 | if err == nil { 60 | return fract.Unit(kern) 61 | } 62 | if err == ErrNotFound { 63 | return 0 64 | } 65 | 66 | msg := "font.Kern failed for glyphs with indices " 67 | msg += strconv.Itoa(int(g1)) + " and " 68 | msg += strconv.Itoa(int(g2)) + ": " + err.Error() 69 | panic(msg) 70 | } 71 | 72 | // Satisfies the [Sizer] interface. 73 | func (self *DefaultSizer) NotifyChange(font *Font, buffer *Buffer, size fract.Unit) { 74 | if font == nil || size == 0 { 75 | self.cachedAscent = 0 76 | self.cachedDescent = 0 77 | self.cachedLineHeight = 0 78 | } else { 79 | metrics, err := font.Metrics(buffer, fixed.Int26_6(size), hintingNone) 80 | if err != nil { 81 | panic("font.Metrics error: " + err.Error()) 82 | } 83 | self.cachedAscent = fract.Unit(metrics.Ascent) 84 | self.cachedDescent = fract.Unit(metrics.Descent) 85 | self.cachedLineHeight = fract.Unit(metrics.Height) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /sizer/doc.go: -------------------------------------------------------------------------------- 1 | // The sizer subpackage defines the [Sizer] interface used within etxt 2 | // and provides multiple ready-to-use implementations. 3 | // 4 | // The job of a [Sizer] is to determine how much space should be taken 5 | // by each glyph. While font files already contain this information, 6 | // using an interface as a middle layer allows etxt users to modify 7 | // spacing manually and achieve specific effects like ignoring kerning, 8 | // adding extra padding between letters or accounting for the extra space 9 | // taken by custom glyph rasterizers. 10 | package sizer 11 | -------------------------------------------------------------------------------- /sizer/helpers.go: -------------------------------------------------------------------------------- 1 | package sizer 2 | 3 | import "golang.org/x/image/font" 4 | 5 | // This should have been named quantization, not hinting, 6 | // but whatever... 7 | const hintingNone = font.HintingNone 8 | 9 | // Alias for unexported type embedding on other sizers. 10 | type defaultSizer = DefaultSizer 11 | -------------------------------------------------------------------------------- /sizer/padded_advance_sizer.go: -------------------------------------------------------------------------------- 1 | package sizer 2 | 3 | import ( 4 | "github.com/tinne26/etxt/fract" 5 | . "golang.org/x/image/font/sfnt" 6 | ) 7 | 8 | var _ Sizer = (*PaddedAdvanceSizer)(nil) 9 | 10 | // Like [PaddedKernSizer], but adds the extra padding in the advance 11 | // instead of the kern. 12 | // 13 | // If you aren't modifying the glyphs, only padding them horizontally, 14 | // use [PaddedKernSizer] instead. This sizer is intended to deal with 15 | // modified glyphs that have actually become wider, like in a faux 16 | // bold process. 17 | type PaddedAdvanceSizer struct { 18 | defaultSizer 19 | } 20 | 21 | // Sets the configurable horizontal padding value. 22 | func (self *PaddedAdvanceSizer) SetPadding(value fract.Unit) { 23 | self.defaultSizer.unused = value 24 | } 25 | 26 | // Returns the configurable horizontal padding value. 27 | func (self *PaddedAdvanceSizer) GetPadding() fract.Unit { 28 | return self.defaultSizer.unused 29 | } 30 | 31 | // Satisfies the [Sizer] interface. 32 | func (self *PaddedAdvanceSizer) GlyphAdvance(font *Font, buffer *Buffer, size fract.Unit, g GlyphIndex) fract.Unit { 33 | return self.defaultSizer.GlyphAdvance(font, buffer, size, g) + self.defaultSizer.unused 34 | } 35 | -------------------------------------------------------------------------------- /sizer/padded_kern_sizer.go: -------------------------------------------------------------------------------- 1 | package sizer 2 | 3 | import ( 4 | "github.com/tinne26/etxt/fract" 5 | . "golang.org/x/image/font/sfnt" 6 | ) 7 | 8 | var _ Sizer = (*PaddedKernSizer)(nil) 9 | 10 | // A [Sizer] that behaves like the default one, but with a configurable 11 | // horizontal padding factor that's added to the kern between glyphs. 12 | // 13 | // See also [PaddedScalableKernSizer] if you need to deal with scalable 14 | // text. 15 | type PaddedKernSizer struct { 16 | defaultSizer 17 | } 18 | 19 | // Sets the configurable horizontal kern padding value. 20 | func (self *PaddedKernSizer) SetPadding(value fract.Unit) { 21 | self.defaultSizer.unused = value 22 | } 23 | 24 | // Returns the configurable horizontal kern padding value. 25 | func (self *PaddedKernSizer) GetPadding() fract.Unit { 26 | return self.defaultSizer.unused 27 | } 28 | 29 | // Satisfies the [Sizer] interface. 30 | func (self *PaddedKernSizer) Kern(font *Font, buffer *Buffer, size fract.Unit, g1, g2 GlyphIndex) fract.Unit { 31 | return self.defaultSizer.Kern(font, buffer, size, g1, g2) + self.defaultSizer.unused 32 | } 33 | -------------------------------------------------------------------------------- /sizer/padded_scalable_kern_sizer.go: -------------------------------------------------------------------------------- 1 | package sizer 2 | 3 | import ( 4 | "github.com/tinne26/etxt/fract" 5 | . "golang.org/x/image/font/sfnt" 6 | ) 7 | 8 | var _ Sizer = (*PaddedScalableKernSizer)(nil) 9 | 10 | // Similar to [PaddedKernSizer], but instead of taking the padding 11 | // as an absolute value, it uses a value relative to a font size of 12 | // 16px and scales it automatically based on the active font size. 13 | // 14 | // After modifying PaddingAt16px you must call [PaddedScalableKernSizer.NotifyChanges](). 15 | type PaddedScalableKernSizer struct { 16 | defaultSizer 17 | PaddingAt16px fract.Unit 18 | } 19 | 20 | // Returns the current padding scaled by the given size. 21 | func (self *PaddedScalableKernSizer) GetPaddingAtSize(size fract.Unit) fract.Unit { 22 | return self.PaddingAt16px.Rescale(16<<6, size) 23 | } 24 | 25 | // Satisfies the [Sizer] interface. 26 | func (self *PaddedScalableKernSizer) Kern(font *Font, buffer *Buffer, size fract.Unit, g1, g2 GlyphIndex) fract.Unit { 27 | return self.defaultSizer.Kern(font, buffer, size, g1, g2) + self.defaultSizer.unused 28 | } 29 | 30 | // Satisfies the [Sizer] interface. 31 | func (self *PaddedScalableKernSizer) NotifyChange(font *Font, buffer *Buffer, size fract.Unit) { 32 | self.defaultSizer.unused = self.GetPaddingAtSize(size) 33 | self.defaultSizer.NotifyChange(font, buffer, size) 34 | } 35 | -------------------------------------------------------------------------------- /sizer/sizer.go: -------------------------------------------------------------------------------- 1 | package sizer 2 | 3 | import ( 4 | "github.com/tinne26/etxt/fract" 5 | . "golang.org/x/image/font/sfnt" 6 | ) 7 | 8 | // When drawing or traversing glyphs, we need some information 9 | // related to the "font metrics". For example, how much we need 10 | // to advance after drawing a glyph or what's the kerning between 11 | // a specific pair of glyphs. 12 | // 13 | // Sizers are the interface that renderers use to obtain that 14 | // information. 15 | // 16 | // You rarely need to care about sizers, but they can be useful 17 | // in the following cases: 18 | // - Customize line height or advances. 19 | // - Disable kerning or adjust horizontal spacing. 20 | // - Make full size adjustments for a custom rasterizer (e.g., 21 | // a rasterizer that puts glyphs into boxes, bubbles or frames). 22 | type Sizer interface { 23 | // Notice: while Ascent(), Descent() and LineGap() may 24 | // seem superfluous, they can be necessary for 25 | // some custom rasterizers. Uncommon but possible. 26 | // I also think they can be generally helpful. 27 | 28 | // Returns the ascent of the given font, at the given size, 29 | // as an absolute value. 30 | // 31 | // The given font and sizes must be consistent with the 32 | // latest Sizer.NotifyChange() call. 33 | Ascent(*Font, *Buffer, fract.Unit) fract.Unit 34 | 35 | // Returns the descent of the given font, at the given size, 36 | // as an absolute value. 37 | // 38 | // The given font and sizes must be consistent with the 39 | // latest Sizer.NotifyChange() call. 40 | Descent(*Font, *Buffer, fract.Unit) fract.Unit 41 | 42 | // Returns the line gap of the given font, at the given size, 43 | // as an absolute value. 44 | // 45 | // The given font and sizes must be consistent with the 46 | // latest Sizer.NotifyChange() call. 47 | LineGap(*Font, *Buffer, fract.Unit) fract.Unit 48 | 49 | // Utility method equivalent to Ascent() + Descent() + LineGap(). 50 | LineHeight(*Font, *Buffer, fract.Unit) fract.Unit 51 | 52 | // Returns the line advance of the given font at the given size. 53 | // 54 | // The given font and the size must be consistent with the 55 | // latest Sizer.NotifyChange() call. 56 | // 57 | // The given int indicates that this is the nth consecutive 58 | // call to the method (consecutive line breaks). In most cases, 59 | // the value will be 1. Values below 1 are invalid. Values 60 | // can only be strictly increasing by +1. 61 | LineAdvance(*Font, *Buffer, fract.Unit, int) fract.Unit 62 | 63 | // Returns the advance of the given glyph for the given font 64 | // and size. 65 | // 66 | // The given font and the size must be consistent with the 67 | // latest Sizer.NotifyChange() call. 68 | GlyphAdvance(*Font, *Buffer, fract.Unit, GlyphIndex) fract.Unit 69 | 70 | // Returns the kerning value between two glyphs of the given font 71 | // and size. 72 | // 73 | // The given font and the size must be consistent with the 74 | // latest Sizer.NotifyChange() call. 75 | Kern(*Font, *Buffer, fract.Unit, GlyphIndex, GlyphIndex) fract.Unit 76 | 77 | // Must be called to sync the state of the sizer and allow it 78 | // to do any caching it may want to do in relation to the given 79 | // active font or size. 80 | NotifyChange(*Font, *Buffer, fract.Unit) 81 | } 82 | -------------------------------------------------------------------------------- /sizer/vertical_sizer.go: -------------------------------------------------------------------------------- 1 | package sizer 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/tinne26/etxt/fract" 7 | . "golang.org/x/image/font/sfnt" 8 | "golang.org/x/image/math/fixed" 9 | ) 10 | 11 | var _ Sizer = (*CustomVertSizer)(nil) 12 | 13 | // A sizer that ignores the specific vertical metrics provided by 14 | // the font and instead replaces them with fixed values relative to 15 | // the font size. This can be used to manually control the line 16 | // height for a single font or a small set of fonts. 17 | // 18 | // One must call [CustomVertSizer.NotifyChange]() to properly update 19 | // values after changing AscentMult, DescentMult or LineGapMult. 20 | type CustomVertSizer struct { 21 | AscentMult fract.Unit 22 | DescentMult fract.Unit 23 | LineGapMult fract.Unit 24 | cachedAscent fract.Unit 25 | cachedDescent fract.Unit 26 | cachedLineHeight fract.Unit 27 | } 28 | 29 | // Satisfies the [Sizer] interface. 30 | func (self *CustomVertSizer) Ascent(*Font, *Buffer, fract.Unit) fract.Unit { 31 | return self.cachedAscent 32 | } 33 | 34 | // Satisfies the [Sizer] interface. 35 | func (self *CustomVertSizer) Descent(*Font, *Buffer, fract.Unit) fract.Unit { 36 | return self.cachedDescent 37 | } 38 | 39 | // Satisfies the [Sizer] interface. 40 | func (self *CustomVertSizer) LineGap(*Font, *Buffer, fract.Unit) fract.Unit { 41 | return self.cachedLineHeight - self.cachedAscent - self.cachedDescent 42 | } 43 | 44 | // Satisfies the [Sizer] interface. 45 | func (self *CustomVertSizer) LineHeight(*Font, *Buffer, fract.Unit) fract.Unit { 46 | return self.cachedLineHeight 47 | } 48 | 49 | // Satisfies the [Sizer] interface. 50 | func (self *CustomVertSizer) LineAdvance(*Font, *Buffer, fract.Unit, int) fract.Unit { 51 | return self.cachedLineHeight 52 | } 53 | 54 | // Satisfies the [Sizer] interface. 55 | func (self *CustomVertSizer) GlyphAdvance(font *Font, buffer *Buffer, size fract.Unit, g GlyphIndex) fract.Unit { 56 | advance, err := font.GlyphAdvance(buffer, g, fixed.Int26_6(size), hintingNone) 57 | if err == nil { 58 | return fract.Unit(advance) 59 | } 60 | panic("font.GlyphAdvance(index = " + strconv.Itoa(int(g)) + ") error: " + err.Error()) 61 | } 62 | 63 | // Satisfies the [Sizer] interface. 64 | func (self *CustomVertSizer) Kern(font *Font, buffer *Buffer, size fract.Unit, g1, g2 GlyphIndex) fract.Unit { 65 | kern, err := font.Kern(buffer, g1, g2, fixed.Int26_6(size), hintingNone) 66 | if err == nil { 67 | return fract.Unit(kern) 68 | } 69 | if err == ErrNotFound { 70 | return 0 71 | } 72 | 73 | msg := "font.Kern failed for glyphs with indices " 74 | msg += strconv.Itoa(int(g1)) + " and " 75 | msg += strconv.Itoa(int(g2)) + ": " + err.Error() 76 | panic(msg) 77 | } 78 | 79 | // Satisfies the [Sizer] interface. 80 | func (self *CustomVertSizer) NotifyChange(_ *Font, _ *Buffer, size fract.Unit) { 81 | self.cachedAscent = size.MulUp(self.AscentMult) 82 | self.cachedDescent = size.MulUp(self.DescentMult) 83 | self.cachedLineHeight = size.MulUp(self.LineGapMult) + self.cachedAscent + self.cachedDescent 84 | } 85 | -------------------------------------------------------------------------------- /string_iterator.go: -------------------------------------------------------------------------------- 1 | package etxt 2 | 3 | import "unicode/utf8" 4 | 5 | // Definitions of private types used to iterate strings and glyphs 6 | // on Traverse* operations. Sometimes we iterate lines in reverse, 7 | // so there's a bit of trickiness here and there. 8 | 9 | type ltrStringIterator struct{ index int } 10 | 11 | func (self *ltrStringIterator) Next(text string) rune { 12 | if self.index < len(text) { 13 | codePoint, runeSize := utf8.DecodeRuneInString(text[self.index:]) 14 | self.index += runeSize 15 | return codePoint 16 | } else { 17 | return -1 18 | } 19 | } 20 | 21 | func (self *ltrStringIterator) PeekNext(text string) rune { 22 | if self.index < len(text) { 23 | codePoint, _ := utf8.DecodeRuneInString(text[self.index:]) 24 | return codePoint 25 | } else { 26 | return -1 27 | } 28 | } 29 | 30 | func (self *ltrStringIterator) Unroll(codePoint rune) { 31 | self.index -= utf8.RuneLen(codePoint) 32 | } 33 | 34 | func (self *ltrStringIterator) StringLeft(text string) string { 35 | if self.index >= len(text) { 36 | return "" 37 | } 38 | return text[self.index:] 39 | } 40 | 41 | type rtlStringIterator struct{ head, tail, index int } 42 | 43 | func (self *rtlStringIterator) Init(text string) { 44 | self.tail = 0 45 | self.head = 0 46 | self.LineSlide(text) 47 | } 48 | 49 | func (self *rtlStringIterator) LineSlide(text string) { 50 | self.tail = self.head 51 | if self.head >= len(text) { 52 | self.index = self.tail 53 | } else { 54 | if text[self.head] == '\n' { 55 | self.head += 1 56 | } else { 57 | for self.head < len(text) { // find next line break or end of string 58 | codePoint, runeSize := utf8.DecodeRuneInString(text[self.head:]) 59 | if codePoint == '\n' { 60 | break 61 | } 62 | self.head += runeSize 63 | } 64 | } 65 | self.index = self.head 66 | } 67 | } 68 | 69 | func (self *rtlStringIterator) Next(text string) rune { 70 | if self.index > self.tail { 71 | codePoint, runeSize := utf8.DecodeLastRuneInString(text[:self.index]) 72 | self.index -= runeSize 73 | if codePoint == '\n' || self.index <= self.tail { 74 | self.LineSlide(text) 75 | } 76 | return codePoint 77 | } else { 78 | return -1 79 | } 80 | } 81 | 82 | func (self *rtlStringIterator) PeekNext(text string) rune { 83 | if self.index > self.tail { 84 | codePoint, _ := utf8.DecodeLastRuneInString(text[:self.index]) 85 | return codePoint 86 | } else { 87 | return -1 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /string_iterator_test.go: -------------------------------------------------------------------------------- 1 | package etxt 2 | 3 | import "testing" 4 | 5 | func testFailRunes(t *testing.T, expected rune, got rune) { 6 | t.Fatalf("expected '%s', got '%s'", string(expected), string(got)) 7 | } 8 | 9 | func TestRtlStringIterator(t *testing.T) { 10 | var iter rtlStringIterator 11 | testString := "abcd" 12 | iter.Init(testString) 13 | for _, expected := range []rune{'d', 'c', 'b', 'a', -1, -1, -1} { 14 | got := iter.Next(testString) 15 | if got != expected { 16 | testFailRunes(t, expected, got) 17 | } 18 | } 19 | 20 | testString = "a" 21 | iter.Init(testString) 22 | for _, expected := range []rune{'a', -1, -1, -1} { 23 | got := iter.Next(testString) 24 | if got != expected { 25 | testFailRunes(t, expected, got) 26 | } 27 | } 28 | 29 | testString = "" 30 | iter.Init(testString) 31 | for _, expected := range []rune{-1, -1, -1} { 32 | got := iter.Next(testString) 33 | if got != expected { 34 | testFailRunes(t, expected, got) 35 | } 36 | } 37 | 38 | testString = "\n" 39 | iter.Init(testString) 40 | for _, expected := range []rune{'\n', -1, -1, -1} { 41 | got := iter.Next(testString) 42 | if got != expected { 43 | testFailRunes(t, expected, got) 44 | } 45 | } 46 | 47 | testString = "\n\n" 48 | iter.Init(testString) 49 | for _, expected := range []rune{'\n', '\n', -1, -1, -1} { 50 | got := iter.Next(testString) 51 | if got != expected { 52 | testFailRunes(t, expected, got) 53 | } 54 | } 55 | 56 | testString = "\na\nb\n" 57 | iter.Init(testString) 58 | for _, expected := range []rune{'\n', 'a', '\n', 'b', '\n', -1, -1, -1} { 59 | got := iter.Next(testString) 60 | if got != expected { 61 | testFailRunes(t, expected, got) 62 | } 63 | } 64 | 65 | testString = "hello\nworld\n" 66 | iter.Init(testString) 67 | for _, expected := range []rune{'o', 'l', 'l', 'e', 'h', '\n', 'd', 'l', 'r', 'o', 'w', '\n', -1, -1, -1} { 68 | got := iter.Next(testString) 69 | if got != expected { 70 | testFailRunes(t, expected, got) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # How to `go test` etxt 2 | 3 | Testing etxt requires placing two different `.ttf` fonts in `/font/test/`. Some tests can be executed even without these fonts, but you will get one failure due to missing assets. 4 | 5 | The main testing command is the following: 6 | ``` 7 | go test -tags gtxt ./... 8 | ``` 9 | While you can test without the `gtxt` tag, most tests won't run because there's no easy way to test Ebitengine's graphical output. I made an effort to compare gtxt and Ebitengine results using `go generate`. That's explained in the next section. Outside that, Ebitengine-specific tests exist mainly to help detect build problems. 10 | 11 | Many scripts are provided in `/test/scripts`. For example, you can run the tests with `run_tests.sh` (or `run_tests.bat` if you are on Windows). Here are the results of an example run: 12 | ``` 13 | $ ./test/scripts/run_tests.sh 14 | [testing with gtxt...] 15 | ok github.com/tinne26/etxt 0.331s coverage: 44.2% of statements 16 | ok github.com/tinne26/etxt/cache 0.266s coverage: 82.2% of statements 17 | ok github.com/tinne26/etxt/font 0.284s coverage: 82.9% of statements 18 | ok github.com/tinne26/etxt/fract 0.307s coverage: 91.0% of statements 19 | ok github.com/tinne26/etxt/mask 0.311s coverage: 83.5% of statements 20 | 21 | [testing with Ebitengine...] 22 | ok github.com/tinne26/etxt 0.506s coverage: 18.0% of statements 23 | ok github.com/tinne26/etxt/cache 0.463s coverage: 82.2% of statements 24 | ok github.com/tinne26/etxt/font 0.286s coverage: 82.9% of statements 25 | ok github.com/tinne26/etxt/fract 0.478s coverage: 90.5% of statements 26 | ok github.com/tinne26/etxt/mask 0.546s coverage: 83.5% of statements 27 | ``` 28 | 29 | ## Testing Ebitengine vs gtxt 30 | 31 | As explained in the previous section, Ebitengine's graphical output is hard to test. The logic between the default etxt version and the `-tags gtxt` version is almost entirely shared, so testing only with `gtxt` is still a fairly decent guarantee that things will also work on Ebitengine. The main difference are blend modes and glyph compositing over a target surface. To help cover this gap, we can use `go generate` from the base `etxt` directory: 32 | ``` 33 | $ go generate 34 | Generating 'testdata_blend_rand_ebiten_test.go'... OK 35 | Generating 'testdata_blend_rand_ebiten_gtxt_test.go'... OK 36 | Generating 'testdata_blend_rand_gtxt_test.go'... OK 37 | ``` 38 | 39 | This will generate a few additional test files that contain only raw render data. Running `go test .` or `go test -tags gtxt .` afterwards will include this data on existing conditional tests. These tests will compare the compositing results of etxt's different modes and report if the results vary in any meaningful way. 40 | 41 | To be honest, this set of tests is fairly limited and simplistic at the moment, but it's still much better than having no cross comparison tests at all. 42 | 43 | 44 | ## Honest reliability assessment 45 | 46 | High test coverage percentages don't really mean much. Some examples: 47 | - The whole `go generate` stuff for Ebitengine doesn't even increase coverage. 48 | - You often have to write many more tests than what's strictly required for coverage to be really confident that something works as intended. I have written many such tests, but many more are still missing. 49 | - Examples go a long way in improving my confidence that something is working, even if this isn't reflected on tests coverage. 50 | - Maturity of v0.0.9 API is still quite heterogeneous. 51 | 52 | v0.0.9 is still on its infancy and it's likely to be much less stable than v0.0.8. At the same time, it also fixes a few big bugs and makes many, many small quality improvements over v0.0.8. 53 | -------------------------------------------------------------------------------- /test/generate/blend_rand/ebiten.go: -------------------------------------------------------------------------------- 1 | //go:build GENERATE_ETXT_TESTDATA 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image/color" 8 | "math/rand" 9 | "os" 10 | "strconv" 11 | 12 | "github.com/hajimehoshi/ebiten/v2" 13 | "github.com/tinne26/etxt" 14 | "github.com/tinne26/etxt/fract" 15 | ) 16 | 17 | // See etxt/testdata_generate.go for details. 18 | // Must be generated from base etxt directory, so testdata 19 | // files are placed at the same level as testdata_generate.go. 20 | 21 | var contents []byte = []byte("package etxt\n\nfunc init() {\n\ttestdata[\"blend_rand_ebiten\"] = []byte{") 22 | 23 | type Game struct{} 24 | 25 | func (self *Game) Layout(w, h int) (int, int) { return w, h } 26 | func (self *Game) Draw(*ebiten.Image) {} 27 | func (self *Game) Update() error { 28 | renderer := etxt.NewRenderer() 29 | target := ebiten.NewImage(8, 8) 30 | target.Fill(color.RGBA{96, 96, 96, 96}) 31 | mask := ebiten.NewImage(1, 1) 32 | mask.Set(0, 0, color.RGBA{255, 255, 255, 255}) 33 | rng := rand.New(rand.NewSource(3707)) 34 | for y := 0; y < 8; y++ { 35 | for x := 0; x < 8; x++ { 36 | a := rng.Intn(256) 37 | r, g, b := rng.Intn(a+1), rng.Intn(a+1), rng.Intn(a+1) 38 | rngColor := color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)} 39 | renderer.SetColor(rngColor) 40 | renderer.Glyph().DrawMask(target, mask, fract.IntsToPoint(x, y)) 41 | } 42 | } 43 | 44 | buffer := make([]byte, 8*8*4) 45 | target.ReadPixels(buffer) 46 | for i, value := range buffer { 47 | if i%32 == 0 { 48 | contents = append(contents, '\n', '\t', '\t') 49 | } else if i%4 == 0 { 50 | contents = append(contents, '/', '*', '*', '/', ' ') 51 | } 52 | contents = append(contents, []byte(strconv.Itoa(int(value)))...) 53 | contents = append(contents, ',', ' ') 54 | } 55 | contents = append(contents, []byte("\n\t}\n}\n")...) 56 | 57 | return ebiten.Termination 58 | } 59 | 60 | func main() { 61 | const filename = "testdata_blend_rand_ebiten_test.go" 62 | fmt.Print("Generating '" + filename + "'... ") 63 | 64 | err := ebiten.RunGame(&Game{}) 65 | if err != nil { 66 | fatal(err) 67 | } 68 | 69 | file, err := os.Create(filename) 70 | if err != nil { 71 | fatal(err) 72 | } 73 | _, err = file.Write(contents) 74 | if err != nil { 75 | _ = os.Remove(filename) 76 | fatal(err) 77 | } 78 | 79 | fmt.Print("OK\n") 80 | } 81 | 82 | func fatal(err error) { 83 | fmt.Fprint(os.Stderr, "\nERROR: "+err.Error()+"\n") 84 | os.Exit(1) 85 | } 86 | -------------------------------------------------------------------------------- /test/generate/blend_rand/ebiten_gtxt.go: -------------------------------------------------------------------------------- 1 | //go:build GENERATE_ETXT_TESTDATA && gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "math/rand" 10 | "os" 11 | "strconv" 12 | 13 | "github.com/hajimehoshi/ebiten/v2" 14 | "github.com/tinne26/etxt" 15 | "github.com/tinne26/etxt/fract" 16 | ) 17 | 18 | // See etxt/testdata_generate.go for details. 19 | // Must be generated from base etxt directory, so testdata 20 | // files are placed at the same level as testdata_generate.go. 21 | 22 | var contents []byte = []byte("package etxt\n\nfunc init() {\n\ttestdata[\"blend_rand_ebiten_gtxt\"] = []byte{") 23 | 24 | type Game struct{} 25 | 26 | func (self *Game) Layout(w, h int) (int, int) { return w, h } 27 | func (self *Game) Draw(*ebiten.Image) {} 28 | func (self *Game) Update() error { 29 | renderer := etxt.NewRenderer() 30 | target := ebiten.NewImage(8, 8) 31 | target.Fill(color.RGBA{96, 96, 96, 96}) 32 | mask := image.NewAlpha(image.Rect(0, 0, 1, 1)) 33 | mask.Set(0, 0, color.Alpha{255}) 34 | rng := rand.New(rand.NewSource(3707)) 35 | for y := 0; y < 8; y++ { 36 | for x := 0; x < 8; x++ { 37 | a := rng.Intn(256) 38 | r, g, b := rng.Intn(a+1), rng.Intn(a+1), rng.Intn(a+1) 39 | rngColor := color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)} 40 | renderer.SetColor(rngColor) 41 | renderer.Glyph().DrawMask(target, mask, fract.IntsToPoint(x, y)) 42 | } 43 | } 44 | 45 | buffer := make([]byte, 8*8*4) 46 | target.ReadPixels(buffer) 47 | for i, value := range buffer { 48 | if i%32 == 0 { 49 | contents = append(contents, '\n', '\t', '\t') 50 | } else if i%4 == 0 { 51 | contents = append(contents, '/', '*', '*', '/', ' ') 52 | } 53 | contents = append(contents, []byte(strconv.Itoa(int(value)))...) 54 | contents = append(contents, ',', ' ') 55 | } 56 | contents = append(contents, []byte("\n\t}\n}\n")...) 57 | 58 | return ebiten.Termination 59 | } 60 | 61 | func main() { 62 | const filename = "testdata_blend_rand_ebiten_gtxt_test.go" 63 | fmt.Print("Generating '" + filename + "'... ") 64 | 65 | err := ebiten.RunGame(&Game{}) 66 | if err != nil { 67 | fatal(err) 68 | } 69 | 70 | file, err := os.Create(filename) 71 | if err != nil { 72 | fatal(err) 73 | } 74 | _, err = file.Write(contents) 75 | if err != nil { 76 | _ = os.Remove(filename) 77 | fatal(err) 78 | } 79 | 80 | fmt.Print("OK\n") 81 | } 82 | 83 | func fatal(err error) { 84 | fmt.Fprint(os.Stderr, "\nERROR: "+err.Error()+"\n") 85 | os.Exit(1) 86 | } 87 | -------------------------------------------------------------------------------- /test/generate/blend_rand/gtxt.go: -------------------------------------------------------------------------------- 1 | //go:build GENERATE_ETXT_TESTDATA && gtxt 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "math/rand" 10 | "os" 11 | "strconv" 12 | 13 | "github.com/tinne26/etxt" 14 | "github.com/tinne26/etxt/fract" 15 | ) 16 | 17 | // See etxt/testdata_generate.go for details. 18 | // Must be generated from base etxt directory, so testdata 19 | // files are placed at the same level as testdata_generate.go. 20 | 21 | var contents []byte = []byte("package etxt\n\nfunc init() {\n\ttestdata[\"blend_rand_gtxt\"] = []byte{") 22 | 23 | func main() { 24 | const filename = "testdata_blend_rand_gtxt_test.go" 25 | fmt.Print("Generating '" + filename + "'... ") 26 | 27 | // draw, obtain result values, encode them 28 | renderer := etxt.NewRenderer() 29 | target := image.NewRGBA(image.Rect(0, 0, 8, 8)) 30 | fill(target, color.RGBA{96, 96, 96, 96}) 31 | mask := image.NewAlpha(image.Rect(0, 0, 1, 1)) 32 | mask.Set(0, 0, color.Alpha{255}) 33 | rng := rand.New(rand.NewSource(3707)) 34 | for y := 0; y < 8; y++ { 35 | for x := 0; x < 8; x++ { 36 | a := rng.Intn(256) 37 | r, g, b := rng.Intn(a+1), rng.Intn(a+1), rng.Intn(a+1) 38 | rngColor := color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)} 39 | renderer.SetColor(rngColor) 40 | renderer.Glyph().DrawMask(target, mask, fract.IntsToPoint(x, y)) 41 | } 42 | } 43 | 44 | for i, value := range target.Pix { 45 | if i%32 == 0 { 46 | contents = append(contents, '\n', '\t', '\t') 47 | } else if i%4 == 0 { 48 | contents = append(contents, '/', '*', '*', '/', ' ') 49 | } 50 | contents = append(contents, []byte(strconv.Itoa(int(value)))...) 51 | contents = append(contents, ',', ' ') 52 | } 53 | contents = append(contents, []byte("\n\t}\n}\n")...) 54 | 55 | file, err := os.Create(filename) 56 | if err != nil { 57 | fatal(err) 58 | } 59 | _, err = file.Write(contents) 60 | if err != nil { 61 | _ = os.Remove(filename) 62 | fatal(err) 63 | } 64 | 65 | fmt.Print("OK\n") 66 | } 67 | 68 | func fatal(err error) { 69 | fmt.Fprint(os.Stderr, "\nERROR: "+err.Error()+"\n") 70 | os.Exit(1) 71 | } 72 | 73 | func fill(img *image.RGBA, clr color.RGBA) { 74 | for y := img.Rect.Min.Y; y < img.Rect.Max.Y; y++ { 75 | for x := img.Rect.Min.X; x < img.Rect.Max.X; x++ { 76 | img.SetRGBA(x, y, clr) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/scripts/run_benchmarks.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Notice: requires one .ttf font file in etxt/test/fonts/ 4 | :: (any normal font will do) 5 | echo [benchmarking with gtxt...] 6 | go test -bench "." -tags "gtxt bench" ./... | findstr /R "^[^?]" 7 | 8 | :: ebitengine pass is the same at the moment so it's disabled 9 | :: echo. 10 | :: echo [benchmarking with Ebitengine...] 11 | :: go test -bench "." -tags "bench" ./... | findstr /R "^[^?]" 12 | 13 | :: You may also use -benchmem 14 | :: go test -bench "." -benchmem -tags "gtxt bench" ./... | findstr /R "^[^?]" 15 | -------------------------------------------------------------------------------- /test/scripts/run_benchmarks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Notice: requires one .ttf font file in etxt/test/fonts/ 4 | # (any normal font will do) 5 | echo "[benchmarking with gtxt...]" 6 | go test -bench "." -tags "gtxt bench" ./... | grep "^[^?]" 7 | 8 | # ebitengine pass is the same at the moment so it's disabled 9 | # echo "" 10 | # echo "[Ebitengine pass...]" 11 | # go test -bench "." -tags "bench" ./... | grep "^[^?]" 12 | 13 | # You may also use -benchmem 14 | # go test -bench "." -benchmem -tags "gtxt bench" ./... | grep "^[^?]" 15 | -------------------------------------------------------------------------------- /test/scripts/run_coverhtml.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Notice: requires one .ttf font file in etxt/font/test/ 4 | :: (any normal font will do) 5 | go test -tags gtxt ./... -coverprofile cover_prof.out >NUL 6 | go tool cover -html=cover_prof.out 7 | del cover_prof.out 8 | -------------------------------------------------------------------------------- /test/scripts/run_coverhtml.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Notice: requires one .ttf font file in etxt/test/fonts/ 4 | # (any normal font will do) 5 | go test -tags "gtxt test" ./... -coverprofile cover_prof.out > /dev/null 6 | go tool cover -html=cover_prof.out 7 | rm cover_prof.out 8 | -------------------------------------------------------------------------------- /test/scripts/run_tests.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Notice: testing requires two .ttf font files in etxt/test/fonts/ 4 | :: (any normal fonts will do) 5 | echo [testing with gtxt...] 6 | go test -tags gtxt -count "1" -cover ./... | findstr /R "^[^?]" 7 | 8 | :: ebitengine pass is barely relevant at the moment, 9 | :: but it helps catch build tag mixups 10 | echo. 11 | echo [testing with Ebitengine...] 12 | go test -count "1" -cover ./... | findstr /R "^[^?]" 13 | -------------------------------------------------------------------------------- /test/scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Notice: testing requires two .ttf font files in etxt/test/fonts/ 4 | # (any normal fonts will do) 5 | echo "[testing with gtxt...]" 6 | go test -tags gtxt -count "1" -cover ./... | grep "^[^?]" 7 | 8 | # ebitengine pass is barely relevant at the moment, 9 | # but it helps catch build tag mixups 10 | echo "" 11 | echo "[testing with Ebitengine...]" 12 | go test -count "1" -cover ./... | grep "^[^?]" 13 | -------------------------------------------------------------------------------- /test_utils_test.go: -------------------------------------------------------------------------------- 1 | package etxt 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/png" 7 | "os" 8 | ) 9 | 10 | func doesNotPanic(function func()) (didNotPanic bool) { 11 | didNotPanic = true 12 | defer func() { didNotPanic = (recover() == nil) }() 13 | function() 14 | return 15 | } 16 | 17 | func debugExport(name string, img image.Image) { 18 | file, err := os.Create(name) 19 | if err != nil { 20 | fmt.Println(err) 21 | os.Exit(1) 22 | } 23 | err = png.Encode(file, img) 24 | if err != nil { 25 | fmt.Println(err) 26 | os.Exit(1) 27 | } 28 | err = file.Close() 29 | if err != nil { 30 | fmt.Println(err) 31 | os.Exit(1) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /testdata_generate.go: -------------------------------------------------------------------------------- 1 | package etxt 2 | 3 | // Ebitengine is hard to test with "go test" because you need to create 4 | // standalone programs with a main function and so on. There are multiple 5 | // ways to work around that, and the truth is that for testing etxt there's 6 | // already the gtxt CPU version that allows achieving high test coverage 7 | // with a fairly decent degree of reliability. Still, in order to make sure 8 | // that the gtxt version (CPU rendering) and the default Ebitengine version 9 | // (GPU rendering [notice that rasterization still happens on CPU]) results 10 | // are matching, we can use go:generate to run standalone Ebitengine programs, 11 | // get some raw image results, plug that into basic tests and print it all 12 | // as static test files. 13 | 14 | // Fixed seed fuzzy test of compositing with gtxt vs Ebitengine. 15 | //go:generate go run -tags "GENERATE_ETXT_TESTDATA" test/generate/blend_rand/ebiten.go 16 | //go:generate go run -tags "GENERATE_ETXT_TESTDATA gtxt" test/generate/blend_rand/ebiten_gtxt.go 17 | //go:generate go run -tags "GENERATE_ETXT_TESTDATA gtxt" test/generate/blend_rand/gtxt.go 18 | -------------------------------------------------------------------------------- /testdata_test.go: -------------------------------------------------------------------------------- 1 | package etxt 2 | 3 | import "testing" 4 | 5 | // Tests using pre-generated test data for comparison 6 | // of Ebitengine and gtxt rendering results. Take a look 7 | // at testdata_generate.go for more details. 8 | 9 | var testdata = make(map[string][]byte) 10 | 11 | func TestTestdataBlendRand(t *testing.T) { 12 | // get test data 13 | valuesE, foundE := testdata["blend_rand_ebiten"] 14 | valuesG, foundG := testdata["blend_rand_gtxt"] 15 | valuesB, foundB := testdata["blend_rand_ebiten_gtxt"] 16 | if foundE != foundG || foundE != foundB { 17 | panic("incorrect test data generation or setup") 18 | } 19 | if !foundE { 20 | t.SkipNow() 21 | } 22 | 23 | // compare values 24 | if !similarByteSlices(valuesE, valuesG) { 25 | t.Fatalf("Mismatched testdata blend_rand results:\nEbitengine results: %v\ngtxt results: %v", valuesE, valuesG) 26 | } 27 | if !similarByteSlices(valuesE, valuesB) { 28 | t.Fatalf("Mismatched testdata blend_rand results:\nEbitengine results: %v\nEbitengine + gtxt results: %v", valuesE, valuesB) 29 | } 30 | } 31 | 32 | func similarByteSlices(a, b []byte) bool { 33 | if len(a) != len(b) { 34 | return false 35 | } 36 | for i := 0; i < len(a); i++ { 37 | if a[i] == b[i] { 38 | continue 39 | } 40 | if a[i] < b[i] && a[i]+1 == b[i] { 41 | continue 42 | } 43 | if b[i] < a[i] && b[i]+1 == a[i] { 44 | continue 45 | } 46 | return false 47 | } 48 | return true 49 | } 50 | --------------------------------------------------------------------------------