github.com/kintar/etxt@v0.0.9/renderer_props.go (about)

     1  package etxt
     2  
     3  import "strconv"
     4  import "image/color"
     5  
     6  import "golang.org/x/image/math/fixed"
     7  import "golang.org/x/image/font"
     8  import "golang.org/x/image/font/sfnt"
     9  
    10  import "github.com/kintar/etxt/efixed"
    11  import "github.com/kintar/etxt/emask"
    12  import "github.com/kintar/etxt/ecache"
    13  import "github.com/kintar/etxt/esizer"
    14  
    15  // This file contains the Renderer type definition and all the
    16  // getter and setter methods. Actual operations are split in other
    17  // files.
    18  
    19  // The [Renderer] is the main type for drawing text provided by etxt.
    20  //
    21  // Renderers allow you to control font, text size, color, text
    22  // alignment and more from a single place.
    23  //
    24  // Basic usage goes like this:
    25  //   - Create, configure and store a renderer.
    26  //   - Use [Renderer.SetTarget]() and then [Renderer.Draw]() as many times
    27  //     as needed.
    28  //   - Re-adjust the properties of the renderer if needed... and keep drawing!
    29  //
    30  // If you need more advice or guidance, check the [renderers document]
    31  // and the [examples].
    32  //
    33  // [renderers document]: https://github.com/kintar/etxt/blob/main/docs/renderer.md
    34  // [examples]: https://github.com/kintar/etxt/tree/main/examples
    35  type Renderer struct {
    36  	font      *Font
    37  	target    TargetImage
    38  	mainColor color.Color
    39  	sizer     esizer.Sizer
    40  
    41  	vertAlign           VertAlign
    42  	horzAlign           HorzAlign
    43  	direction           Direction
    44  	lineAdvanceIsCached bool
    45  
    46  	horzQuantStep uint8
    47  	vertQuantStep uint8
    48  	_             uint8
    49  	mixMode       MixMode
    50  
    51  	sizePx            fixed.Int26_6
    52  	lineSpacing       fixed.Int26_6 // 64 by default (which is 1 in 26_6)
    53  	lineHeight        fixed.Int26_6 // non-negative value or -1 to match font height
    54  	cachedLineAdvance fixed.Int26_6
    55  
    56  	cacheHandler ecache.GlyphCacheHandler
    57  	rasterizer   emask.Rasterizer
    58  	metrics      *font.Metrics // cached for the current font and size
    59  	buffer       sfnt.Buffer
    60  }
    61  
    62  // Creates a new renderer with the default vector rasterizer.
    63  // See [NewRenderer]() documentation for more details.
    64  func NewStdRenderer() *Renderer {
    65  	return NewRenderer(&emask.DefaultRasterizer{})
    66  }
    67  
    68  // Creates a new renderer with the given glyph mask rasterizer.
    69  // For the default rasterizer, see [NewStdRenderer]() instead.
    70  //
    71  // After creating a renderer, you must set at least the font and
    72  // the target in order to be able to draw. In most cases, you will
    73  // also want to set a cache handler and a color. Check the setter
    74  // functions for more details on all those.
    75  //
    76  // Renderers are not safe for concurrent use.
    77  func NewRenderer(rasterizer emask.Rasterizer) *Renderer {
    78  	return &Renderer{
    79  		sizer:         &esizer.DefaultSizer{},
    80  		vertAlign:     Baseline,
    81  		horzAlign:     Left,
    82  		direction:     LeftToRight,
    83  		horzQuantStep: 64,
    84  		vertQuantStep: 64,
    85  		lineSpacing:   fixed.Int26_6(1 << 6),
    86  		lineHeight:    -1,
    87  		sizePx:        fixed.I(16),
    88  		rasterizer:    rasterizer,
    89  		mainColor:     color.RGBA{255, 255, 255, 255},
    90  		mixMode:       defaultMixMode,
    91  	}
    92  }
    93  
    94  // Sets the font to be used on subsequent operations.
    95  // Make sure to set one before starting to draw!
    96  func (self *Renderer) SetFont(font *Font) {
    97  	// Notice: you *can* call this function with a nil font, but
    98  	//         only if you *really really have to ensure* that the
    99  	//         font can be released by the garbage collector while
   100  	//         this renderer still exists... which is almost never.
   101  	if font == self.font {
   102  		return
   103  	}
   104  	self.font = font
   105  	if self.cacheHandler != nil {
   106  		self.cacheHandler.NotifyFontChange(font)
   107  	}
   108  
   109  	// drop cached information
   110  	self.lineAdvanceIsCached = false
   111  	self.metrics = nil
   112  }
   113  
   114  // Returns the current font. The font is nil by default.
   115  func (self *Renderer) GetFont() *Font { return self.font }
   116  
   117  // Sets the target of subsequent operations.
   118  // Attempting to draw without a target will cause the
   119  // renderer to panic.
   120  //
   121  // You can also clear the target by setting it to nil once
   122  // it's no longer needed.
   123  func (self *Renderer) SetTarget(target TargetImage) {
   124  	self.target = target
   125  }
   126  
   127  // Sets the mix mode to be used on subsequent operations.
   128  // The default mix mode will compose glyphs over the active
   129  // target with regular alpha blending.
   130  func (self *Renderer) SetMixMode(mixMode MixMode) {
   131  	self.mixMode = mixMode
   132  }
   133  
   134  // Sets the color to be used on subsequent draw operations.
   135  // The default color is white.
   136  func (self *Renderer) SetColor(mainColor color.Color) {
   137  	self.mainColor = mainColor
   138  }
   139  
   140  // Returns the current drawing color.
   141  func (self *Renderer) GetColor() color.Color { return self.mainColor }
   142  
   143  // Sets the granularity of the glyph position quantization applied to
   144  // rendering and measurement operations, in 1/64th parts of a pixel.
   145  //
   146  // At minimum granularity (step = 1), glyphs will be laid out
   147  // without any changes to their advances and kerns, fully respecting
   148  // the font's intended spacing and flow.
   149  //
   150  // At maximum granularity (step = 64), glyphs will be effectively
   151  // quantized to the pixel grid instead. This is the default value.
   152  //
   153  // The higher the precision, the higher the pressure on the glyph cache
   154  // (glyphs may have to be cached at many more different fractional
   155  // pixel positions).
   156  //
   157  // Recommended step values come from the formula ceil(64/N): 64, 32,
   158  // 22, 16, 13, 11, 10, 8, 7, 6, 5, 4, 3, 2, 1.
   159  //
   160  // For more details, read the [quantization document].
   161  //
   162  // [quantization document]: https://github.com/kintar/etxt/blob/main/docs/quantization.md
   163  func (self *Renderer) SetQuantizerStep(horzStep fixed.Int26_6, vertStep fixed.Int26_6) {
   164  	if horzStep < 1 || horzStep > 64 {
   165  		panic("horzStep outside the [1, 64] range")
   166  	}
   167  	if vertStep < 1 || vertStep > 64 {
   168  		panic("vertStep outside the [1, 64] range")
   169  	}
   170  	self.horzQuantStep = uint8(horzStep)
   171  	self.vertQuantStep = uint8(vertStep)
   172  }
   173  
   174  // Ask for it if you need it.
   175  // func (self *Renderer) GetQuantizerStep() (horzStep, vertStep fixed.Int26_6) {
   176  // 	return self.horzQuantStep, self.vertQuantStep
   177  // }
   178  
   179  // Returns the current glyph cache handler, which is nil by default.
   180  //
   181  // Rarely used unless you are examining the cache handler manually.
   182  func (self *Renderer) GetCacheHandler() ecache.GlyphCacheHandler {
   183  	return self.cacheHandler
   184  }
   185  
   186  // Sets the glyph cache handler used by the renderer. By default,
   187  // no cache is used, but you almost always want to set one, e.g.:
   188  //
   189  //	cache := etxt.NewDefaultCache(16*1024*1024) // 16MB
   190  //	textRenderer.SetCacheHandler(cache.NewHandler())
   191  //
   192  // A cache handler can only be used with a single renderer, but you
   193  // can create multiple handlers from the same underlying cache and
   194  // use them with multiple renderers.
   195  func (self *Renderer) SetCacheHandler(cacheHandler ecache.GlyphCacheHandler) {
   196  	self.cacheHandler = cacheHandler
   197  
   198  	if cacheHandler == nil {
   199  		if self.rasterizer != nil {
   200  			self.rasterizer.SetOnChangeFunc(nil)
   201  		}
   202  		return
   203  	}
   204  
   205  	if self.rasterizer != nil {
   206  		self.rasterizer.SetOnChangeFunc(cacheHandler.NotifyRasterizerChange)
   207  	}
   208  
   209  	cacheHandler.NotifySizeChange(self.sizePx)
   210  	if self.font != nil {
   211  		cacheHandler.NotifyFontChange(self.font)
   212  	}
   213  	if self.rasterizer != nil {
   214  		cacheHandler.NotifyRasterizerChange(self.rasterizer)
   215  	}
   216  }
   217  
   218  // Returns the current glyph mask rasterizer.
   219  //
   220  // This function is only useful when working with configurable rasterizers;
   221  // ignore it if you are using the default glyph mask rasterizer.
   222  //
   223  // Mask rasterizers are not concurrent-safe, so be careful with
   224  // what you do and where you put them.
   225  func (self *Renderer) GetRasterizer() emask.Rasterizer {
   226  	return self.rasterizer
   227  }
   228  
   229  // Sets the glyph mask rasterizer to be used on subsequent operations.
   230  func (self *Renderer) SetRasterizer(rasterizer emask.Rasterizer) {
   231  	// clear rasterizer onChangeFunc
   232  	if self.rasterizer != nil {
   233  		self.rasterizer.SetOnChangeFunc(nil)
   234  	}
   235  
   236  	// set rasterizer
   237  	self.rasterizer = rasterizer
   238  
   239  	// link new rasterizer to the cache handler
   240  	if rasterizer != nil {
   241  		if self.cacheHandler == nil {
   242  			rasterizer.SetOnChangeFunc(nil)
   243  		} else {
   244  			rasterizer.SetOnChangeFunc(self.cacheHandler.NotifyRasterizerChange)
   245  			self.cacheHandler.NotifyRasterizerChange(rasterizer)
   246  		}
   247  	}
   248  }
   249  
   250  // Sets the font size to be used on subsequent operations.
   251  //
   252  // Sizes are given in pixels and must be >= 1.
   253  // By default, the renderer will draw text at a size of 16px.
   254  //
   255  // The relationship between font size and the size of its glyphs
   256  // is complicated and can vary a lot between fonts, but
   257  // to provide a [general reference]:
   258  //   - A capital latin letter is usually around 70% as tall as
   259  //     the given size. E.g.: at 16px, "A" will be 10-12px tall.
   260  //   - A lowercase latin letter is usually around 48% as tall as
   261  //     the given size. E.g.: at 16px, "x" will be 7-9px tall.
   262  //
   263  // [general reference]: https://github.com/kintar/etxt/blob/main/docs/px-size.md
   264  func (self *Renderer) SetSizePx(sizePx int) {
   265  	self.SetSizePxFract(fixed.Int26_6(sizePx << 6))
   266  }
   267  
   268  // Like [Renderer.SetSizePx], but accepting a float64 fractional pixel size.
   269  // func (self *Renderer) SetSizePxFloat(sizePx float64) {
   270  // 	self.SetSizePxFract(efixed.FromFloat64RoundToZero(sizePx))
   271  // }
   272  
   273  // Like [Renderer.SetSizePx], but accepting a fractional pixel size in
   274  // the form of a [26.6 fixed point] integer.
   275  //
   276  // [26.6 fixed point]: https://github.com/kintar/etxt/blob/main/docs/fixed-26-6.md
   277  func (self *Renderer) SetSizePxFract(sizePx fixed.Int26_6) {
   278  	if sizePx < 64 {
   279  		panic("sizePx must be >= 1")
   280  	}
   281  	if sizePx == self.sizePx {
   282  		return
   283  	}
   284  
   285  	// set new size and check it's in range
   286  	// (we are artificially limiting sizes so glyphs don't take
   287  	// more than ~1GB on ebiten or ~0.25GB as alpha images. Even
   288  	// at those levels most computers will choke to death if they
   289  	// try to render multiple characters, but I tried...)
   290  	self.sizePx = sizePx
   291  	if self.sizePx & ^fixed.Int26_6(0x000FFFFF) != 0 {
   292  		panic("sizePx " + strconv.FormatFloat(float64(sizePx)/64, 'f', 2, 64) + " too big")
   293  	}
   294  
   295  	// notify update to the cacheHandler
   296  	if self.cacheHandler != nil {
   297  		self.cacheHandler.NotifySizeChange(sizePx)
   298  	}
   299  
   300  	// drop cached information
   301  	self.lineAdvanceIsCached = false
   302  	self.metrics = nil
   303  }
   304  
   305  // Returns the current font size as a [fixed.Int26_6].
   306  //
   307  // [fixed.Int26_6]: https://github.com/kintar/etxt/blob/main/docs/fixed-26-6.md
   308  func (self *Renderer) GetSizePxFract() fixed.Int26_6 {
   309  	return self.sizePx
   310  }
   311  
   312  // Sets the line height to be used on subsequent operations.
   313  //
   314  // Line height is only used when line breaks are found in the input
   315  // text to be processed. Notice that line spacing will also affect the
   316  // space between lines of text (lineAdvance = lineHeight*lineSpacing).
   317  //
   318  // The units are pixels, not points, and only non-negative values
   319  // are allowed. If you need negative line heights for some reason,
   320  // use negative line spacing factors instead.
   321  //
   322  // By default, the line height is set to auto (see
   323  // [Renderer.SetLineHeightAuto]()).
   324  func (self *Renderer) SetLineHeight(heightPx float64) {
   325  	if heightPx < 0 {
   326  		panic("negative line height not allowed, use negative line spacing instead")
   327  	}
   328  	// See SetLineSpacing notes a couple methods below.
   329  	self.lineHeight = efixed.FromFloat64RoundToZero(heightPx)
   330  	self.lineAdvanceIsCached = false
   331  }
   332  
   333  // Sets the line height to automatically match the height of the
   334  // active font and size. This is the default behavior for line
   335  // height.
   336  //
   337  // For manual line height configuration, see [Renderer.SetLineHeight]().
   338  func (self *Renderer) SetLineHeightAuto() {
   339  	self.lineHeight = -1
   340  	self.lineAdvanceIsCached = false
   341  }
   342  
   343  // Sets the line spacing to be used on subsequent operations.
   344  // By default, the line spacing factor is 1.0.
   345  //
   346  // Line spacing is only applied when line breaks are found in the
   347  // input text to be processed.
   348  //
   349  // Notice that line spacing and line height are different things.
   350  // See [Renderer.SetLineHeight]() for more details.
   351  func (self *Renderer) SetLineSpacing(factor float64) {
   352  	// Line spacing will be quantized to a multiple of 1/64.
   353  	// Providing a float64 that already adjusts to that will
   354  	// guarantee a conversion that always takes the "fast" path.
   355  	// You shouldn't worry too much about this, though.
   356  	self.lineSpacing = efixed.FromFloat64RoundToZero(factor)
   357  	self.lineAdvanceIsCached = false
   358  }
   359  
   360  // Returns the result of lineHeight*lineSpacing. This is a low level
   361  // function rarely needed unless you are drawing lines one by one and
   362  // setting their y coordinate manually.
   363  //
   364  // The result is always unquantized and cached.
   365  //
   366  // For more context, see [Renderer.SetLineHeight]() and [Renderer.SetLineSpacing]().
   367  func (self *Renderer) GetLineAdvance() fixed.Int26_6 {
   368  	if self.lineAdvanceIsCached {
   369  		return self.cachedLineAdvance
   370  	}
   371  
   372  	var newLineAdvance fixed.Int26_6
   373  	if self.lineHeight == -1 { // auto mode (match font height)
   374  		if self.metrics == nil {
   375  			self.updateMetrics()
   376  		}
   377  		if self.lineSpacing == 64 { // fast common case
   378  			newLineAdvance = self.metrics.Height
   379  		} else {
   380  			// TODO: fixed.Int26_6.Mul() implementation is biased (rounding up).
   381  			//       See: https://go.dev/play/p/UCzCWSBPesH
   382  			//       In our case, only one value can be negative (lineSpacing),
   383  			//       see if the current approach is biased or determine what's
   384  			//       the appropriate bias (maybe round down on negative)
   385  			newLineAdvance = fixed.Int26_6((int64(self.metrics.Height) * int64(self.lineSpacing)) >> 6)
   386  		}
   387  	} else { // manual mode, use stored line height
   388  		if self.lineSpacing == 64 { // fast case
   389  			newLineAdvance = self.lineHeight
   390  		} else {
   391  			newLineAdvance = fixed.Int26_6((int64(self.lineHeight) * int64(self.lineSpacing)) >> 6)
   392  		}
   393  	}
   394  
   395  	self.cachedLineAdvance = newLineAdvance
   396  	self.lineAdvanceIsCached = true
   397  	return newLineAdvance
   398  }
   399  
   400  // See documentation for [Renderer.SetAlign]().
   401  func (self *Renderer) SetVertAlign(vertAlign VertAlign) {
   402  	if vertAlign < Top || vertAlign > Bottom {
   403  		panic("bad VertAlign")
   404  	}
   405  	self.vertAlign = vertAlign
   406  }
   407  
   408  // See documentation for [Renderer.SetAlign]().
   409  func (self *Renderer) SetHorzAlign(horzAlign HorzAlign) {
   410  	if horzAlign < Left || horzAlign > Right {
   411  		panic("bad HorzAlign")
   412  	}
   413  	self.horzAlign = horzAlign
   414  }
   415  
   416  // Configures how [Renderer.Draw]*() coordinates will be interpreted. For example:
   417  //   - If the alignment is set to (etxt.[Top], etxt.[Left]), coordinates
   418  //     passed to subsequent operations will be interpreted as the
   419  //     top-left corner of the box in which the text has to be drawn.
   420  //   - If the alignment is set to (etxt.[YCenter], etxt.[XCenter]), coordinates
   421  //     passed to subsequent operations will be interpreted
   422  //     as the center of the box in which the text has to be drawn.
   423  //
   424  // Check out [this image] for a visual explanation instead.
   425  //
   426  // By default, the renderer's alignment is (etxt.[Baseline], etxt.[Left]).
   427  //
   428  // [this image]: https://github.com/kintar/etxt/blob/main/docs/img/gtxt_aligns.png
   429  func (self *Renderer) SetAlign(vertAlign VertAlign, horzAlign HorzAlign) {
   430  	self.SetVertAlign(vertAlign)
   431  	self.SetHorzAlign(horzAlign)
   432  }
   433  
   434  // Returns the current align. See [Renderer.SetAlign]() documentation for
   435  // more details on text align.
   436  func (self *Renderer) GetAlign() (VertAlign, HorzAlign) {
   437  	return self.vertAlign, self.horzAlign
   438  }
   439  
   440  // Sets the text direction to be used on subsequent operations.
   441  //
   442  // By default, the direction is [LeftToRight].
   443  func (self *Renderer) SetDirection(dir Direction) {
   444  	if dir != LeftToRight && dir != RightToLeft {
   445  		panic("bad direction")
   446  	}
   447  	self.direction = dir
   448  }
   449  
   450  // Returns the current Sizer. You shouldn't worry about sizers unless
   451  // you are making custom glyph mask rasterizers or want to disable
   452  // kerning or adjust spacing in some other unusual way.
   453  func (self *Renderer) GetSizer() esizer.Sizer {
   454  	return self.sizer
   455  }
   456  
   457  // Sets the current sizer, which must be non-nil.
   458  //
   459  // As [Renderer.GetSizer]() documentation explains, you rarely
   460  // need to care about or even know what sizers are.
   461  func (self *Renderer) SetSizer(sizer esizer.Sizer) {
   462  	if sizer == nil {
   463  		panic("nil sizer")
   464  	}
   465  	self.sizer = sizer
   466  }
   467  
   468  // --- helper methods ---
   469  func (self *Renderer) updateMetrics() {
   470  	metrics := self.sizer.Metrics(self.font, self.sizePx)
   471  	self.metrics = &metrics
   472  }