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

     1  package emask
     2  
     3  import "image"
     4  
     5  import "golang.org/x/image/math/fixed"
     6  import "golang.org/x/image/font/sfnt"
     7  
     8  import "github.com/kintar/etxt/efixed"
     9  
    10  // TODO: extension of joints must be limited through special mechanisms,
    11  //       otherwise they can go wild on some aberrant cases
    12  
    13  // NOTICE: work in progress. There are a couple big clipping issues
    14  // that will easily cause panics, and most likely still a few minor
    15  // bugs on polygon filling edge cases. Clipping is tricky due to Bézier
    16  // cusps and similar situations where two lines end up joining at
    17  // very tight angles, but also in some edge cases where natural path
    18  // intersections will go outside the intended paths area due to thickness
    19  // becoming larger than path length and multiple paths colliding and stuff.
    20  //
    21  // The algorithm is also quite slow and there's much room for improvement,
    22  // but I'm still focusing on the baseline implementation.
    23  //
    24  // In web API terms, the line cap is "butt" and the line joint is "miter".
    25  type OutlineRasterizer struct {
    26  	rasterizer     outliner
    27  	onChange       func(Rasterizer)
    28  	cacheSignature uint64
    29  	rectOffset     image.Point
    30  	normOffset     fixed.Point26_6
    31  }
    32  
    33  func NewOutlineRasterizer(outlineThickness float64) *OutlineRasterizer {
    34  	rast := &OutlineRasterizer{}
    35  	rast.SetThickness(outlineThickness)
    36  	rast.rasterizer.CurveSegmenter.SetThreshold(1 / 1024)
    37  	rast.rasterizer.CurveSegmenter.SetMaxSplits(8) // TODO: store somewhere
    38  	rast.SetMarginFactor(2.0)
    39  	return rast
    40  }
    41  
    42  // Satisfies the [UserCfgCacheSignature] interface.
    43  func (self *OutlineRasterizer) SetHighByte(value uint8) {
    44  	self.cacheSignature = uint64(value) << 56
    45  	if self.onChange != nil {
    46  		self.onChange(self)
    47  	}
    48  }
    49  
    50  // Sets the outline thickness. Values must be in the [0.1, 1024] range.
    51  func (self *OutlineRasterizer) SetThickness(thickness float64) {
    52  	thickness = self.rasterizer.SetThickness(thickness)
    53  	self.cacheSignature &= 0xFFFFFFFFFFF00000
    54  	self.cacheSignature |= uint64(thickness*1024) - 1
    55  	if self.onChange != nil {
    56  		self.onChange(self)
    57  	}
    58  }
    59  
    60  // When two lines of the outline connect at a tight angle, the resulting
    61  // vertex may extend far beyond 'thickness' distance. The margin factor
    62  // allows setting a limit, in multiples of 'thickness', to adjust the
    63  // paths so they don't extend further away than intended.
    64  //
    65  // The default value is 2. Valid values range from 1 to 16.
    66  func (self *OutlineRasterizer) SetMarginFactor(factor float64) {
    67  	// TODO: document how multiples provide coverage up to different
    68  	// angles. TODO: this is still causing panics.
    69  	self.rasterizer.SetMarginFactor(factor)
    70  }
    71  
    72  // Satisfies the [Rasterizer] interface.
    73  func (self *OutlineRasterizer) SetOnChangeFunc(onChange func(Rasterizer)) {
    74  	self.onChange = onChange
    75  }
    76  
    77  // Satisfies the [Rasterizer] interface.
    78  func (self *OutlineRasterizer) CacheSignature() uint64 {
    79  	self.cacheSignature &= 0xFF00FFFFFFFFFFFF
    80  	self.cacheSignature |= 0x0037000000000000
    81  	return self.cacheSignature
    82  }
    83  
    84  // Satisfies the unexported vectorTracer interface.
    85  func (self *OutlineRasterizer) MoveTo(point fixed.Point26_6) {
    86  	x, y := self.fixedToFloat64Coords(point)
    87  	self.rasterizer.MoveTo(x, y)
    88  }
    89  
    90  // Satisfies the unexported vectorTracer interface.
    91  func (self *OutlineRasterizer) LineTo(point fixed.Point26_6) {
    92  	x, y := self.fixedToFloat64Coords(point)
    93  	self.rasterizer.LineTo(x, y)
    94  }
    95  
    96  // Satisfies the unexported vectorTracer interface.
    97  func (self *OutlineRasterizer) QuadTo(control, target fixed.Point26_6) {
    98  	cx, cy := self.fixedToFloat64Coords(control)
    99  	tx, ty := self.fixedToFloat64Coords(target)
   100  	self.rasterizer.QuadTo(cx, cy, tx, ty)
   101  }
   102  
   103  // Satisfies the unexported vectorTracer interface.
   104  func (self *OutlineRasterizer) CubeTo(controlA, controlB, target fixed.Point26_6) {
   105  	cax, cay := self.fixedToFloat64Coords(controlA)
   106  	cbx, cby := self.fixedToFloat64Coords(controlB)
   107  	tx, ty := self.fixedToFloat64Coords(target)
   108  	self.rasterizer.CubeTo(cax, cay, cbx, cby, tx, ty)
   109  }
   110  
   111  // Satisfies the Rasterizer interface.
   112  func (self *OutlineRasterizer) Rasterize(outline sfnt.Segments, fract fixed.Point26_6) (*image.Alpha, error) {
   113  	// prepare rasterizer
   114  	var size image.Point
   115  	bounds := outline.Bounds()
   116  	margin := efixed.FromFloat64RoundAwayZero(self.rasterizer.MaxMargin())
   117  	bounds.Min.X -= margin
   118  	bounds.Max.X += margin
   119  	bounds.Min.Y -= margin
   120  	bounds.Max.Y += margin
   121  	size, self.normOffset, self.rectOffset = figureOutBounds(bounds, fract)
   122  	buffer := &self.rasterizer.Buffer
   123  	buffer.Resize(size.X, size.Y) // also clears the buffer
   124  	// TODO: use a buffer8 uint8 buffer and set manually
   125  	//       .Width, .Height and swap the .Buffer directly
   126  
   127  	// process outline
   128  	processOutline(self, outline)
   129  
   130  	// allocate glyph mask and move results from buffer
   131  	mask := image.NewAlpha(image.Rect(0, 0, buffer.Width, buffer.Height))
   132  	for i := 0; i < len(buffer.Values); i++ {
   133  		mask.Pix[i] = uint8(clampUnit64(buffer.Values[i]) * 255)
   134  	}
   135  
   136  	// translate the mask to its final position
   137  	mask.Rect = mask.Rect.Add(self.rectOffset)
   138  	return mask, nil
   139  }
   140  
   141  func (self *OutlineRasterizer) fixedToFloat64Coords(point fixed.Point26_6) (float64, float64) {
   142  	x := float64(point.X+self.normOffset.X) / 64
   143  	y := float64(point.Y+self.normOffset.Y) / 64
   144  	return x, y
   145  }