github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/examples/ebiten/elastic_sizer/main.go (about) 1 package main 2 3 import "os" 4 import "log" 5 import "fmt" 6 import "image/color" 7 8 import "golang.org/x/image/math/fixed" 9 import "github.com/hajimehoshi/ebiten/v2" 10 import "github.com/Kintar/etxt" 11 import "github.com/Kintar/etxt/esizer" 12 import "github.com/Kintar/etxt/ecache" 13 14 // you can play around with these, but it can get out of hand quite easily 15 const SpringText = "Bouncy!" 16 const MinExpansion = 0.34 // must be strictly below 1.0 17 const MaxExpansion = 4.0 // must be strictly above 1.0 18 const Timescaling = 0.8 / 40.0 // make the first factor smaller to slow down 19 const Bounciness = 25.0 20 21 type Game struct { 22 txtRenderer *etxt.Renderer 23 24 // spring related variables 25 restLength float64 26 textLen float64 // number of code points in SpringText - 1 27 expansion float64 // between MinExpansion - MaxExpansion 28 inertia float64 29 holdX int 30 holding bool 31 } 32 33 func NewGame(renderer *etxt.Renderer) *Game { 34 textRect := renderer.SelectionRect(SpringText) 35 precacheText(renderer) // not necessary, but a useful example 36 return &Game{ 37 txtRenderer: renderer, 38 restLength: float64(textRect.Width) / 64, 39 textLen: float64(len([]rune(SpringText))), 40 expansion: 1.0, 41 inertia: 0.0, 42 holdX: 0, 43 holding: false, 44 } 45 } 46 47 func (self *Game) Layout(w int, h int) (int, int) { return w, h } 48 func (self *Game) Update() error { 49 // All this code in Update() doesn't have much to do with text rendering 50 // or anything, it's the spring simulation and related logic. It's the 51 // most complex part of the program, but you may ignore it completely, 52 // as the spring simulation is not even good (too linear). 53 if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { 54 // manual spring manipulation with the mouse 55 if !self.holding { 56 // just started to hold 57 self.holding = true 58 self.holdX, _ = ebiten.CursorPosition() 59 } else { 60 // continue holding and moving 61 newHold, _ := ebiten.CursorPosition() 62 diff := newHold - self.holdX 63 self.holdX = newHold 64 expansionChange := float64(diff) / self.restLength 65 self.expansion += expansionChange 66 if self.expansion < MinExpansion { 67 self.expansion = MinExpansion 68 } 69 if self.expansion > MaxExpansion { 70 self.expansion = MaxExpansion 71 } 72 } 73 } else { // spring simulation 74 self.holding = false 75 var tension float64 76 workingLength := (MaxExpansion - MinExpansion) * self.restLength 77 if self.expansion < 1.0 { 78 tension = ((1.0 - self.expansion) / (1.0 - MinExpansion)) * workingLength 79 } else { // expansion >= 1.0 80 tension = -((self.expansion - 1.0) / (MaxExpansion - 1.0)) * workingLength 81 } 82 83 // apply movement and update inertia 84 movement := (self.inertia + tension) * Timescaling 85 self.inertia += Bounciness * tension * Timescaling 86 self.expansion = self.expansion + (movement / self.restLength) 87 88 // clamp expansion if it went outside range 89 if self.expansion < MinExpansion { 90 self.expansion = MinExpansion 91 self.inertia = 0 92 } 93 if self.expansion > MaxExpansion { 94 self.expansion = MaxExpansion 95 self.inertia = 0 96 } 97 } 98 99 return nil 100 } 101 102 // For the purposes of this example, the only key line is 103 // "sizer.SetHorzPaddingFloat"... but whatever, the others 104 // may be interesting too as general usage examples. 105 func (self *Game) Draw(screen *ebiten.Image) { 106 // dark background 107 screen.Fill(color.RGBA{0, 0, 0, 255}) 108 109 // get and adjust sizer (we could have stored it earlier too) 110 sizer := self.txtRenderer.GetSizer().(*esizer.HorzPaddingSizer) 111 sizer.SetHorzPaddingFloat((self.expansion*self.restLength - self.restLength) / self.textLen) 112 113 // get some values that we will use later 114 w, h := screen.Size() 115 preVertAlign, preHorzAlign := self.txtRenderer.GetAlign() 116 startX := 16 117 if preHorzAlign == etxt.XCenter { 118 startX = w / 2 119 } 120 121 // draw text 122 self.txtRenderer.SetTarget(screen) 123 self.txtRenderer.Draw(SpringText, startX, h/2) 124 125 // draw fps and instructions text 126 sizer.SetHorzPadding(0) 127 preSize := self.txtRenderer.GetSizePxFract() 128 129 self.txtRenderer.SetColor(color.RGBA{255, 255, 255, 128}) 130 self.txtRenderer.SetAlign(etxt.Baseline, etxt.Right) 131 self.txtRenderer.SetSizePx(14) 132 self.txtRenderer.Draw(fmt.Sprintf("%.2f FPS", ebiten.CurrentFPS()), w-8, h-8) 133 self.txtRenderer.SetHorzAlign(etxt.Left) 134 txt := "click and drag horizontally to interact" 135 if self.holding { 136 txt += " (holding)" 137 } 138 self.txtRenderer.Draw(txt, 8, h-8) 139 140 // restore renderer state after fps/instructions 141 self.txtRenderer.SetColor(color.RGBA{255, 255, 255, 255}) 142 self.txtRenderer.SetAlign(preVertAlign, preHorzAlign) 143 self.txtRenderer.SetSizePxFract(preSize) 144 } 145 146 func main() { 147 // get font path 148 if len(os.Args) != 2 { 149 msg := "Usage: expects one argument with the path to the font to be used\n" 150 fmt.Fprint(os.Stderr, msg) 151 os.Exit(1) 152 } 153 154 // parse font 155 font, fontName, err := etxt.ParseFontFrom(os.Args[1]) 156 if err != nil { 157 log.Fatal(err) 158 } 159 fmt.Printf("Font loaded: %s\n", fontName) 160 161 // create cache 162 cache := etxt.NewDefaultCache(1024 * 1024 * 1024) // 1GB cache 163 164 // create and configure renderer 165 renderer := etxt.NewStdRenderer() 166 renderer.SetCacheHandler(cache.NewHandler()) 167 renderer.SetSizePx(64) 168 renderer.SetFont(font) 169 renderer.SetAlign(etxt.YCenter, etxt.Left) // you can try etxt.XCenter too 170 renderer.SetSizer(&esizer.HorzPaddingSizer{}) 171 renderer.SetQuantizerStep(1, 64) // * 172 // * Disabling horizontal quantization is helpful here to get 173 // smoother results. But it also means we have to cache each 174 // glyph in 64 different positions! At big sizes this is not 175 // cheap. You can try commenting it out to see the difference. 176 // You may also use bigger step values and see how the animation 177 // becomes more or less smooth. 178 179 // run the game 180 ebiten.SetWindowTitle("etxt/examples/ebiten/elastic") 181 ebiten.SetWindowSize(840, 360) 182 err = ebiten.RunGame(NewGame(renderer)) 183 if err != nil { 184 log.Fatal(err) 185 } 186 } 187 188 // This code has been added mostly to provide an example of how to manually 189 // cache text at fractional px positions. It might serve as a nice example of 190 // fixed.Int26_6 manipulation. 191 // 192 // More effective caching mechanisms may be added to etxt in the future, 193 // but this is a good example of how to do it by hand if required. 194 func precacheText(renderer *etxt.Renderer) { 195 tmpTarget := ebiten.NewImage(1, 1) 196 renderer.SetTarget(tmpTarget) 197 for i := 0; i < 64; i++ { 198 renderer.DrawFract(SpringText, fixed.Int26_6(i), 0) 199 } 200 renderer.SetTarget(nil) 201 tmpTarget.Dispose() 202 203 // print info about cache size 204 cacheHandler := renderer.GetCacheHandler().(*ecache.DefaultCacheHandler) 205 peakSize := cacheHandler.PeakCacheSize() 206 mbSize := float64(peakSize) / 1024 / 1024 207 fmt.Printf("Cache size after pre-caching: %d bytes (%.2fMB)\n", peakSize, mbSize) 208 }