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