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

     1  package emask
     2  
     3  // A small struct to handle Bézier curve segmentation into straight
     4  // lines. It has a configurable curveThreshold and a maxCurveSplits
     5  // limit. Used by edge_marker and outliner.
     6  type curveSegmenter struct {
     7  	curveThreshold float32 // threshold to decide if a segment approximates
     8  	// a bézier curve well enough or we should split
     9  	maxCurveSplits uint8 // a cutoff for curve segmentation
    10  }
    11  
    12  // Sets the threshold distance to use when splitting Bézier curves into
    13  // linear segments. If a linear segment misses the curve by more than
    14  // the threshold value, the curve will be split. Otherwise, the linear
    15  // segment will be used to approximate it.
    16  //
    17  // Values very close to zero could prevent algorithms from converging
    18  // due to floating point instability, but MaxCurveSplits will still
    19  // prevent infinite loops.
    20  func (self *curveSegmenter) SetThreshold(dist float32) {
    21  	self.curveThreshold = dist
    22  }
    23  
    24  // Sets the maximum amount of times a curve can be recursively split
    25  // into subsegments while trying to approximate it with TraceQuad or
    26  // TraceCube.
    27  //
    28  // The maximum number of segments that will approximate a curve is
    29  // 2^maxCurveSplits.
    30  //
    31  // This value is typically used as a cutoff to prevent low curve thresholds
    32  // from making the curve splitting process too slow, but it can also be used
    33  // creatively to get jaggy results instead of smooth curves.
    34  //
    35  // Values outside the [0, 255] range will be silently clamped.
    36  func (self *curveSegmenter) SetMaxSplits(maxCurveSplits int) {
    37  	if maxCurveSplits < 0 {
    38  		self.maxCurveSplits = 0
    39  	} else if maxCurveSplits > 255 {
    40  		self.maxCurveSplits = 255
    41  	} else {
    42  		self.maxCurveSplits = uint8(maxCurveSplits)
    43  	}
    44  }
    45  
    46  type traceFunc = func(x, y float64) // called for each segment during curve segmentation
    47  func (self *curveSegmenter) TraceQuad(lineTo traceFunc, x, y, ctrlX, ctrlY, fx, fy float64) {
    48  	self.recursiveTraceQuad(lineTo, x, y, ctrlX, ctrlY, fx, fy, 0)
    49  }
    50  
    51  func (self *curveSegmenter) recursiveTraceQuad(lineTo traceFunc, x, y, ctrlX, ctrlY, fx, fy float64, depth uint8) (float64, float64) {
    52  	if depth >= self.maxCurveSplits || self.withinThreshold(x, y, fx, fy, ctrlX, ctrlY) {
    53  		lineTo(fx, fy)
    54  		return fx, fy
    55  	}
    56  
    57  	ocx, ocy := lerp(x, y, ctrlX, ctrlY, 0.5)   // origin to control
    58  	cfx, cfy := lerp(ctrlX, ctrlY, fx, fy, 0.5) // control to end
    59  	ix, iy := lerp(ocx, ocy, cfx, cfy, 0.5)     // interpolated point
    60  	x, y = self.recursiveTraceQuad(lineTo, x, y, ocx, ocy, ix, iy, depth+1)
    61  	return self.recursiveTraceQuad(lineTo, x, y, cfx, cfy, fx, fy, depth+1)
    62  }
    63  
    64  func (self *curveSegmenter) TraceCube(lineTo traceFunc, x, y, cx1, cy1, cx2, cy2, fx, fy float64) {
    65  	self.recursiveTraceCube(lineTo, x, y, cx1, cy1, cx2, cy2, fx, fy, 0)
    66  }
    67  
    68  func (self *curveSegmenter) recursiveTraceCube(lineTo traceFunc, x, y, cx1, cy1, cx2, cy2, fx, fy float64, depth uint8) (float64, float64) {
    69  	if depth >= self.maxCurveSplits || (self.withinThreshold(x, y, cx2, cy2, cx1, cy1) && self.withinThreshold(cx1, cy1, fx, fy, cx2, cy2)) {
    70  		lineTo(fx, fy)
    71  		return fx, fy
    72  	}
    73  
    74  	oc1x, oc1y := lerp(x, y, cx1, cy1, 0.5)         // origin to control 1
    75  	c1c2x, c1c2y := lerp(cx1, cy1, cx2, cy2, 0.5)   // control 1 to control 2
    76  	c2fx, c2fy := lerp(cx2, cy2, fx, fy, 0.5)       // control 2 to end
    77  	iox, ioy := lerp(oc1x, oc1y, c1c2x, c1c2y, 0.5) // first interpolation from origin
    78  	ifx, ify := lerp(c1c2x, c1c2y, c2fx, c2fy, 0.5) // second interpolation to end
    79  	ix, iy := lerp(iox, ioy, ifx, ify, 0.5)         // cubic interpolation
    80  	x, y = self.recursiveTraceCube(lineTo, x, y, oc1x, oc1y, iox, ioy, ix, iy, depth+1)
    81  	return self.recursiveTraceCube(lineTo, x, y, ifx, ify, c2fx, c2fy, fx, fy, depth+1)
    82  }
    83  
    84  func (self *curveSegmenter) withinThreshold(ox, oy, fx, fy, px, py float64) bool {
    85  	// https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
    86  	// dist = |a*x + b*y + c| / sqrt(a^2 + b^2)
    87  	a, b, c := toLinearFormABC(ox, oy, fx, fy)
    88  	n := a*px + b*py + c
    89  	return n*n <= float64(self.curveThreshold)*float64(self.curveThreshold)*(a*a+b*b)
    90  }