diff --git a/experiments/rounded_rect.nim b/experiments/rounded_rect.nim new file mode 100644 index 0000000..8313e3a --- /dev/null +++ b/experiments/rounded_rect.nim @@ -0,0 +1,123 @@ +import benchy, chroma, pixie, pixie/internal, strformat +import benchy, chroma, pixie + +proc newRoundedRectImage1(w, h, r: int, color: Color): Image = + result = newImage(w, h) + let ctx = newContext(result) + ctx.fillStyle = color(0, 1, 0, 1) + let + pos = vec2(0, 0) + wh = vec2(w.float32, h.float32) + r = r.float32 + ctx.fillRoundedRect(rect(pos, wh), r) + +proc newRoundedRectImage15(w, h, r: int, color: Color): Image = + let path = newPath() + let + pos = vec2(0, 0) + wh = vec2(w.float32, h.float32) + r = r.float32 + path.roundedRect(rect(pos, wh), r, r, r, r) + result = path.fillImage(w, h, color(0, 1, 0, 1)) + +proc newRoundedRectImage2(w, h, r: int, color: Color): Image = + result = newImage(w, h) + result.fill(color) + + let + w1 = w - 1 + h1 = h - 1 + for y in 0 ..< r: + for x in 0 ..< r: + var a: float32 = 0 + for s in 0 ..< 5: + let + yc = y.float32 + s.float32 / 5 + (1 / 5 / 2) + xc = r.float32 - sqrt(r.float32*r.float32 - (yc - r.float32) ^ 2) + let mid = (x.float32 - xc + 1).clamp(0, 1) + a += 1/5 * mid + + if a < 1: + var c = color + c.a = a + let cx = c.rgbx + result.setRgbaUnsafe(x, y, cx) + result.setRgbaUnsafe(w1 - x, y, cx) + result.setRgbaUnsafe(w1 - x, h1 - y, cx) + result.setRgbaUnsafe(x, h1 - y, cx) + +proc newRoundedRectImage3(w, h, r: int, color: Color): Image = + result = newImage(w, h) + result.fill(color) + + if r == 0: + return + + const + q = 5 + qf = q.float32 + qoffset: float32 = (1 / qf / 2) + + let + r = r.clamp(0, min(w, h) div 2) + rf = r.float32 + w1 = w - 1 + h1 = h - 1 + rgbx = color.rgbx + channels = [rgbx.r.uint32, rgbx.g.uint32, rgbx.b.uint32, rgbx.a.uint32] + + var coverage = newSeq[uint8](r) + + for y in 0 ..< r: + zeroMem(coverage[0].addr, coverage.len) + var yf: float32 = y.float32 + qoffset + for m in 0 ..< q: + let hit = sqrt(rf^2 - yf^2) + coverage[hit.int] += max((1 - (hit - hit.trunc)) * 255 / qf, 0).uint8 + for x in hit.int + 1 ..< r: + coverage[x] += (255 div q).uint8 + yf += 1 / qf + + for x in 0 ..< r: + let coverage = 255 - coverage[x] + if coverage != 255: + var cx: ColorRGBX + cx.r = ((channels[0] * coverage) div 255).uint8 + cx.g = ((channels[1] * coverage) div 255).uint8 + cx.b = ((channels[2] * coverage) div 255).uint8 + cx.a = ((channels[3] * coverage) div 255).uint8 + + let + xn = r - x - 1 + yn = r - y - 1 + result.setRgbaUnsafe(xn, yn, cx) + result.setRgbaUnsafe(w1 - xn, yn, cx) + result.setRgbaUnsafe(w1 - xn, h1 - yn, cx) + result.setRgbaUnsafe(xn, h1 - yn, cx) + +const r = 16 + +let img1 = newRoundedRectImage1(200, 200, r, color(0, 1, 0, 1)) +img1.writeFile("rrect_current.png") +let img2 = newRoundedRectImage3(200, 200, r, color(0, 1, 0, 1)) +img2.writeFile("rrect_new.png") + +let (diffScore, diffImage) = diff(img1, img2) +echo &"score: {diffScore}" +diffImage.writeFile("rrect_diff.png") + +timeIt "fill rounded rect via path 1": + for i in 0 ..< 10: + discard newRoundedRectImage1(200, 200, r, color(0, 1, 0, 1)) + +timeIt "fill rounded rect via path 1.5": + for i in 0 ..< 10: + discard newRoundedRectImage15(200, 200, r, color(0, 1, 0, 1)) + +timeIt "fill rounded rect via math 2": + for i in 0 ..< 10: + discard newRoundedRectImage2(200, 200, 50, color(0, 1, 0, 1)) + +timeIt "fill rounded rect via math 3": + for i in 0 ..< 10: + discard newRoundedRectImage3(200, 200, r, color(0, 1, 0, 1)) diff --git a/experiments/sweeps3.nim b/experiments/sweeps3.nim index 655783e..6d81fbd 100644 --- a/experiments/sweeps3.nim +++ b/experiments/sweeps3.nim @@ -64,6 +64,10 @@ var hourGlass = parsePath(""" M 20 20 L 180 20 L 20 180 L 180 180 z """) +var hourGlass2 = parsePath(""" + M 20 20 L 180 20 L 20 180 L 180 180 z M 62 24 L 132 24 L 50 173 L 156 173 z +""") + # Hole var hole = parsePath(""" M 40 40 L 40 160 L 160 160 L 160 40 z @@ -88,6 +92,8 @@ when defined(bench): test("cricle", cricle, 100) test("halfAarc", halfAarc, 100) test("hourGlass", hourGlass, 100) + test("hourGlass2", hourGlass2, wr=wrNonZero) + test("hourGlass2", hourGlass2, wr=wrEvenOdd) test("hole", hole, 100) test("holeEvenOdd", holeEvenOdd, 100, wr=wrNonZero) test("holeEvenOdd", holeEvenOdd, 100, wr=wrEvenOdd) @@ -99,7 +105,8 @@ else: # test("cricle", cricle) # test("halfAarc", halfAarc) # test("hourGlass", hourGlass) - #test("hole", hole, wr=wrEvenOdd) - test("holeEvenOdd", holeEvenOdd, wr=wrNonZero) - test("holeEvenOdd", holeEvenOdd, wr=wrEvenOdd) + test("hourGlass2", hourGlass2, wr=wrEvenOdd) + # test("hole", hole, wr=wrEvenOdd) + # test("holeEvenOdd", holeEvenOdd, wr=wrNonZero) + # test("holeEvenOdd", holeEvenOdd, wr=wrEvenOdd) # test("letterG", letterG) diff --git a/experiments/trapezoids/output_sweep.png b/experiments/trapezoids/output_sweep.png index 4835f2d..516a83a 100644 Binary files a/experiments/trapezoids/output_sweep.png and b/experiments/trapezoids/output_sweep.png differ diff --git a/src/pixie/fileformats/svg.nim b/src/pixie/fileformats/svg.nim index f786db8..293d3a2 100644 --- a/src/pixie/fileformats/svg.nim +++ b/src/pixie/fileformats/svg.nim @@ -2,6 +2,7 @@ import chroma, pixie/common, pixie/images, pixie/internal, pixie/paints, pixie/paths, strutils, tables, vmath, xmlparser, xmltree + when defined(pixieDebugSvg): import strtabs diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 4aa032a..acca6bb 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -2103,6 +2103,17 @@ when defined(pixieSweeps): line.winding = s[1] return line + proc intersectsYLine(y: float32, s: Segment, atx: var float32): bool {.inline.} = + let + s2y = s.to.y - s.at.y + denominator = -s2y + numerator = s.at.y - y + u = numerator / denominator + if u >= 0 and u <= 1: + let at = s.at + (u * vec2(s.to.x - s.at.x, s2y)) + atx = at.x + return true + proc binaryInsert(arr: var seq[float32], v: float32) = if arr.len == 0: arr.add(v) @@ -2195,16 +2206,23 @@ when defined(pixieSweeps): windingRule: WindingRule, blendMode: BlendMode ) = - const q = 1/256.0 + let rgbx = color.rgbx var segments = shapes.shapesToSegments() let bounds = computeBounds(segments).snapToPixels() startX = max(0, bounds.x.int) - if segments.len == 0: + if segments.len == 0 or bounds.w.int == 0 or bounds.h.int == 0: return + # const q = 1/10 + # for i in 0 ..< segments.len: + # segments[i][0].at.x = quantize(segments[i][0].at.x, q) + # segments[i][0].at.y = quantize(segments[i][0].at.y, q) + # segments[i][0].to.x = quantize(segments[i][0].to.x, q) + # segments[i][0].to.y = quantize(segments[i][0].to.y, q) + # Create sorted segments. segments.sortSegments(0, segments.high) @@ -2227,14 +2245,13 @@ when defined(pixieSweeps): let s = segments[lastSeg] if s[0].to.y != cutLines[i + 1]: - var at: Vec2 + var atx: float32 var seg = s[0] for j in i ..< sweeps.len: let y = cutLines[j + 1] - #TODO: speed up with horizintal line intersect - if intersects(line(vec2(0, y), vec2(1, y)), seg, at): - sweeps[j].add(toLine((segment(seg.at, at), s[1]))) - seg = segment(at, seg.to) + if intersectsYLine(y, seg, atx): + sweeps[j].add(toLine((segment(seg.at, vec2(atx, y)), s[1]))) + seg = segment(vec2(atx, y), seg.to) else: if seg.at.y != seg.to.y: sweeps[j].add(toLine(s)) @@ -2247,40 +2264,49 @@ when defined(pixieSweeps): break inc i - i = 0 - while i < sweeps.len: - # TODO: Maybe finds all cuts first, add them to array, cut all lines at once. - for t in 0 ..< 10: # TODO: maybe while true: - # keep cutting sweep - var needsCut = false - var cutterLine: float32 = 0 - block doubleFor: - for a in sweeps[i]: - let aSeg = segment(vec2(a.atx, cutLines[i]), vec2(a.tox, cutLines[i+1])) - for b in sweeps[i]: - let bSeg = segment(vec2(b.atx, cutLines[i]), vec2(b.tox, cutLines[i+1])) - var at: Vec2 - if intersectsInner(aSeg, bSeg, at): - needsCut = true - cutterLine = at.y - break doubleFor - # TODO enable? - if false and needsCut: - # Doing a cut. - var - thisSweep = sweeps[i] - sweeps[i].setLen(0) - sweeps.insert(newSeq[SweepLine](), i + 1) - for a in thisSweep: - let seg = segment(vec2(a.atx, cutLines[i]), vec2(a.tox, cutLines[i+1])) - var at: Vec2 - if intersects(line(vec2(0, cutterLine), vec2(1, cutterLine)), seg, at): - sweeps[i+0].add(toLine((segment(seg.at, at), a.winding))) - sweeps[i+1].add(toLine((segment(at, seg.to), a.winding))) - cutLines.binaryInsert(cutterLine) - else: - break - inc i + # i = 0 + # while i < sweeps.len: + # # TODO: Maybe finds all cuts first, add them to array, cut all lines at once. + # var crossCuts: seq[float32] + + # # echo i, " cut?" + + # for aIndex in 0 ..< sweeps[i].len: + # let a = sweeps[i][aIndex] + # # echo i, ":", sweeps.len, ":", cutLines.len + # let aSeg = segment(vec2(a.atx, cutLines[i]), vec2(a.tox, cutLines[i+1])) + # for bIndex in aIndex + 1 ..< sweeps[i].len: + # let b = sweeps[i][bIndex] + # let bSeg = segment(vec2(b.atx, cutLines[i]), vec2(b.tox, cutLines[i+1])) + # var at: Vec2 + # if intersectsInner(aSeg, bSeg, at): + # crossCuts.binaryInsert(at.y) + + # if crossCuts.len > 0: + # var + # thisSweep = sweeps[i] + # yTop = cutLines[i] + # yBottom = cutLines[i + 1] + # sweeps[i].setLen(0) + + # for k in crossCuts: + # let prevLen = cutLines.len + # cutLines.binaryInsert(k) + # if prevLen != cutLines.len: + # sweeps.insert(newSeq[SweepLine](), i + 1) + + # for a in thisSweep: + # var seg = segment(vec2(a.atx, yTop), vec2(a.tox, yBottom)) + # var at: Vec2 + # for j, cutterLine in crossCuts: + # if intersects(line(vec2(0, cutterLine), vec2(1, cutterLine)), seg, at): + # sweeps[i+j].add(toLine((segment(seg.at, at), a.winding))) + # seg = segment(at, seg.to) + # sweeps[i+crossCuts.len].add(toLine((seg, a.winding))) + + # i += crossCuts.len + + # inc i i = 0 while i < sweeps.len: @@ -2320,13 +2346,18 @@ when defined(pixieSweeps): # echo "L ", sw.x, " ", sw.y proc computeCoverage( - coverages: var seq[uint8], + coverages: var seq[uint16], y: int, startX: int, cutLines: seq[float32], currCutLine: int, sweep: seq[SweepLine] ) = + + if cutLines[currCutLine + 1] - cutLines[currCutLine] < 1/256: + # TODO some thing about micro sweeps + return + let sweepHeight = cutLines[currCutLine + 1] - cutLines[currCutLine] yFracTop = ((y.float32 - cutLines[currCutLine]) / sweepHeight).clamp(0, 1) @@ -2341,59 +2372,67 @@ when defined(pixieSweeps): swX = mix(sweep[i+0].atx, sweep[i+0].tox, yFracBottom) seX = mix(sweep[i+1].atx, sweep[i+1].tox, yFracBottom) - minWi = min(nwX, swX).int - maxWi = max(nwX, swX).ceil.int + minWi = min(nwX, swX).int#.clamp(startX, coverages.len + startX) + maxWi = max(nwX, swX).ceil.int#.clamp(startX, coverages.len + startX) - minEi = min(neX, seX).int - maxEi = max(neX, seX).ceil.int - - # TODO: Add case when trapezoids both starts and stops on same pixle. + minEi = min(neX, seX).int#.clamp(startX, coverages.len + startX) + maxEi = max(neX, seX).ceil.int#.clamp(startX, coverages.len + startX) let nw = vec2(sweep[i+0].atx, cutLines[currCutLine]) sw = vec2(sweep[i+0].tox, cutLines[currCutLine + 1]) + f16 = (256 * 256 - 1).float32 for x in minWi ..< maxWi: - var area = pixelCover(nw - vec2(x.float32, y.float32), sw - vec2( - x.float32, y.float32)) - coverages[x - startX] += (area * 255).uint8 + var area = pixelCover( + nw - vec2(x.float32, y.float32), + sw - vec2(x.float32, y.float32) + ) + coverages[x - startX] += (area * f16).uint16 let x = maxWi - var midArea = pixelCover(nw - vec2(x.float32, y.float32), sw - vec2( - x.float32, y.float32)) - var midArea8 = (midArea * 255).uint8 - for x in maxWi ..< minEi: - # TODO: Maybe try coverages of uint16 to prevent streeks in solid white fill? - coverages[x - startX] += midArea8 + var midArea = pixelCover( + nw - vec2(x.float32, y.float32), + sw - vec2(x.float32, y.float32) + ) + for x in maxWi ..< maxEi: + coverages[x - startX] += (midArea * f16).uint16 let ne = vec2(sweep[i+1].atx, cutLines[currCutLine]) se = vec2(sweep[i+1].tox, cutLines[currCutLine + 1]) for x in minEi ..< maxEi: - var area = midArea - pixelCover(ne - vec2(x.float32, y.float32), se - - vec2(x.float32, y.float32)) - coverages[x - startX] += (area * 255).uint8 + var area = pixelCover( + ne - vec2(x.float32, y.float32), + se - vec2(x.float32, y.float32) + ) + coverages[x - startX] -= (area * f16).uint16 i += 2 var currCutLine = 0 - coverages = newSeq[uint8](bounds.w.int) - for scanLine in cutLines[0].int ..< cutLines[^1].ceil.int: - zeroMem(coverages[0].addr, coverages.len) + coverages16 = newSeq[uint16](bounds.w.int) + coverages8 = newSeq[uint8](bounds.w.int) + for scanLine in max(cutLines[0].int, 0) ..< min(cutLines[^1].ceil.int, image.height): - coverages.computeCoverage(scanLine, startX, cutLines, currCutLine, sweeps[currCutLine]) + zeroMem(coverages16[0].addr, coverages16.len * 2) + + coverages16.computeCoverage( + scanLine, startX, cutLines, currCutLine, sweeps[currCutLine]) while cutLines[currCutLine + 1] < scanLine.float + 1.0: inc currCutLine if currCutLine == sweeps.len: break - coverages.computeCoverage(scanLine, startX, cutLines, currCutLine, - sweeps[currCutLine]) + coverages16.computeCoverage( + scanLine, startX, cutLines, currCutLine, sweeps[currCutLine]) + for i in 0 ..< coverages16.len: + coverages8[i] = (coverages16[i] shr 8).uint8 image.fillCoverage( rgbx, startX = startX, y = scanLine, - coverages, + coverages8, blendMode )