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

     1  package emask
     2  
     3  import "math"
     4  
     5  // edgeMarker implements a simplified and more readable version of the
     6  // algorithm used by vector.Rasterizer, providing access to the result of
     7  // the first rasterization step. The final accumulation process can be
     8  // seen in EdgeMarkerRasterizer's Rasterize() method too.
     9  //
    10  // The algorithms are documented and contextualized here:
    11  // >> https://github.com/Kintar/etxt/blob/main/docs/rasterize-outlines.md
    12  //
    13  // The zero value is usable, but curves will not be segmented smoothly.
    14  // You should manually SetCurveThreshold() and SetMaxCurveSplits() to
    15  // the desired values.
    16  type edgeMarker struct {
    17  	x              float64 // current drawing point position
    18  	y              float64 // current drawing point position
    19  	Buffer         buffer
    20  	CurveSegmenter curveSegmenter
    21  }
    22  
    23  func (self *edgeMarker) X() float64 { return self.x }
    24  func (self *edgeMarker) Y() float64 { return self.y }
    25  
    26  // Moves the current position to the given coordinates.
    27  func (self *edgeMarker) MoveTo(x, y float64) {
    28  	self.x = x
    29  	self.y = y
    30  }
    31  
    32  // Creates a straight boundary from the current position to the given
    33  // target and moves the current position to the new one.
    34  //
    35  // While the 'LineTo' name is used to stay consistent with other similar
    36  // interfaces, don't think in terms of "drawing lines"; we are defining
    37  // the boundaries of an outline.
    38  func (self *edgeMarker) LineTo(x, y float64) {
    39  	// This method is the core of edgeMarker. Additional context
    40  	// and explanations are available at docs/rasterize-outlines.md.
    41  
    42  	// changes in y equal or below this threshold are considered 0.
    43  	// this is bigger than 0 in order to account for floating point
    44  	// division unstability
    45  	const HorizontalityThreshold = 0.000001
    46  
    47  	// make sure to set the new current position at the end
    48  	defer self.MoveTo(x, y)
    49  
    50  	// get position increases in both axes
    51  	deltaX := x - self.x
    52  	deltaY := y - self.y
    53  
    54  	// if the y doesn't change, we are marking an horizontal boundary...
    55  	// but horizontal boundaries don't have to be marked
    56  	if math.Abs(deltaY) <= HorizontalityThreshold {
    57  		return
    58  	}
    59  	xAdvancePerY := deltaX / deltaY
    60  
    61  	// mark boundaries for every pixel that we pass through
    62  	for {
    63  		// get next whole position in the current direction
    64  		nextX := nextWholeCoord(self.x, deltaX)
    65  		nextY := nextWholeCoord(self.y, deltaY)
    66  
    67  		// check if we reached targets and clamp
    68  		atHorzTarget := hasReachedTarget(nextX, x, deltaX)
    69  		atVertTarget := hasReachedTarget(nextY, y, deltaY)
    70  		if atHorzTarget {
    71  			nextX = x
    72  		}
    73  		if atVertTarget {
    74  			nextY = y
    75  		}
    76  
    77  		// find distances to next coords
    78  		horzAdvance := nextX - self.x
    79  		vertAdvance := nextY - self.y
    80  
    81  		// determine which whole coordinate we reach first
    82  		// with the current line direction and position
    83  		altHorzAdvance := xAdvancePerY * vertAdvance
    84  		if math.Abs(altHorzAdvance) <= math.Abs(horzAdvance) {
    85  			// reach vertical whole coord first
    86  			horzAdvance = altHorzAdvance
    87  		} else {
    88  			// reach horizontal whole coord first
    89  			// (notice that here xAdvancePerY can't be 0)
    90  			vertAdvance = horzAdvance / xAdvancePerY
    91  		}
    92  
    93  		// mark the boundary segment traversing the vertical axis at
    94  		// the current pixel
    95  		self.markBoundary(self.x, self.y, horzAdvance, vertAdvance)
    96  
    97  		// update current position *(note 1)
    98  		self.x += horzAdvance
    99  		self.y += vertAdvance
   100  
   101  		// return if we reached the final position
   102  		if atHorzTarget && atVertTarget {
   103  			return
   104  		}
   105  	}
   106  
   107  	// *note 1: precision won't cause trouble here. Since we calculated
   108  	//          horizontal and vertical advances from a whole position,
   109  	//          once we re-add that difference we will reach the whole
   110  	//          position again if that's what has to happen.
   111  	// *note 2: notice that there are many optimizations that are not
   112  	//          being applied here. for example, vector.Rasterizer treats
   113  	//          the buffer as a continuous space instead of using rows
   114  	//          as boundaries, which allows for faster buffer accumulation
   115  	//          at a later stage (also using SIMD instructions). Many
   116  	//          other small optimizations are possible if you don't mind
   117  	//          hurting readability. for example, figuring out advance
   118  	//          directions and applying them directly instead of depending
   119  	//          on functions like nextWholeCoord. using float32 instead of
   120  	//          float64 can also speed up things.
   121  }
   122  
   123  // Creates a boundary from the current position to the given target
   124  // as a quadratic Bézier curve through the given control point and
   125  // moves the current position to the new one.
   126  func (self *edgeMarker) QuadTo(ctrlX, ctrlY, fx, fy float64) {
   127  	self.CurveSegmenter.TraceQuad(self.LineTo, self.x, self.y, ctrlX, ctrlY, fx, fy)
   128  }
   129  
   130  // Creates a boundary from the current position to the given target
   131  // as a cubic Bézier curve through the given control points and
   132  // moves the current position to the new one.
   133  func (self *edgeMarker) CubeTo(cx1, cy1, cx2, cy2, fx, fy float64) {
   134  	self.CurveSegmenter.TraceCube(self.LineTo, self.x, self.y, cx1, cy1, cx2, cy2, fx, fy)
   135  }
   136  
   137  // --- helper functions ---
   138  
   139  func (self *edgeMarker) markBoundary(x, y, horzAdvance, vertAdvance float64) {
   140  	// find the pixel position on which we have to mark the boundary
   141  	col := intFloorOfSegment(x, horzAdvance)
   142  	row := intFloorOfSegment(y, vertAdvance)
   143  
   144  	// stop if going outside bounds (except for negative
   145  	// x coords, which have to be applied anyway as they
   146  	// would accumulate)
   147  	if row < 0 || row >= self.Buffer.Height {
   148  		return
   149  	}
   150  	if col >= self.Buffer.Width {
   151  		return
   152  	}
   153  
   154  	// to mark the boundary, we have to see how much we have moved vertically,
   155  	// and accumulate that change into the relevant pixel(s). the vertAdvance
   156  	// tells us how much total change we have, but we also have to interpolate
   157  	// the change *through* the current pixel if it's not fully filled or
   158  	// unfilled exactly at the pixel boundary, marking the boundary through
   159  	// two pixels instead of only one
   160  
   161  	// edge case with negative columns. in this case, the whole change
   162  	// is applied to the first column of the affected row
   163  	if col < 0 {
   164  		self.Buffer.Values[row*self.Buffer.Width] += vertAdvance
   165  		return
   166  	}
   167  
   168  	// determine total and partial changes
   169  	totalChange := vertAdvance
   170  	var partialChange float64
   171  	if horzAdvance >= 0 {
   172  		partialChange = (1 - (x - math.Floor(x) + horzAdvance/2)) * vertAdvance
   173  	} else { // horzAdvance < 0
   174  		partialChange = (math.Ceil(x) - x - horzAdvance/2) * vertAdvance
   175  	}
   176  
   177  	// set the accumulator values
   178  	self.Buffer.Values[row*self.Buffer.Width+col] += partialChange
   179  	if col+1 < self.Buffer.Width {
   180  		self.Buffer.Values[row*self.Buffer.Width+col+1] += (totalChange - partialChange)
   181  	}
   182  }
   183  
   184  func hasReachedTarget(current float64, limit float64, deltaSign float64) bool {
   185  	if deltaSign >= 0 {
   186  		return current >= limit
   187  	}
   188  	return current <= limit
   189  }
   190  
   191  func nextWholeCoord(position float64, deltaSign float64) float64 {
   192  	if deltaSign == 0 {
   193  		return position
   194  	} // this works for *our* context
   195  	if deltaSign > 0 {
   196  		ceil := math.Ceil(position)
   197  		if ceil != position {
   198  			return ceil
   199  		}
   200  		return ceil + 1.0
   201  	} else { // deltaSign < 0
   202  		floor := math.Floor(position)
   203  		if floor != position {
   204  			return floor
   205  		}
   206  		return floor - 1
   207  	}
   208  }
   209  
   210  func intFloorOfSegment(start, advance float64) int {
   211  	floor := math.Floor(start)
   212  	if advance >= 0 {
   213  		return int(floor)
   214  	}
   215  	if floor != start {
   216  		return int(floor)
   217  	}
   218  	return int(floor) - 1
   219  }