diff --git a/pixie.nimble b/pixie.nimble index b96e0ab..214778a 100644 --- a/pixie.nimble +++ b/pixie.nimble @@ -1,4 +1,4 @@ -version = "0.0.17" +version = "0.0.18" author = "Andre von Houck and Ryan Oldenburg" description = "Full-featured 2d graphics library for Nim." license = "MIT" @@ -11,4 +11,4 @@ requires "chroma >= 0.2.1" requires "zippy >= 0.3.5" requires "flatty >= 0.1.3" requires "nimsimd >= 0.4.6" -requires "bumpy >= 1.0.0" +requires "bumpy >= 1.0.1" diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index f1ccc3d..6932bc9 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -362,33 +362,32 @@ proc polygon*(path: var Path, x, y, size: float32, sides: int) = y + size * sin(side.float32 * 2.0 * PI / sides.float32) ) -proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] = +proc commandsToShapes*(path: Path): seq[seq[Segment]] = ## Converts SVG-like commands to simpler polygon - var start, at, to, ctr, ctr2: Vec2 - var prevCommand: PathCommandKind + var + start, at: Vec2 + shape: seq[Segment] - var polygon: seq[Vec2] + # Some commands use data from the previous command + var + prevCommandKind = Move + prevCtrl, prevCtrl2: Vec2 - proc drawLine(at, to: Vec2) = - # Don't add any 0 length lines. - if at - to != vec2(0, 0): - # Don't double up points. - if polygon.len == 0 or polygon[^1] != at: - polygon.add(at) - polygon.add(to) + const errorMargin = 0.2 - proc drawCurve(at, ctrl1, ctrl2, to: Vec2) = + proc addCubic(shape: var seq[Segment], at, ctrl1, ctrl2, to: Vec2) = proc compute(at, ctrl1, ctrl2, to: Vec2, t: float32): Vec2 {.inline.} = pow(1 - t, 3) * at + - 3 * pow(1 - t, 2) * t * ctrl1 + - 3 * (1 - t) * pow(t, 2) * ctrl2 + + pow(1 - t, 2) * 3 * t * ctrl1 + + (1 - t) * 3 * pow(t, 2) * ctrl2 + pow(t, 3) * to var prev = at - proc discretize(i, steps: int) = + proc discretize(shape: var seq[Segment], i, steps: int) = + # Closure captures at, ctrl1, ctrl2, to and prev let tPrev = (i - 1).float32 / steps.float32 t = i.float32 / steps.float32 @@ -397,37 +396,47 @@ proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] = midpoint = (prev + next) / 2 error = (midpoint - halfway).length - if error >= 0.25: + if error >= errorMargin: # Error too large, double precision for this step - discretize(i * 2 - 1, steps * 2) - discretize(i * 2, steps * 2) + shape.discretize(i * 2 - 1, steps * 2) + shape.discretize(i * 2, steps * 2) else: - drawLine(prev, next) + shape.add(segment(prev, next)) prev = next - discretize(1, 1) + shape.discretize(1, 1) - proc drawQuad(p0, p1, p2: Vec2) = - let devx = p0.x - 2.0 * p1.x + p2.x - let devy = p0.y - 2.0 * p1.y + p2.y - let devsq = devx * devx + devy * devy - if devsq < 0.333: - drawLine(p0, p2) - return - let tol = 3.0 - let n = 1 + (tol * (devsq)).sqrt().sqrt().floor() - var p = p0 - let nrecip = 1 / n - var t = 0.0 - for i in 0 ..< int(n): - t += nrecip - let pn = lerp(lerp(p0, p1, t), lerp(p1, p2, t), t) - drawLine(p, pn) - p = pn + proc addQuadratic(shape: var seq[Segment], at, ctrl, to: Vec2) = - drawLine(p, p2) + proc compute(at, ctrl, to: Vec2, t: float32): Vec2 {.inline.} = + pow(1 - t, 2) * at + + 2 * (1 - t) * t * ctrl + + pow(t, 2) * to - proc drawArc( + var prev = at + + proc discretize(shape: var seq[Segment], i, steps: int) = + # Closure captures at, ctrl, to and prev + let + tPrev = (i - 1).float32 / steps.float32 + t = i.float32 / steps.float32 + next = compute(at, ctrl, to, t) + halfway = compute(at, ctrl, to, tPrev + (t - tPrev) / 2) + midpoint = (prev + next) / 2 + error = (midpoint - halfway).length + + if error >= errorMargin: + # Error too large, double precision for this step + shape.discretize(i * 2 - 1, steps * 2) + shape.discretize(i * 2, steps * 2) + else: + shape.add(segment(prev, next)) + prev = next + + shape.discretize(1, 1) + + proc addArc( + shape: var seq[Segment], at, radii: Vec2, rotation: float32, large, sweep: bool, @@ -513,7 +522,7 @@ proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] = var prev = at - proc discretize(arc: ArcParams, i, steps: int) = + proc discretize(shape: var seq[Segment], arc: ArcParams, i, steps: int) = let step = arc.delta / steps.float32 aPrev = arc.theta + step * (i - 1).float32 @@ -523,277 +532,278 @@ proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] = midpoint = (prev + next) / 2 error = (midpoint - halfway).length - if error >= 0.25: + if error >= errorMargin: # Error too large, try again with doubled precision - discretize(arc, i * 2 - 1, steps * 2) - discretize(arc, i * 2, steps * 2) + shape.discretize(arc, i * 2 - 1, steps * 2) + shape.discretize(arc, i * 2, steps * 2) else: - drawLine(prev, next) + shape.add(segment(prev, next)) prev = next let arc = endpointToCenterArcParams(at, radii, rotation, large, sweep, to) - discretize(arc, 1, 1) + shape.discretize(arc, 1, 1) - for command in commands: + for command in path.commands: if command.numbers.len != command.kind.parameterCount(): raise newException(PixieError, "Invalid path") - case command.kind - of Move: - at.x = command.numbers[0] - at.y = command.numbers[1] - start = at + case command.kind: + of Move: + at.x = command.numbers[0] + at.y = command.numbers[1] + start = at - of Line: - to.x = command.numbers[0] - to.y = command.numbers[1] - drawLine(at, to) - at = to + of Line: + let to = vec2(command.numbers[0], command.numbers[1]) + shape.add(segment(at, to)) + at = to - of VLine: - to.x = at.x - to.y = command.numbers[0] - drawLine(at, to) - at = to + of HLine: + let to = vec2(command.numbers[0], at.y) + shape.add(segment(at, to)) + at = to - of HLine: - to.x = command.numbers[0] - to.y = at.y - drawLine(at, to) - at = to + of VLine: + let to = vec2(at.x, command.numbers[0]) + shape.add(segment(at, to)) + at = to - of Quad: - var i = 0 - while i < command.numbers.len: - ctr.x = command.numbers[i+0] - ctr.y = command.numbers[i+1] - to.x = command.numbers[i+2] - to.y = command.numbers[i+3] + of Cubic: + let + ctrl1 = vec2(command.numbers[0], command.numbers[1]) + ctrl2 = vec2(command.numbers[2], command.numbers[3]) + to = vec2(command.numbers[4], command.numbers[5]) + shape.addCubic(at, ctrl1, ctrl2, to) + at = to + prevCtrl2 = ctrl2 - drawQuad(at, ctr, to) - at = to - i += 4 + of SCubic: + let + ctrl2 = vec2(command.numbers[0], command.numbers[1]) + to = vec2(command.numbers[2], command.numbers[3]) + if prevCommandKind in {Cubic, SCubic, RCubic, RSCubic}: + let ctrl1 = 2 * at - prevCtrl2 + shape.addCubic(at, ctrl1, ctrl2, to) + else: + shape.addCubic(at, at, ctrl2, to) + at = to + prevCtrl2 = ctrl2 - of TQuad: - if prevCommand != Quad and prevCommand != TQuad: - ctr = at - to.x = command.numbers[0] - to.y = command.numbers[1] - ctr = at - (ctr - at) - drawQuad(at, ctr, to) - at = to + of Quad: + let + ctrl = vec2(command.numbers[0], command.numbers[1]) + to = vec2(command.numbers[2], command.numbers[3]) + shape.addQuadratic(at, ctrl, to) + at = to + prevCtrl = ctrl - of Cubic: - ctr.x = command.numbers[0] - ctr.y = command.numbers[1] - ctr2.x = command.numbers[2] - ctr2.y = command.numbers[3] - to.x = command.numbers[4] - to.y = command.numbers[5] - drawCurve(at, ctr, ctr2, to) - at = to - - of Arc: - let - radii = vec2(command.numbers[0], command.numbers[1]) - rotation = command.numbers[2] - large = command.numbers[3] == 1 - sweep = command.numbers[4] == 1 - to = vec2(command.numbers[5], command.numbers[6]) - drawArc(at, radii, rotation, large, sweep, to) - at = to - - of Close: - if at != start: - if prevCommand == Quad or prevCommand == TQuad: - drawQuad(at, ctr, start) + of TQuad: + let + to = vec2(command.numbers[0], command.numbers[1]) + ctrl = + if prevCommandKind in {Quad, TQuad, RQuad, RTQuad}: + 2 * at - prevCtrl else: - drawLine(at, start) - if polygon.len > 0: - result.add(polygon) - polygon = newSeq[Vec2]() + at + shape.addQuadratic(at, ctrl, to) + at = to + prevCtrl = ctrl + + of Arc: + let + radii = vec2(command.numbers[0], command.numbers[1]) + rotation = command.numbers[2] + large = command.numbers[3] == 1 + sweep = command.numbers[4] == 1 + to = vec2(command.numbers[5], command.numbers[6]) + shape.addArc(at, radii, rotation, large, sweep, to) + at = to + + of RMove: + at.x += command.numbers[0] + at.y += command.numbers[1] + start = at + + of RLine: + let to = vec2(at.x + command.numbers[0], at.y + command.numbers[1]) + shape.add(segment(at, to)) + at = to + + of RHLine: + let to = vec2(at.x + command.numbers[0], at.y) + shape.add(segment(at, to)) + at = to + + of RVLine: + let to = vec2(at.x, at.y + command.numbers[0]) + shape.add(segment(at, to)) + at = to + + of RCubic: + let + ctrl1 = vec2(at.x + command.numbers[0], at.y + command.numbers[1]) + ctrl2 = vec2(at.x + command.numbers[2], at.y + command.numbers[3]) + to = vec2(at.x + command.numbers[4], at.y + command.numbers[5]) + shape.addCubic(at, ctrl1, ctrl2, to) + at = to + prevCtrl2 = ctrl2 + + of RSCubic: + let + ctrl2 = vec2(at.x + command.numbers[0], at.y + command.numbers[1]) + to = vec2(at.x + command.numbers[2], at.y + command.numbers[3]) + ctrl1 = + if prevCommandKind in {Cubic, SCubic, RCubic, RSCubic}: + 2 * at - prevCtrl2 + else: + at + shape.addCubic(at, ctrl1, ctrl2, to) + at = to + prevCtrl2 = ctrl2 + + of RQuad: + let + ctrl = vec2(at.x + command.numbers[0], at.y + command.numbers[1]) + to = vec2(at.x + command.numbers[2], at.y + command.numbers[3]) + shape.addQuadratic(at, ctrl, to) + at = to + prevCtrl = ctrl + + of RTQuad: + let + to = vec2(at.x + command.numbers[0], at.y + command.numbers[1]) + ctrl = + if prevCommandKind in {Quad, TQuad, RQuad, RTQuad}: + 2 * at - prevCtrl + else: + at + shape.addQuadratic(at, ctrl, to) + at = to + prevCtrl = ctrl + + of RArc: + let + radii = vec2(command.numbers[0], command.numbers[1]) + rotation = command.numbers[2] + large = command.numbers[3] == 1 + sweep = command.numbers[4] == 1 + to = vec2(at.x + command.numbers[5], at.y + command.numbers[6]) + shape.addArc(at, radii, rotation, large, sweep, to) + at = to + + of Close: + if at != start: + shape.add(segment(at, start)) at = start + if shape.len > 0: + result.add(shape) + shape.setLen(0) - of RMove: - at.x += command.numbers[0] - at.y += command.numbers[1] - start = at + prevCommandKind = command.kind - of RLine: - to.x = at.x + command.numbers[0] - to.y = at.y + command.numbers[1] - drawLine(at, to) - at = to + if shape.len > 0: + result.add(shape) - of RVLine: - to.x = at.x - to.y = at.y + command.numbers[0] - drawLine(at, to) - at = to - - of RHLine: - to.x = at.x + command.numbers[0] - to.y = at.y - drawLine(at, to) - at = to - - of RQuad: - ctr.x = at.x + command.numbers[0] - ctr.y = at.y + command.numbers[1] - to.x = at.x + command.numbers[2] - to.y = at.y + command.numbers[3] - drawQuad(at, ctr, to) - at = to - - of RTQuad: - if prevCommand != RQuad and prevCommand != RTQuad: - ctr = at - to.x = at.x + command.numbers[0] - to.y = at.y + command.numbers[1] - ctr = at - (ctr - at) - drawQuad(at, ctr, to) - at = to - - of RCubic: - ctr.x = at.x + command.numbers[0] - ctr.y = at.y + command.numbers[1] - ctr2.x = at.x + command.numbers[2] - ctr2.y = at.y + command.numbers[3] - to.x = at.x + command.numbers[4] - to.y = at.y + command.numbers[5] - drawCurve(at, ctr, ctr2, to) - at = to - - of RSCubic: - if prevCommand in {Cubic, SCubic, RCubic, RSCubic}: - ctr = 2 * at - ctr2 - else: - ctr = at - ctr2.x = at.x + command.numbers[0] - ctr2.y = at.y + command.numbers[1] - to.x = at.x + command.numbers[2] - to.y = at.y + command.numbers[3] - drawCurve(at, ctr, ctr2, to) - at = to - - else: - raise newException(ValueError, "not supported path command " & $command) - - prevCommand = command.kind - - if polygon.len > 0: - result.add(polygon) - -iterator zipline*[T](s: seq[T]): (T, T) = - ## Return elements in pairs: (1st, 2nd), (2nd, 3rd) ... (nth, last). - for i in 0 ..< s.len - 1: - yield(s[i], s[i + 1]) - -iterator segments*(s: seq[Vec2]): Segment = - ## Return elements in pairs: (1st, 2nd), (2nd, 3rd) ... (last, 1st). - for i in 0 ..< s.len - 1: - yield(Segment(at: s[i], to: s[i + 1])) - # if s.len > 0: - # let - # first = s[0] - # last = s[^1] - # if first != last: - # yield(Segment(at: s[^1], to: s[0])) - -proc strokePolygons*(ps: seq[seq[Vec2]], strokeWidthR, strokeWidthL: float32): seq[seq[Vec2]] = - ## Converts simple polygons into stroked versions: - # TODO: Stroke location, add caps and joins. - for p in ps: - var poly: seq[Vec2] - var back: seq[Vec2] # Back side of poly. - var prevRSeg: Segment - var prevLSeg: Segment - var first = true - for (at, to) in p.zipline: - let tangent = (at - to).normalize() - let normal = vec2(-tangent.y, tangent.x) - - var - rSeg = segment(at + normal * strokeWidthR, to + normal * strokeWidthR) - lSeg = segment(at - normal * strokeWidthL, to - normal * strokeWidthL) - - if first: - first = false - # TODO: draw start cap - else: - var touch: Vec2 - if intersects(prevRSeg, rSeg, touch): - rSeg.at = touch - poly.setLen(poly.len - 1) - else: - discard # TODO: draw joint - - if intersects(prevLSeg, lSeg, touch): - lSeg.at = touch - back.setLen(back.len - 1) - else: - discard # TODO: draw joint - - poly.add rSeg.at - back.add lSeg.at - poly.add rSeg.to - back.add lSeg.to - - prevRSeg = rSeg - prevLSeg = lSeg - - # Add the backside reversed: - for i in 1 .. back.len: - poly.add back[^i] - - # TODO: draw end cap - # Cap it at the end: - poly.add poly[0] - - result.add(poly) - -proc computeBounds(poly: seq[Vec2]): Rect = - if poly.len == 0: +proc strokeShapes( + shapes: seq[seq[Segment]], + color: ColorRGBA, + strokeWidth: float32, + windingRule: WindingRule +): seq[seq[Segment]] = + if strokeWidth == 0: return - proc min(a, b: Vec2): Vec2 = - result.x = min(a.x, b.x) - result.y = min(a.y, b.y) - proc max(a, b: Vec2): Vec2 = - result.x = max(a.x, b.x) - result.y = max(a.y, b.y) - proc floor(a: Vec2): Vec2 = - result.x = a.x.floor - result.y = a.y.floor - proc ceil(a: Vec2): Vec2 = - result.x = a.x.ceil - result.y = a.y.ceil + + let + widthLeft = strokeWidth / 2 + widthRight = strokeWidth / 2 + + for shape in shapes: + var + points: seq[Vec2] + back: seq[Vec2] + for segment in shape: + let + tangent = (segment.at - segment.to).normalize() + normal = vec2(-tangent.y, tangent.x) + left = segment( + segment.at - normal * widthLeft, + segment.to - normal * widthLeft + ) + right = segment( + segment.at + normal * widthRight, + segment.to + normal * widthRight + ) + + points.add([right.at, right.to]) + back.add([left.at, left.to]) + + # Add the back side reversed + for i in 1 .. back.len: + points.add(back[^i]) + + points.add(points[0]) + + # Walk the points to create the shape + var strokeShape: seq[Segment] + for i in 0 ..< points.len - 1: + strokeShape.add(segment(points[i], points[i + 1])) + + if strokeShape.len > 0: + result.add(strokeShape) + +proc computeBounds(shape: seq[Segment]): Rect = var - vMin = poly[0] - vMax = poly[0] - for v in poly: - vMin = min(v, vMin) - vMax = max(v, vMax) - result.xy = vMin.floor - result.wh = (vMax - vMin).ceil + xMin = float32.high + xMax = float32.low + yMin = float32.high + yMax = float32.low + for segment in shape: + xMin = min(xMin, min(segment.at.x, segment.to.x)) + xMax = max(xMax, max(segment.at.x, segment.to.x)) + yMin = min(yMin, min(segment.at.y, segment.to.y)) + yMax = max(yMax, max(segment.at.y, segment.to.y)) + + xMin = floor(xMin) + xMax = ceil(xMax) + yMin = floor(yMin) + yMax = ceil(yMax) + + result.x = xMin + result.y = yMin + result.w = xMax - xMin + result.h = yMax - yMin {.push checks: off, stacktrace: off.} -proc fillPolygons*( +proc fillShapes*( image: Image, - size: Vec2, - polys: seq[seq[Vec2]], + shapes: seq[seq[Segment]], color: ColorRGBA, windingRule: WindingRule, quality = 4 ) = - var bounds = newSeq[Rect](polys.len) - for i, poly in polys: - bounds[i] = computeBounds(poly) + var sortedShapes = newSeq[seq[(Segment, bool)]](shapes.len) + for i, sorted in sortedShapes.mpairs: + for j, segment in shapes[i]: + if segment.at.y == segment.to.y or segment.at - segment.to == Vec2(): + # Skip horizontal and zero-length + continue + var + segment = segment + winding = segment.at.y > segment.to.y + if winding: + swap(segment.at, segment.to) + sorted.add((segment, winding)) + + # Compute the bounds of each shape + var bounds = newSeq[Rect](shapes.len) + for i, shape in shapes: + bounds[i] = computeBounds(shape) const ep = 0.0001 * PI proc scanLineHits( - polys: seq[seq[Vec2]], + shapes: seq[seq[(Segment, bool)]], bounds: seq[Rect], hits: var seq[(float32, bool)], size: Vec2, @@ -804,25 +814,17 @@ proc fillPolygons*( let yLine = y.float32 + ep + shiftY - scan = Line(a: vec2(-10000, yLine), b: vec2(10000, yLine)) + scanline = Line(a: vec2(-10000, yLine), b: vec2(10000, yLine)) - for i, poly in polys: + for i, shape in shapes: let bounds = bounds[i] if bounds.y > y.float32 or bounds.y + bounds.h < y.float32: continue - for line in poly.segments: - if line.at.y == line.to.y: # Skip horizontal lines - continue - var line2 = line - if line2.at.y > line2.to.y: # Sort order doesn't actually matter - swap(line2.at, line2.to) + for (segment, winding) in shape: # Lines often connect and we need them to not share starts and ends var at: Vec2 - if line2.intersects(scan, at) and line2.to != at: - let - winding = line.at.y > line.to.y - x = at.x.clamp(0, size.x) - hits.add((x, winding)) + if scanline.intersects(segment, at) and segment.to != at: + hits.add((at.x.clamp(0, size.x), winding)) hits.sort(proc(a, b: (float32, bool)): int = cmp(a[0], b[0])) @@ -835,7 +837,7 @@ proc fillPolygons*( # Do scan lines for this row. for m in 0 ..< quality: - polys.scanLineHits(bounds, hits, size, y, float32(m) / float32(quality)) + sortedShapes.scanLineHits(bounds, hits, image.wh, y, float32(m) / float32(quality)) if hits.len == 0: continue var @@ -877,17 +879,15 @@ proc fillPolygons*( {.pop.} -type SomePath = seq[seq[Vec2]] | string | Path | seq[PathCommand] +type SomePath = seq[seq[Segment]] | string | Path -proc parseSomePath(path: SomePath): seq[seq[Vec2]] = +proc parseSomePath(path: SomePath): seq[seq[Segment]] = ## Given some path, turns it into polys. when type(path) is string: - commandsToPolygons(parsePath(path).commands) + commandsToShapes(parsePath(path)) elif type(path) is Path: - commandsToPolygons(path.commands) - elif type(path) is seq[PathCommand]: - commandsToPolygons(path) - elif type(path) is seq[seq[Vec2]]: + commandsToShapes(path) + elif type(path) is seq[seq[Segment]]: path proc fillPath*( @@ -896,9 +896,8 @@ proc fillPath*( color: ColorRGBA, windingRule = wrNonZero ) = - let - polys = parseSomePath(path) - image.fillPolygons(image.wh, polys, color, windingRule) + let polys = parseSomePath(path) + image.fillShapes(polys, color, windingRule) proc fillPath*( image: Image, @@ -911,7 +910,7 @@ proc fillPath*( for poly in polys.mitems: for i, p in poly.mpairs: poly[i] = p + pos - image.fillPolygons(image.wh, polys, color, windingRule) + image.fillShapes(polys, color, windingRule) proc fillPath*( image: Image, @@ -924,24 +923,7 @@ proc fillPath*( for poly in polys.mitems: for i, p in poly.mpairs: poly[i] = mat * p - image.fillPolygons(image.wh, polys, color, windingRule) - -proc strokePath*( - image: Image, - path: Path, - color: ColorRGBA, - strokeWidth: float32 = 1.0, - windingRule = wrNonZero - # TODO: Add more params: - # strokeLocation: StrokeLocation, - # strokeCap: StorkeCap, - # strokeJoin: StorkeJoin -) = - let - polys = parseSomePath(path) - (strokeL, strokeR) = (strokeWidth/2, strokeWidth/2) - polys2 = strokePolygons(polys, strokeL, strokeR) - image.fillPath(polys2, color, windingRule) + image.fillShapes(polys, color, windingRule) proc strokePath*( image: Image, @@ -950,7 +932,13 @@ proc strokePath*( strokeWidth: float32, windingRule = wrNonZero ) = - image.strokePath(parsePath(path), color, strokeWidth) + var strokeShapes = strokeShapes( + parseSomePath(path), + color, + strokeWidth, + windingRule + ) + image.fillShapes(strokeShapes, color, windingRule) proc strokePath*( image: Image, @@ -960,13 +948,16 @@ proc strokePath*( pos: Vec2, windingRule = wrNonZero ) = - var polys = parseSomePath(path) - let (strokeL, strokeR) = (strokeWidth/2, strokeWidth/2) - var polys2 = strokePolygons(polys, strokeL, strokeR) - for poly in polys2.mitems: - for i, p in poly.mpairs: - poly[i] = p + pos - image.fillPolygons(image.wh, polys2, color, windingRule) + var strokeShapes = strokeShapes( + parseSomePath(path), + color, + strokeWidth, + windingRule + ) + for shape in strokeShapes.mitems: + for segment in shape.mitems: + segment += pos + image.fillShapes(strokeShapes, color, windingRule) proc strokePath*( image: Image, @@ -976,10 +967,13 @@ proc strokePath*( mat: Mat3, windingRule = wrNonZero ) = - var polys = parseSomePath(path) - let (strokeL, strokeR) = (strokeWidth/2, strokeWidth/2) - var polys2 = strokePolygons(polys, strokeL, strokeR) - for poly in polys2.mitems: - for i, p in poly.mpairs: - poly[i] = mat * p - image.fillPolygons(image.wh, polys2, color, windingRule) + var strokeShapes = strokeShapes( + parseSomePath(path), + color, + strokeWidth, + windingRule + ) + for shape in strokeShapes.mitems: + for segment in shape.mitems: + segment = mat * segment + image.fillShapes(strokeShapes, color, windingRule) diff --git a/tests/images/paths/pathCornerArc.png b/tests/images/paths/pathCornerArc.png index 9e0a56a..98f8c19 100644 Binary files a/tests/images/paths/pathCornerArc.png and b/tests/images/paths/pathCornerArc.png differ diff --git a/tests/images/paths/pathHeart.png b/tests/images/paths/pathHeart.png index 524f694..60bcb67 100644 Binary files a/tests/images/paths/pathHeart.png and b/tests/images/paths/pathHeart.png differ diff --git a/tests/images/paths/pathInvertedCornerArc.png b/tests/images/paths/pathInvertedCornerArc.png index bad9372..28ce885 100644 Binary files a/tests/images/paths/pathInvertedCornerArc.png and b/tests/images/paths/pathInvertedCornerArc.png differ diff --git a/tests/images/paths/pathRotatedArc.png b/tests/images/paths/pathRotatedArc.png index f197060..a66e691 100644 Binary files a/tests/images/paths/pathRotatedArc.png and b/tests/images/paths/pathRotatedArc.png differ diff --git a/tests/images/paths/pathStroke2.png b/tests/images/paths/pathStroke2.png index 7e99ee7..c783557 100644 Binary files a/tests/images/paths/pathStroke2.png and b/tests/images/paths/pathStroke2.png differ diff --git a/tests/images/paths/pathStroke3.png b/tests/images/paths/pathStroke3.png index 21006c7..a51468f 100644 Binary files a/tests/images/paths/pathStroke3.png and b/tests/images/paths/pathStroke3.png differ diff --git a/tests/images/svg/Ghostscript_Tiger.png b/tests/images/svg/Ghostscript_Tiger.png index 1d55efa..b783a3d 100644 Binary files a/tests/images/svg/Ghostscript_Tiger.png and b/tests/images/svg/Ghostscript_Tiger.png differ diff --git a/tests/images/svg/circle01.png b/tests/images/svg/circle01.png index 783fdc1..97979a5 100644 Binary files a/tests/images/svg/circle01.png and b/tests/images/svg/circle01.png differ diff --git a/tests/images/svg/ellipse01.png b/tests/images/svg/ellipse01.png index 84bc81c..542845b 100644 Binary files a/tests/images/svg/ellipse01.png and b/tests/images/svg/ellipse01.png differ diff --git a/tests/images/svg/polygon01.png b/tests/images/svg/polygon01.png index 60d4d91..0f7ba76 100644 Binary files a/tests/images/svg/polygon01.png and b/tests/images/svg/polygon01.png differ diff --git a/tests/images/svg/quad01.png b/tests/images/svg/quad01.png index 452a9e2..645d2f0 100644 Binary files a/tests/images/svg/quad01.png and b/tests/images/svg/quad01.png differ diff --git a/tests/images/svg/rect02.png b/tests/images/svg/rect02.png index 36e9ecc..f3d3c0e 100644 Binary files a/tests/images/svg/rect02.png and b/tests/images/svg/rect02.png differ diff --git a/tests/images/svg/triangle01.png b/tests/images/svg/triangle01.png index 67b2c79..56e722d 100644 Binary files a/tests/images/svg/triangle01.png and b/tests/images/svg/triangle01.png differ