github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/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 }