github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/emask/impl_edge_marker.go (about)

     1  package emask
     2  
     3  import "math"
     4  import "image"
     5  
     6  import "golang.org/x/image/math/fixed"
     7  import "golang.org/x/image/font/sfnt"
     8  
     9  // TODO: actually, the default value won't have the cache signature
    10  //       set properly. Don't recommend using the zero value, and
    11  //       use NewEdgeMarkerRasterizer instead... unless you explicitly
    12  //       set the curve threshold and the max splits.
    13  
    14  // An alternative to [DefaultRasterizer] that avoids using
    15  // [golang.org/x/image/vector.Rasterizer] under the hood. Results are
    16  // visually very similar, but performance is 3 times worse.
    17  //
    18  // The purpose of this rasterizer is to offer a simpler, more
    19  // readable and [well-documented] version of the algorithm used by
    20  // vector.Rasterizer that anyone can edit, adapt or learn from.
    21  //
    22  // The zero-value is usable but will produce jaggy results, as curve
    23  // segmentation parameters are not configured. Use [NewStdEdgeMarkerRasterizer]()
    24  // if you prefer a pre-configured rasterizer. You may also configure the
    25  // rasterizer manually through [EdgeMarkerRasterizer.SetCurveThreshold]() and
    26  // [EdgeMarkerRasterizer.SetMaxCurveSplits]().
    27  //
    28  // [well-documented]: https://github.com/Kintar/etxt/blob/main/docs/rasterize-outlines.md
    29  type EdgeMarkerRasterizer struct {
    30  	// All relevant algorithms are implemented inside the unexported
    31  	// edgeMarker type (see emask/edge_marker.go), except for final
    32  	// buffer accumulation which is done directly on the Rasterize()
    33  	// method. The rest is only a wrapper to comply with the
    34  	// emask.Rasterizer interface.
    35  	rasterizer     edgeMarker
    36  	onChange       func(Rasterizer)
    37  	cacheSignature uint64
    38  	rectOffset     image.Point
    39  	normOffset     fixed.Point26_6
    40  }
    41  
    42  // Creates a new [EdgeMarkerRasterizer] with reasonable default values.
    43  func NewStdEdgeMarkerRasterizer() *EdgeMarkerRasterizer {
    44  	rast := &EdgeMarkerRasterizer{}
    45  	rast.SetCurveThreshold(0.1)
    46  	rast.SetMaxCurveSplits(8) // this is excessive for most glyph rendering
    47  	return rast
    48  }
    49  
    50  // Satisfies the [UserCfgCacheSignature] interface.
    51  func (self *EdgeMarkerRasterizer) SetHighByte(value uint8) {
    52  	self.cacheSignature = uint64(value) << 56
    53  	if self.onChange != nil {
    54  		self.onChange(self)
    55  	}
    56  }
    57  
    58  // Sets the threshold distance to use when splitting Bézier curves into
    59  // linear segments. If a linear segment misses the curve by more than
    60  // the threshold value, the curve will be split. Otherwise, the linear
    61  // segment will be used to approximate it.
    62  //
    63  // Values very close to zero could prevent the algorithm from converging
    64  // due to floating point instability, but the MaxCurveSplits cutoff will
    65  // prevent infinite looping anyway.
    66  //
    67  // Reasonable values range from 0.01 to 1.0. [NewStdEdgeMarkerRasterizer]()
    68  // uses 0.1 by default.
    69  func (self *EdgeMarkerRasterizer) SetCurveThreshold(threshold float32) {
    70  	self.rasterizer.CurveSegmenter.SetThreshold(threshold)
    71  	bits := math.Float32bits(threshold)
    72  	self.cacheSignature &= 0xFFFFFFFF00000000
    73  	self.cacheSignature |= uint64(bits)
    74  	if self.onChange != nil {
    75  		self.onChange(self)
    76  	}
    77  }
    78  
    79  // Sets the maximum amount of times a curve can be recursively split
    80  // into subsegments while trying to approximate it.
    81  //
    82  // The maximum number of segments that will approximate a curve is
    83  // 2^maxCurveSplits.
    84  //
    85  // This value is typically used as a cutoff to prevent low curve thresholds
    86  // from making the curve splitting process too slow, but it can also be used
    87  // creatively to get jaggy results instead of smooth curves.
    88  //
    89  // Values outside the [0, 255] range will be silently clamped. Reasonable
    90  // values range from 0 to 10. [NewStdEdgeMarkerRasterizer]() uses 8 by default.
    91  func (self *EdgeMarkerRasterizer) SetMaxCurveSplits(maxCurveSplits int) {
    92  	segmenter := &self.rasterizer.CurveSegmenter
    93  	segmenter.SetMaxSplits(maxCurveSplits)
    94  	self.cacheSignature &= 0xFFFFFF00FFFFFFFF
    95  	self.cacheSignature |= uint64(segmenter.maxCurveSplits) << 32
    96  	if self.onChange != nil {
    97  		self.onChange(self)
    98  	}
    99  }
   100  
   101  // Satisfies the [Rasterizer] interface.
   102  func (self *EdgeMarkerRasterizer) SetOnChangeFunc(onChange func(Rasterizer)) {
   103  	self.onChange = onChange
   104  }
   105  
   106  // Satisfies the [Rasterizer] interface.
   107  func (self *EdgeMarkerRasterizer) CacheSignature() uint64 {
   108  	self.cacheSignature &= 0xFF00FFFFFFFFFFFF
   109  	self.cacheSignature |= 0x00E6000000000000
   110  	return self.cacheSignature
   111  }
   112  
   113  // See [DefaultRasterizer.MoveTo]().
   114  func (self *EdgeMarkerRasterizer) MoveTo(point fixed.Point26_6) {
   115  	x, y := self.fixedToFloat64Coords(point)
   116  	self.rasterizer.MoveTo(x, y)
   117  }
   118  
   119  // See [DefaultRasterizer.LineTo]().
   120  func (self *EdgeMarkerRasterizer) LineTo(point fixed.Point26_6) {
   121  	x, y := self.fixedToFloat64Coords(point)
   122  	self.rasterizer.LineTo(x, y)
   123  }
   124  
   125  // See [DefaultRasterizer.QuadTo]().
   126  func (self *EdgeMarkerRasterizer) QuadTo(control, target fixed.Point26_6) {
   127  	cx, cy := self.fixedToFloat64Coords(control)
   128  	tx, ty := self.fixedToFloat64Coords(target)
   129  	self.rasterizer.QuadTo(cx, cy, tx, ty)
   130  }
   131  
   132  // See [DefaultRasterizer.CubeTo]().
   133  func (self *EdgeMarkerRasterizer) CubeTo(controlA, controlB, target fixed.Point26_6) {
   134  	cax, cay := self.fixedToFloat64Coords(controlA)
   135  	cbx, cby := self.fixedToFloat64Coords(controlB)
   136  	tx, ty := self.fixedToFloat64Coords(target)
   137  	self.rasterizer.CubeTo(cax, cay, cbx, cby, tx, ty)
   138  }
   139  
   140  // Satisfies the [Rasterizer] interface.
   141  func (self *EdgeMarkerRasterizer) Rasterize(outline sfnt.Segments, fract fixed.Point26_6) (*image.Alpha, error) {
   142  	// prepare rasterizer
   143  	var size image.Point
   144  	size, self.normOffset, self.rectOffset = figureOutBounds(outline.Bounds(), fract)
   145  	buffer := &self.rasterizer.Buffer
   146  	buffer.Resize(size.X, size.Y)
   147  
   148  	// process outline
   149  	processOutline(self, outline)
   150  
   151  	// allocate glyph mask and apply buffer accumulation
   152  	// (this takes around 50% of the time of the process)
   153  	mask := image.NewAlpha(image.Rect(0, 0, buffer.Width, buffer.Height))
   154  	buffer.AccumulateUint8(mask.Pix)
   155  
   156  	// translate the mask to its final position
   157  	mask.Rect = mask.Rect.Add(self.rectOffset)
   158  	return mask, nil
   159  }
   160  
   161  func (self *EdgeMarkerRasterizer) fixedToFloat64Coords(point fixed.Point26_6) (float64, float64) {
   162  	x := float64(point.X+self.normOffset.X) / 64
   163  	y := float64(point.Y+self.normOffset.Y) / 64
   164  	return x, y
   165  }