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  }