github.com/kintar/etxt@v0.0.9/emask/shape.go (about) 1 package emask 2 3 import "image" 4 import "image/color" 5 6 import "golang.org/x/image/font/sfnt" 7 import "golang.org/x/image/math/fixed" 8 9 // TODO: add some ArcTo method to draw quarter circles based on 10 // cubic bézier curves? so we can (from 0, 0) ArcTo(0, 10, 10, 10) 11 // instead of CubeTo(0, 5, 5, 10, 10, 10) 12 13 // A helper type to assist the creation of shapes that can later be 14 // converted to [sfnt.Segments] and rasterized with the [Rasterize]() method. 15 // Notice that this is actually unrelated to fonts, but once you have some 16 // rasterizers it's nice to have a way to play with them manually. Notice 17 // also that since [Rasterize]() is a CPU process, working with big shapes 18 // (based on their bounding rectangle) can be quite expensive. 19 // 20 // Despite what the names of the methods might lead you to believe, 21 // shapes are not created by "drawing lines", but rather by defining 22 // a set of boundaries that enclose an area. If you get unexpected 23 // results using shapes, come back to think about this. 24 // 25 // Shapes by themselves do not care about the direction you use to define 26 // the segments (clockwise/counter-clockwise), but rasterizers that use 27 // the segments most often do. For example, if you define two squares one 28 // inside the other, both in the same order (e.g: top-left to top-right, 29 // top-right to bottom right...) the rasterized result will be a single 30 // square. If you define them following opposite directions, instead, 31 // the result will be the difference between the two squares. 32 // 33 // [sfnt.Segments]: https://pkg.go.dev/golang.org/x/image/font/sfnt#Segments 34 type Shape struct { 35 segments []sfnt.Segment 36 invertY bool // but rasterizers already invert coords, so this is negated 37 } 38 39 // Creates a new Shape object. The commandsCount is used to 40 // indicate the initial capacity of its internal segments buffer. 41 func NewShape(commandsCount int) Shape { 42 return Shape{ 43 segments: make([]sfnt.Segment, 0, commandsCount), 44 invertY: false, 45 } 46 } 47 48 // Returns whether [Shape.InvertY] is active or inactive. 49 func (self *Shape) HasInvertY() bool { return self.invertY } 50 51 // Let's say you want to draw a triangle pointing up, similar to an 52 // "A". By default, you would move to (0, 0) and then draw lines to 53 // (k, 2*k), (2*k, 0) and back to (0, 0). 54 // 55 // If you set InvertY to true, the previous shape will draw a triangle 56 // pointing down instead, similar to a "V". This is a convenient flag 57 // that makes it easier to work on different contexts (e.g., font glyphs 58 // are defined with the ascenders going into the negative y plane). 59 // 60 // InvertY can also be used creatively or to switch between clockwise and 61 // counter-clockwise directions when drawing symmetrical shapes that have 62 // their center at (0, 0). 63 func (self *Shape) InvertY(active bool) { self.invertY = active } 64 65 // Gets the shape information as [sfnt.Segments]. The underlying data 66 // is referenced both by the Shape and the sfnt.Segments, so be 67 // careful what you do with it. 68 // 69 // [sfnt.Segments]: https://pkg.go.dev/golang.org/x/image/font/sfnt#Segments 70 func (self *Shape) Segments() sfnt.Segments { 71 return sfnt.Segments(self.segments) 72 } 73 74 // Moves the current position to (x, y). 75 // See [golang.org/x/image/vector.Rasterizer] operations and 76 // [golang.org/x/image/font/sfnt.Segment]. 77 func (self *Shape) MoveTo(x, y int) { 78 self.MoveToFract(fixed.Int26_6(x<<6), fixed.Int26_6(y<<6)) 79 } 80 81 // Like [Shape.MoveTo], but with fixed.Int26_6 coordinates. 82 func (self *Shape) MoveToFract(x, y fixed.Int26_6) { 83 if !self.invertY { 84 y = -y 85 } 86 self.segments = append(self.segments, 87 sfnt.Segment{ 88 Op: sfnt.SegmentOpMoveTo, 89 Args: [3]fixed.Point26_6{ 90 fixed.Point26_6{x, y}, 91 fixed.Point26_6{}, 92 fixed.Point26_6{}, 93 }, 94 }) 95 } 96 97 // Creates a straight boundary from the current position to (x, y). 98 // See [golang.org/x/image/vector.Rasterizer] operations and 99 // [golang.org/x/image/font/sfnt.Segment]. 100 func (self *Shape) LineTo(x, y int) { 101 self.LineToFract(fixed.Int26_6(x<<6), fixed.Int26_6(y<<6)) 102 } 103 104 // Like [Shape.LineTo], but with fixed.Int26_6 coordinates. 105 func (self *Shape) LineToFract(x, y fixed.Int26_6) { 106 if !self.invertY { 107 y = -y 108 } 109 self.segments = append(self.segments, 110 sfnt.Segment{ 111 Op: sfnt.SegmentOpLineTo, 112 Args: [3]fixed.Point26_6{ 113 fixed.Point26_6{x, y}, 114 fixed.Point26_6{}, 115 fixed.Point26_6{}, 116 }, 117 }) 118 } 119 120 // Creates a quadratic Bézier curve (also known as a conic Bézier curve) to 121 // (x, y) with (ctrlX, ctrlY) as the control point. 122 // See [golang.org/x/image/vector.Rasterizer] operations and 123 // [golang.org/x/image/font/sfnt.Segment]. 124 func (self *Shape) QuadTo(ctrlX, ctrlY, x, y int) { 125 self.QuadToFract( 126 fixed.Int26_6(ctrlX<<6), fixed.Int26_6(ctrlY<<6), 127 fixed.Int26_6(x<<6), fixed.Int26_6(y<<6)) 128 } 129 130 // Like [Shape.QuadTo], but with fixed.Int26_6 coordinates. 131 func (self *Shape) QuadToFract(ctrlX, ctrlY, x, y fixed.Int26_6) { 132 if !self.invertY { 133 ctrlY, y = -ctrlY, -y 134 } 135 self.segments = append(self.segments, 136 sfnt.Segment{ 137 Op: sfnt.SegmentOpQuadTo, 138 Args: [3]fixed.Point26_6{ 139 fixed.Point26_6{ctrlX, ctrlY}, 140 fixed.Point26_6{x, y}, 141 fixed.Point26_6{}, 142 }, 143 }) 144 } 145 146 // Creates a cubic Bézier curve to (x, y) with (cx1, cy1) and (cx2, cy2) 147 // as the control points. 148 // See [golang.org/x/image/vector.Rasterizer] operations and 149 // [golang.org/x/image/font/sfnt.Segment]. 150 func (self *Shape) CubeTo(cx1, cy1, cx2, cy2, x, y int) { 151 self.CubeToFract( 152 fixed.Int26_6(cx1<<6), fixed.Int26_6(cy1<<6), 153 fixed.Int26_6(cx2<<6), fixed.Int26_6(cy2<<6), 154 fixed.Int26_6(x<<6), fixed.Int26_6(y<<6)) 155 } 156 157 // Like [Shape.CubeTo], but with fixed.Int26_6 coordinates. 158 func (self *Shape) CubeToFract(cx1, cy1, cx2, cy2, x, y fixed.Int26_6) { 159 if !self.invertY { 160 cy1, cy2, y = -cy1, -cy2, -y 161 } 162 self.segments = append(self.segments, 163 sfnt.Segment{ 164 Op: sfnt.SegmentOpCubeTo, 165 Args: [3]fixed.Point26_6{ 166 fixed.Point26_6{cx1, cy1}, 167 fixed.Point26_6{cx2, cy2}, 168 fixed.Point26_6{x, y}, 169 }, 170 }) 171 } 172 173 // Resets the shape segments. Be careful to not be holding the segments 174 // from [Shape.Segments]() when calling this (they may be overriden soon). 175 func (self *Shape) Reset() { self.segments = self.segments[0:0] } 176 177 // A helper method to rasterize the current shape with the default 178 // rasterizer. You could then export the result to a png file, e.g.: 179 // 180 // file, _ := os.Create("my_ugly_shape.png") 181 // _ = png.Encode(file, shape.Paint(color.White, color.Black)) 182 // // ...maybe even checking errors and closing the file ;) 183 func (self *Shape) Paint(drawColor, backColor color.Color) *image.RGBA { 184 segments := self.Segments() 185 if len(segments) == 0 { 186 return nil 187 } 188 mask, err := Rasterize(segments, &DefaultRasterizer{}, fixed.P(0, 0)) 189 if err != nil { 190 panic(err) 191 } // default rasterizer doesn't return errors 192 rgba := image.NewRGBA(mask.Rect) 193 194 r, g, b, a := drawColor.RGBA() 195 nrgba := color.NRGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: 0} 196 for y := mask.Rect.Min.Y; y < mask.Rect.Max.Y; y++ { 197 for x := mask.Rect.Min.X; x < mask.Rect.Max.X; x++ { 198 nrgba.A = uint16((a * uint32(mask.AlphaAt(x, y).A)) / 255) 199 rgba.Set(x, y, mixColors(nrgba, backColor)) 200 } 201 } 202 return rgba 203 } 204 205 // Helper method for Shape.Paint. The same as mixOverFunc 206 // (defined at etxt/ebiten_no.go) on the generic version of etxt. 207 func mixColors(draw color.Color, back color.Color) color.Color { 208 dr, dg, db, da := draw.RGBA() 209 if da == 0xFFFF { 210 return draw 211 } 212 if da == 0 { 213 return back 214 } 215 br, bg, bb, ba := back.RGBA() 216 if ba == 0 { 217 return draw 218 } 219 return color.RGBA64{ 220 R: uint16N((dr*0xFFFF + br*(0xFFFF-da)) / 0xFFFF), 221 G: uint16N((dg*0xFFFF + bg*(0xFFFF-da)) / 0xFFFF), 222 B: uint16N((db*0xFFFF + bb*(0xFFFF-da)) / 0xFFFF), 223 A: uint16N((da*0xFFFF + ba*(0xFFFF-da)) / 0xFFFF), 224 } 225 } 226 227 // clamping from uint32 to uint16 values 228 func uint16N(value uint32) uint16 { 229 if value > 65535 { 230 return 65535 231 } 232 return uint16(value) 233 }