github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/examples/ebiten/all_glyphs/main.go (about) 1 package main 2 3 import "os" 4 import "log" 5 import "fmt" 6 import "time" 7 import "image/color" 8 9 import "github.com/hajimehoshi/ebiten/v2" 10 import "golang.org/x/image/math/fixed" 11 12 import "github.com/Kintar/etxt" 13 import "github.com/Kintar/etxt/esizer" 14 15 // NOTE: to honor the beauty of fonts, I decided to make this example 16 // resizable and fullscreen-able (press F, but release fast or 17 // it will re-trigger). You can navigate glyphs with the up/down 18 // arrows. You can also press H to switch on/off the current glyph 19 // info. Enjoy your fonts! 20 21 const GlyphSize = 64 // controls the glyph size 22 const GlyphSpacing = 1.24 // controls the space around/between glyphs 23 24 type Game struct { 25 txtRenderer *etxt.Renderer 26 numGlyphs int 27 position int 28 spacing int 29 lastPressTime time.Time 30 glyphs []etxt.GlyphIndex 31 32 screenWidth int 33 screenHeight int 34 hint bool 35 hintRenderer *etxt.Renderer 36 bottomFade *ebiten.Image 37 } 38 39 func NewGame(renderer *etxt.Renderer, font *etxt.Font, noHint bool) *Game { 40 numGlyphs := font.NumGlyphs() 41 fixedSizer := &esizer.FixedSizer{} 42 renderer.SetSizer(fixedSizer) 43 44 var hintRenderer *etxt.Renderer 45 if !noHint { 46 hintRenderer = etxt.NewStdRenderer() 47 hintRenderer.SetFont(font) 48 hintRenderer.SetSizePx(14) 49 hintRenderer.SetHorzAlign(etxt.Right) 50 cache := etxt.NewDefaultCache(1024 * 1024) // 1MB cache 51 hintRenderer.SetCacheHandler(cache.NewHandler()) 52 hintRenderer.SetColor(color.RGBA{92, 92, 92, 255}) 53 } 54 55 game := &Game{ 56 txtRenderer: renderer, 57 numGlyphs: numGlyphs, 58 position: 0, 59 spacing: 0, 60 lastPressTime: time.Now(), 61 screenWidth: 640, 62 screenHeight: 480, 63 hint: true, 64 hintRenderer: hintRenderer, 65 } 66 game.refreshScreenProperties() 67 return game 68 } 69 70 func (self *Game) refreshScreenProperties() { 71 size := self.txtRenderer.GetSizePxFract() 72 spacing := int((float64(size) * GlyphSpacing) / 64) 73 sizer := self.txtRenderer.GetSizer().(*esizer.FixedSizer) 74 sizer.SetAdvance(spacing) 75 glyphsPerLine := self.screenWidth / spacing 76 if glyphsPerLine != len(self.glyphs) { 77 self.glyphs = make([]etxt.GlyphIndex, glyphsPerLine) 78 } 79 self.spacing = spacing 80 self.bottomFade = ebiten.NewImage(self.screenWidth, 16) 81 self.bottomFade.Fill(color.RGBA{0, 0, 0, 64}) 82 } 83 84 func (self *Game) Layout(w int, h int) (int, int) { 85 if w != self.screenWidth || h != self.screenHeight { 86 self.screenWidth = w 87 self.screenHeight = h 88 self.refreshScreenProperties() 89 } 90 return w, h 91 } 92 93 func (self *Game) Update() error { 94 now := time.Now() 95 if now.Sub(self.lastPressTime) < time.Millisecond*250 { 96 return nil 97 } 98 99 if ebiten.IsKeyPressed(ebiten.KeyF) { 100 ebiten.SetFullscreen(!ebiten.IsFullscreen()) 101 self.lastPressTime = now 102 return nil 103 } 104 105 if ebiten.IsKeyPressed(ebiten.KeyH) { 106 self.hint = !self.hint 107 self.lastPressTime = now 108 return nil 109 } 110 111 up := ebiten.IsKeyPressed(ebiten.KeyUp) 112 down := ebiten.IsKeyPressed(ebiten.KeyDown) 113 glyphsPerLine := len(self.glyphs) 114 line := self.position / glyphsPerLine 115 maxLine := (self.numGlyphs + glyphsPerLine - 1) / glyphsPerLine 116 maxLine -= (self.screenHeight / self.spacing) 117 if self.numGlyphs > glyphsPerLine { 118 maxLine += 1 119 } 120 if maxLine < 0 { 121 maxLine = 0 122 } 123 124 if self.position > 0 && up { 125 self.position -= glyphsPerLine 126 if self.position < 0 { 127 self.position = 0 128 } 129 self.lastPressTime = now 130 } else if line < maxLine && down { 131 self.position += glyphsPerLine 132 if self.position >= self.numGlyphs { 133 self.position = self.numGlyphs - 1 134 } 135 self.lastPressTime = now 136 } 137 return nil 138 } 139 140 func (self *Game) Draw(screen *ebiten.Image) { 141 // dark background 142 screen.Fill(color.RGBA{0, 0, 0, 255}) 143 144 // draw text 145 self.txtRenderer.SetTarget(screen) 146 currentPos := self.position 147 sp := self.spacing // I'm gonna use this to death 148 lineWidth := sp*len(self.glyphs) - sp/3 149 leftMargin := (self.screenWidth - lineWidth) / 2 150 for i := 0; i < (self.screenHeight+sp-1)/sp; i++ { 151 // set the glyph indices to draw 152 lastPos := currentPos + len(self.glyphs) 153 if lastPos >= self.numGlyphs { 154 lastPos = self.numGlyphs 155 } 156 if lastPos <= currentPos { 157 continue 158 } 159 for i := 0; currentPos+i < lastPos; i++ { 160 self.glyphs[i] = etxt.GlyphIndex(currentPos + i) 161 } 162 163 // draw the glyphs 164 glyphs := self.glyphs[0 : lastPos-currentPos] 165 x, y := leftMargin, (sp*3)/4+i*sp 166 self.txtRenderer.TraverseGlyphs(glyphs, fixed.P(x, y), 167 func(dot fixed.Point26_6, index etxt.GlyphIndex) { 168 mask := self.txtRenderer.LoadGlyphMask(index, dot) 169 self.txtRenderer.DefaultDrawFunc(dot, mask, index) 170 }) 171 172 // advance to next glyphs 173 currentPos = lastPos 174 } 175 176 // make the bottom fade out 177 for i := 0; i < 16; i++ { 178 opts := &ebiten.DrawImageOptions{} 179 opts.GeoM.Translate(0, float64(self.screenHeight-16+i)) 180 screen.DrawImage(self.bottomFade, opts) 181 } 182 183 // draw current glyph index and total 184 if self.hint && self.hintRenderer != nil { 185 self.hintRenderer.SetTarget(screen) 186 text := fmt.Sprintf("%d of %d glyphs", self.position+1, self.numGlyphs) 187 self.hintRenderer.Draw(text, self.screenWidth-4, self.screenHeight-6) 188 self.hintRenderer.SetTarget(nil) 189 } 190 } 191 192 func main() { 193 // get font path 194 if len(os.Args) != 2 { 195 msg := "Usage: expects one argument with the path to the font to be used\n" 196 fmt.Fprint(os.Stderr, msg) 197 os.Exit(1) 198 } 199 200 // parse font 201 font, fontName, err := etxt.ParseFontFrom(os.Args[1]) 202 if err != nil { 203 log.Fatal(err) 204 } 205 fmt.Printf("Font loaded: %s\n", fontName) 206 207 // create cache 208 cache := etxt.NewDefaultCache(10 * 1024 * 1024) // 10MB 209 // **IMPORTANT CACHE INFO** 210 // In almost every example we have been setting caches to 1GB, 211 // mostly because we weren't expecting to fill them anywhere near 212 // that. In this example, instead, we are setting the cache to a 213 // more reasonable value, because otherwise the cache could really 214 // fill up a lot for some fonts. 215 // 216 // This example is almost a textbook situation for a cache: glyphs 217 // only appear once, and if they are on the screen they will be 218 // heavily reused, but once we scroll past them, they aren't likely 219 // to come up again. And... if they come up again, it is because we 220 // saw them recently and scrolled back to them. So least recently 221 // used policies are a perfect fit for this. Technically, the default 222 // cache uses random sampling for eviction, not perfect LRU, but 223 // the results should be quite decent anyway. 224 225 // create and configure renderer 226 renderer := etxt.NewStdRenderer() 227 renderer.SetCacheHandler(cache.NewHandler()) 228 renderer.SetSizePx(GlyphSize) 229 renderer.SetFont(font) 230 renderer.SetAlign(etxt.YCenter, etxt.Left) 231 232 // determine if we have the right glyphs to show hint text 233 const alphabet = " ofglyphs0123456789" 234 missing, err := etxt.GetMissingRunes(font, alphabet) 235 if err != nil { 236 log.Fatal(err) 237 } 238 239 // run the game 240 ebiten.SetWindowTitle("etxt/examples/ebiten/all_glyphs") 241 ebiten.SetWindowResizable(true) 242 ebiten.SetWindowSize(640, 480) 243 ebiten.SetCursorMode(ebiten.CursorModeHidden) // doing this right, boys... 244 err = ebiten.RunGame(NewGame(renderer, font, len(missing) > 0)) 245 if err != nil { 246 log.Fatal(err) 247 } 248 }