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 }