github.com/kintar/etxt@v0.0.9/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 }