From f4bb34ee5efdefea1351dc5a4f104c672f2d6958 Mon Sep 17 00:00:00 2001 From: treeform Date: Sat, 27 Nov 2021 19:06:14 -0800 Subject: [PATCH] Experiment with sweeps. --- experiments/sweeps.nim | 749 +++++++++++++++++++++ experiments/sweeps2.nim | 481 +++++++++++++ experiments/trapezoid.nim | 311 +++++---- experiments/trapezoid0.nim | 260 +++++++ experiments/trapezoid2.nim | 505 ++++++++++++++ experiments/trapezoids/g.png | Bin 10985 -> 28721 bytes experiments/trapezoids/heart.png | Bin 4311 -> 1588 bytes experiments/trapezoids/l.png | Bin 7739 -> 11331 bytes experiments/trapezoids/output_diff.png | Bin 0 -> 3680 bytes experiments/trapezoids/output_scanline.png | Bin 0 -> 4767 bytes experiments/trapezoids/output_sweep.png | Bin 0 -> 5147 bytes experiments/trapezoids/rect.png | Bin 921 -> 2357 bytes experiments/trapezoids/rhombus.png | Bin 1260 -> 1264 bytes 13 files changed, 2174 insertions(+), 132 deletions(-) create mode 100644 experiments/sweeps.nim create mode 100644 experiments/sweeps2.nim create mode 100644 experiments/trapezoid0.nim create mode 100644 experiments/trapezoid2.nim create mode 100644 experiments/trapezoids/output_diff.png create mode 100644 experiments/trapezoids/output_scanline.png create mode 100644 experiments/trapezoids/output_sweep.png diff --git a/experiments/sweeps.nim b/experiments/sweeps.nim new file mode 100644 index 0000000..0fae4a8 --- /dev/null +++ b/experiments/sweeps.nim @@ -0,0 +1,749 @@ + +import algorithm, bumpy, chroma, pixie/images, print, + sequtils, vmath, benchy + +import pixie, pixie/paths {.all.} + + +printColors = false + +proc intersects*(a, b: Segment, at: var Vec2): bool {.inline.} = + ## Checks if the a segment intersects b segment. + ## If it returns true, at will have point of intersection + let + s1 = a.to - a.at + s2 = b.to - b.at + denominator = (-s2.x * s1.y + s1.x * s2.y) + s = (-s1.y * (a.at.x - b.at.x) + s1.x * (a.at.y - b.at.y)) / denominator + t = (s2.x * (a.at.y - b.at.y) - s2.y * (a.at.x - b.at.x)) / denominator + + if s > 0 and s < 1 and t > 0 and t < 1: + at = a.at + (t * s1) + return true + +proc pixelCover(a0, b0: Vec2): float32 = + ## Returns the amount of area a given segment sweeps to the right + ## in a [0,0 to 1,1] box. + var + a = a0 + b = b0 + aI: Vec2 + bI: Vec2 + area: float32 = 0.0 + + # # Sort A on top. + # if a.y > b.y: + # let tmp = a + # a = b + # b = tmp + + # if (b.y < 0 or a.y > 1) or # Above or bellow, no effect. + # (a.x >= 1 and b.x >= 1) or # To the right, no effect. + # (a.y == b.y): # Horizontal line, no effect. + # return 0 + + if (a.x < 0 and b.x < 0) or # Both to the left. + (a.x == b.x): # Vertical line + # Area of the rectangle: + return (1 - clamp(a.x, 0, 1)) * (min(b.y, 1) - max(a.y, 0)) + + else: + # y = mm*x + bb + let + mm: float32 = (b.y - a.y) / (b.x - a.x) + bb: float32 = a.y - mm * a.x + + if a.x >= 0 and a.x <= 1 and a.y >= 0 and a.y <= 1: + # A is in pixel bounds. + aI = a + else: + aI = vec2((0 - bb) / mm, 0) + if aI.x < 0: + let y = mm * 0 + bb + # Area of the extra rectangle. + area += (min(bb, 1) - max(a.y, 0)).clamp(0, 1) + aI = vec2(0, y.clamp(0, 1)) + elif aI.x > 1: + let y = mm * 1 + bb + aI = vec2(1, y.clamp(0, 1)) + + if b.x >= 0 and b.x <= 1 and b.y >= 0 and b.y <= 1: + # B is in pixel bounds. + bI = b + else: + bI = vec2((1 - bb) / mm, 1) + if bI.x < 0: + let y = mm * 0 + bb + # Area of the extra rectangle. + area += (min(b.y, 1) - max(bb, 0)).clamp(0, 1) + bI = vec2(0, y.clamp(0, 1)) + elif bI.x > 1: + let y = mm * 1 + bb + bI = vec2(1, y.clamp(0, 1)) + + area += ((1 - aI.x) + (1 - bI.x)) / 2 * (bI.y - aI.y) + return area + +proc intersectsInner*(a, b: Segment, at: var Vec2): bool {.inline.} = + ## Checks if the a segment intersects b segment. + ## If it returns true, at will have point of intersection + let + s1 = a.to - a.at + s2 = b.to - b.at + denominator = (-s2.x * s1.y + s1.x * s2.y) + s = (-s1.y * (a.at.x - b.at.x) + s1.x * (a.at.y - b.at.y)) / denominator + t = (s2.x * (a.at.y - b.at.y) - s2.y * (a.at.x - b.at.x)) / denominator + + if s > 0 and s < 1 and t >= 0 and t <= 1: + at = a.at + (t * s1) + return true + +type + + Trapezoid = object + nw, ne, se, sw: Vec2 + + Line = object + #m, x, b: float32 + atx, tox: float32 + +proc toLine(s: Segment): Line = + var line = Line() + # y = mx + b + line.atx = s.at.x + line.tox = s.to.x + # line.m = (s.at.y - s.to.y) / (s.at.x - s.to.x) + # line.b = s.at.y - line.m * s.at.x + return line + +proc roundBy*(v: Vec2, n: float32): Vec2 {.inline.} = + result.x = sign(v.x) * round(abs(v.x) / n) * n + result.y = sign(v.y) * round(abs(v.y) / n) * n + +proc fillPath2(mask: Mask, p: Path) = + + var polygons = p.commandsToShapes() + + const q = 1/256.0 + + # Creates segment q, quantize and remove horizontal lines. + var segments1: seq[Segment] + for shape in polygons: + for s in shape.segments: + var s = s + s.at = s.at.roundBy(q) + s.to = s.to.roundBy(q) + if s.at.y != s.to.y: + if s.at.y > s.to.y: + # make sure segments always are at.y higher + swap(s.at, s.to) + segments1.add(s) + segments1.sort(proc(a, b: Segment): int = cmp(a.at.y, b.at.y)) + + # Compute cutLines + var + cutLines: seq[float32] + last = segments1[0].at.y + bottom = segments1[0].to.y + cutLines.add(last) + for s in segments1: + if s.at.y != last: + last = s.at.y + cutLines.add(last) + if bottom < s.to.y: + bottom = s.to.y + cutLines.add(bottom) + #print cutLines + + var + sweeps = newSeq[seq[Line]](cutLines.len - 1) # dont add bottom cutLine + lastSeg = 0 + for i, sweep in sweeps.mpairs: + #print "sweep", i, cutLines[i] + while segments1[lastSeg].at.y == cutLines[i]: + let s = segments1[lastSeg] + #print s + if s.to.y != cutLines[i + 1]: + #print "needs cut?" + quit() + sweep.add(toLine(segments1[lastSeg])) + inc lastSeg + if lastSeg >= segments1.len: + # Sort the last sweep by X + break + # Sort the sweep by X + sweep.sort proc(a, b: Line): int = + result = cmp(a.atx, b.atx) + if result == 0: + result = cmp(a.tox, b.tox) + + #print sweeps + + proc fillCoverage(y: int, currCutLine: int, sweep: seq[Line]) = + + let + sweepHeight = cutLines[currCutLine + 1] - cutLines[currCutLine] + yFracTop = (y.float32 - cutLines[currCutLine]) / sweepHeight + yFracBottom = (y.float32 + 1 - cutLines[currCutLine]) / sweepHeight + var i = 0 + while i < sweep.len: + let + nwX = mix(sweep[i+0].atx, sweep[i+0].tox, yFracTop) + neX = mix(sweep[i+1].atx, sweep[i+1].tox, yFracTop) + + 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 + + minEi = min(neX, seX).int + maxEi = max(neX, seX).ceil.int + + template write(x, y: int, alpha: float32) = + let backdrop = mask.getValueUnsafe(x, y) + mask.setValueUnsafe(x, y, backdrop + (alpha * 255).uint8) + + let + nw = vec2(sweep[i+0].atx, cutLines[currCutLine]) + sw = vec2(sweep[i+0].tox, cutLines[currCutLine + 1]) + for x in minWi ..< maxWi: + var area = pixelCover(nw - vec2(x.float32, y.float32), sw - vec2(x.float32, y.float32)) + write(x, y, area) + + let x = maxWi + var midArea = pixelCover(nw - vec2(x.float32, y.float32), sw - vec2(x.float32, y.float32)) + for x in maxWi ..< minEi: + write(x, y, midArea) + + 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)) + write(x, y, area) + + i += 2 + + + # ## x10 slower + # for x in 0 ..< mask.width: + # for i, line in sweep: + # let + # segat = vec2(line.atx, cutLines[currCutLine]) + # segto = vec2(line.tox, cutLines[currCutLine + 1]) + # var area = pixelCover(segat - vec2(x.float32, y.float32), segto - vec2(x.float32, y.float32)) + # if i mod 2 == 1: + # area = -area + # let backdrop = mask.getValueUnsafe(x, y) + # mask.setValueUnsafe(x, y, backdrop + (area * 255).uint8) + + + # var i = 0 + # let + # sweepHeight = cutLines[currCutLine + 1] - cutLines[currCutLine] + # yFracTop = (y.float32 - cutLines[currCutLine]) / sweepHeight + # yFracBottom = (y.float32 + 1 - cutLines[currCutLine]) / sweepHeight + # #print "cover", y, sweepHeight, yFrac + # while i < sweep.len: + # #print "fill", sweep[i].at.x, "..", sweep[i+1].at.x + + + # var ay = 1.0.float32 + # if y.float32 < cutLines[currCutLine]: + # let a2 = cutLines[currCutLine] - y.float32 + # #print "cut from top", y.float, cutLines[currCutLine], a2 + # ay *= a2 + # if y.float32 + 1 > cutLines[currCutLine + 1]: + # let a2 = (y.float32 + 1) - cutLines[currCutLine + 1] + # #print "cut from bottom", y.float, cutLines[currCutLine], a2 + # ay *= a2 + + # #if y == 20: + # # print "--", y + + # template write(x, y: int, alpha: float32) = + # let backdrop = mask.getValueUnsafe(x, y) + # mask.setValueUnsafe(x, y, backdrop + (ay * alpha * 255).uint8) + + # let + # nwX = mix(sweep[i+0].atx, sweep[i+0].tox, yFracTop) + # neX = mix(sweep[i+1].atx, sweep[i+1].tox, yFracTop) + + # swX = mix(sweep[i+0].atx, sweep[i+0].tox, yFracBottom) + # seX = mix(sweep[i+1].atx, sweep[i+1].tox, yFracBottom) + + # # minW = min(nwX, swX) + # # maxW = max(nwX, swX) + # # minE = min(neX, seX) + # # maxE = max(neX, seX) + + + + # var + # endWAt: float32 = 0 + # endWTo: float32 = 0 + # endWArea: float32 = 0 + + # slopeWUp = swX < nwX + # slopeWAt: float32 = 0 + # slopeWTo: float32 = 0 + # slopeWArea: float32 = 0 + # slopeWRate: float32 = 0 + + # transitionWAt: float32 = 0 + # transitionWTo: float32 = 0 + # transitionWArea: float32 = 0 + + # fillAt: float32 = 0 + # fillTo: float32 = 0 + + # transitionEAt: float32 = 0 + # transitionETo: float32 = 0 + # transitionEArea: float32 = 0 + + # slopeEUp = seX < neX + # slopeEAt: float32 = 0 + # slopeETo: float32 = 0 + # slopeEArea: float32 = 0 + # slopeERate: float32 = 0 + + # endEAt: float32 = 0 + # endETo: float32 = 0 + # endEArea: float32 = 0 + + # if slopeWUp: + # endWAt = swX.floor + # endWTo = swX.ceil + + # slopeWAt = endWTo + # slopeWTo = nwX.floor + + # transitionWAt = slopeWAt + # transitionWTo = nwX.ceil + + # fillAt = transitionWTo + + # else: + # endWAt = nwX.floor + # endWTo = nwX.ceil + + # slopeWAt = endWTo + # slopeWTo = swX.floor + + # transitionWAt = slopeWAt + # transitionWTo = swX.ceil + + # fillAt = transitionWTo + + + # if slopeEUp: + + # fillTo = seX.floor + + # transitionEAt = fillTo + # transitionETo = seX.ceil + + # slopeEAt = transitionETo + # slopeETo = neX.floor + + # endEAt = slopeETo + # endETo = neX.ceil + + # else: + + # fillTo = neX.floor + + # transitionEAt = fillTo + # transitionETo = neX.ceil + + # slopeEAt = transitionETo + # slopeETo = seX.floor + + # endEAt = slopeETo + # endETo = seX.ceil + + + # let + # segat = vec2(line.atx, cutLines[currCutLine]) + # segto = vec2(line.tox, cutLines[currCutLine + 1]) + + + # var area = pixelCover(segat - vec2(x.float32, y.float32), segto - vec2(x.float32, y.float32)) + # if i mod 2 == 1: + # area = -area + + + # #print endWAt, slopeWAt, transitionWAt, fillAt, fillTo, transitionEAt, slopeEAt, endEAt + + # # if nwX < swX: + # # print nwX.int, swX.int + # # for x in nwX.int ..< swX.int: + # # var a = 0.5 # ((x.float32 - nwX) / (swX - nwX)) + # # #print a + # # write(x, y, a.clamp(0, a)) + # # else: + # # for x in swX.int ..< nwX.int: + # # var a = 0.5 #((x.float32 - swX) / (nwX - swX)) + # # #print a + # # write(x, y, a.clamp(0, a)) + + # #write(wX.int, y, 1 - (wX - wX.floor)) + + # # for x in endWAt.int ..< endWTo.int: + # # write(x, y, 0.1) + + # # for x in slopeWAt.int ..< slopeWTo.int: + # # write(x, y, 0.25) + + # # for x in transitionWAt.int ..< transitionWTo.int: + # # write(x, y, 0.50) + + # for x in fillAt.int ..< fillTo.int: + # write(x, y, 1) + + # # for x in transitionEAt.int ..< transitionETo.int: + # # write(x, y, 0.50) + + # # for x in slopeEAt.int ..< slopeETo.int: + # # write(x, y, 0.25) + + # # for x in endEAt.int ..< endETo.int: + # # write(x, y, 0.1) + + # # print 1 - (eX - eX.floor) + # # write(eX.int - 1, y, 1 - (eX - eX.floor)) + + # # if neX < seX: + # # for x in neX.int ..< seX.int: + # # var a = 0.5 # ((neX - x.float32) / (seX - neX)) + # # #print a + # # write(x, y, a.clamp(0, a)) + # # else: + # # for x in seX.int ..< neX.int: + # # var a = 0.5 # ((seX - x.float32) / (neX - seX)) + # # #print a + # # write(x, y, a.clamp(0, a)) + + # i += 2 + + # let quality = 5 + # for m in 0 ..< quality: + # let + # sweepHeight = cutLines[currCutLine + 1] - cutLines[currCutLine] + # yFrac = (y.float32 + (m.float32 / quality.float32) - cutLines[currCutLine]) / sweepHeight + # if yFrac < 0.0 or yFrac >= 1.0: + # continue + # var i = 0 + # while i < sweep.len: + # let + # minXf1 = mix(sweep[i+0].at.x, sweep[i+0].to.x, yFrac) + # maxXf1 = mix(sweep[i+1].at.x, sweep[i+1].to.x, yFrac) + # minXi1 = minXf1.int + # maxXi1 = maxXf1.int + # for x in minXi1 ..< maxXi1: + # let backdrop = mask.getValueUnsafe(x, y) + # mask.setValueUnsafe(x, y, backdrop + (255 div quality).uint8) + # # if x == 100 and y == 165: + # # print backdrop, 255 div quality + # # print mask.getValueUnsafe(x, y) + # i += 2 + + # let + # sweepHeight = cutLines[currCutLine + 1] - cutLines[currCutLine] + # yFrac = (y.float32 - cutLines[currCutLine]) / sweepHeight + # var i = 0 + # while i < sweep.len: + # let + # minXf1 = mix(sweep[i+0].atx, sweep[i+0].tox, yFrac) + # maxXf1 = mix(sweep[i+1].atx, sweep[i+1].tox, yFrac) + # minXi1 = minXf1.floor.int + # maxXi1 = maxXf1.floor.int + # for x in minXi1 .. maxXi1: + # mask.setValueUnsafe(x, y, 255) + # i += 2 + + var + currCutLine = 0 + for scanLine in cutLines[0].int ..< cutLines[^1].ceil.int: + #print scanLine, "..<", scanLine + 1 + #print " ", currCutLine, cutLines[currCutLine], "..<", cutLines[currCutLine + 1] + fillCoverage(scanLine, currCutLine, sweeps[currCutLine]) + + while cutLines[currCutLine + 1] < scanLine.float + 1.0: + inc currCutLine + if currCutLine == sweeps.len: + break + #print " ", currCutLine, cutLines[currCutLine], "..<", cutLines[currCutLine + 1] + fillCoverage(scanLine, currCutLine, sweeps[currCutLine]) + + # print sweeps[^1] + # print cutLines + + + + + # var segments: seq[Segment] + # while segments1.len > 0: + # #print segments1.len, segments.len + # var s = segments1.pop() + # var collision = false + # for y in cutLines: + # let scanLine = line(vec2(0, y), vec2(1, y)) + # var at: Vec2 + # if intersects(scanLine, s, at): + # at = at.roundBy(q) + # at.y = y + # if s.at.y != at.y and s.to.y != at.y: + # #print "seg2yline intersects!", a, y, at + # collision = true + # var s1 = segment(s.at, at) + # var s2 = segment(at, s.to) + # #print s.length, "->", s1.length, s2.length + # segments1.add(s1) + # segments1.add(s2) + # break + + # if not collision: + # # means its touching, not intersecting + # segments.add(s) + + # # sort at/to in segments + # # for s in segments.mitems: + # # if s.at.y > s.to.y: + # # swap(s.at, s.to) + + + # #let blender = blendMode.blender() + + # for yScanLine in cutLines[0..^2]: + + # var scanSegments: seq[Segment] + # for s in segments: + # if s.at.y == yScanLine: + # scanSegments.add(s) + # scanSegments.sort(proc(a, b: Segment): int = + # cmp(a.at.x, b.at.x)) + + # # if scanSegments.len mod 2 != 0: + # # print "error???" + # # print yScanLine + # # print scanSegments + # # quit() + + # # TODO: winding rules will go here + + # var trapezoids: seq[Trapezoid] + # for i in 0 ..< scanSegments.len div 2: + # let + # a = scanSegments[i*2+0] + # b = scanSegments[i*2+1] + + # assert a.at.y == b.at.y + # assert a.to.y == b.to.y + # #assert a.at.x < b.at.x + # #assert a.to.x < b.to.x + + # trapezoids.add(Trapezoid( + # nw: a.at, + # ne: b.at, + # se: b.to, # + vec2(0,0.7), + # sw: a.to # + vec2(0,0.7) + # )) + + # var i = 0 + # while i < trapezoids.len: + + # let t = trapezoids[i] + # # print t + # let + # nw = t.nw + # ne = t.ne + # se = t.se + # sw = t.sw + + # let + # height = sw.y - nw.y + # minYf = nw.y + # maxYf = sw.y + # minYi = minYf.floor.int + # maxYi = maxYf.floor.int + + # # print t + + # for y in minYi .. maxYi: + # let + # yFrac = (y.float - nw.y) / height + # minXf = mix(nw.x, sw.x, yFrac) + # maxXf = mix(ne.x, se.x, yFrac) + # minXi = minXf.floor.int + # maxXi = maxXf.floor.int + # #print yFrac + # # if not(minY.int == 58 or maxY.int == 58) or minX > 100: + # # continue + + # var ay: float32 + # if y == minYi and y == maxYi: + # ay = maxYf - minYf + # # print "middle", maxYf, minYf, a + # #print "double", y, a, minY, maxY, round(a * 255) + # elif y == minYi: + # ay = (1 - (minYf - float32(minYi))) + # # print "min y", minYf, minYi, a + # #print "s", y, a, minY, round(a * 255) + # elif y == maxYi: + # ay = (maxYf - float32(maxYi)) + # #print "max y", maxYf, maxYi, a + # # print "e", y, a, maxY, round(a * 255) + # else: + # ay = 1.0 + + # for x in minXi .. maxXi: + # var ax: float32 + # # if x == minXi: + # # a2 = (1 - (minXf - float32(minXi))) + # # #a2 = 1.0 + # # elif x == maxXi: + # # a2 = (maxXf - float32(maxXi)) + # # #a2 = 1.0 + # # else: + # # a2 = 1.0 + + # if x.float32 < max(nw.x, sw.x): + # ax = 0.5 + # elif x.float32 > min(ne.x, se.x): + # ax = 0.25 + # else: + # ax = 1.0 + + # let backdrop = mask.getValueUnsafe(x, y) + # mask.setValueUnsafe(x, y, backdrop + floor(255 * ay * ax).uint8) + # # if x == 100 and y == 172: + # # print backdrop, round(255 * a * a2).uint8 + # # print mask.getValueUnsafe(x, y) + + # inc i + +block: + # Rect + print "rect" + #var image = newImage(200, 200) + + # rect + # var p = Path() + # p.moveTo(50.5, 50.5) + # p.lineTo(50.5, 150.5) + # p.lineTo(150.5, 150.5) + # p.lineTo(150.5, 50.5) + # p.closePath() + + ## rhobus + var p = Path() + p.moveTo(100, 50) + p.lineTo(150, 100) + p.lineTo(100, 150) + p.lineTo(50, 100) + p.closePath() + + # ## heart + # var p = parsePath(""" + # M 20 60 + # A 40 40 90 0 1 100 60 + # A 40 40 90 0 1 180 60 + # Q 180 120 100 180 + # Q 20 120 20 60 + # z + # """) + + ## cricle + # var p = Path() + # p.arc(100, 100, 50, 0, PI * 2, true) + # p.closePath() + + # image.fill(rgba(255, 255, 255, 255)) + #image.fillPath2(p, color(0, 0, 0, 1)) + + var mask = newMask(200, 200) + timeIt "rect sweeps", 100: + for i in 0 ..< 100: + mask.fill(0) + mask.fillPath2(p) + #image.fillPath2(p, color(0, 0, 0, 1)) + mask.writeFile("experiments/trapezoids/output_sweep.png") + + var mask2 = newMask(200, 200) + timeIt "rect scanline", 10: + for i in 0 ..< 100: + mask2.fill(0) + mask2.fillPath(p) + mask2.writeFile("experiments/trapezoids/output_scanline.png") + + let (score, image) = diff(mask.newImage, mask2.newImage) + print score + image.writeFile("experiments/trapezoids/output_diff.png") + + + +# block: +# # Rhombus +# print "rhombus" +# var image = newImage(200, 200) +# image.fill(rgba(255, 255, 255, 255)) + +# var p = Path() +# p.moveTo(100, 50) +# p.lineTo(150, 100) +# p.lineTo(100, 150) +# p.lineTo(50, 100) +# p.closePath() + +# image.fillPath2(p, color(0, 0, 0, 1)) + +# image.writeFile("experiments/trapezoids/rhombus.png") + +# block: +# # heart +# print "heart" +# var image = newImage(400, 400) +# image.fill(rgba(0, 0, 0, 0)) + +# var p = parsePath(""" +# M 40 120 A 80 80 90 0 1 200 120 A 80 80 90 0 1 360 120 +# Q 360 240 200 360 Q 40 240 40 120 z +# """) + +# var mask = newMask(image) +# mask.fillPath2(p) + +# image.draw(mask, blendMode = bmOverwrite) + +# image.writeFile("experiments/trapezoids/heart.png") + +# block: +# # l +# print "l" +# var image = newImage(500, 800) +# image.fill(rgba(255, 255, 255, 255)) + +# var p = parsePath(""" +# M 236 20 Q 150 22 114 57 T 78 166 V 790 L 171 806 V 181 Q 171 158 175 143 T 188 119 T 212 105.5 T 249 98 Z +# """) + +# image.fillPath2(p, color(0, 0, 0, 1)) + +# image.writeFile("experiments/trapezoids/l.png") + +# block: +# # g +# print "g" +# var image = newImage(500, 800) +# image.fill(rgba(255, 255, 255, 255)) + +# var p = parsePath(""" +# M 406 538 Q 394 546 359.5 558.5 T 279 571 Q 232 571 190.5 556 T 118 509.5 T 69 431 T 51 319 Q 51 262 68 214.5 T 117.5 132.5 T 197 78.5 T 303 59 Q 368 59 416.5 68.5 T 498 86 V 550 Q 498 670 436 724 T 248 778 Q 199 778 155.5 770 T 80 751 L 97 670 Q 125 681 165.5 689.5 T 250 698 Q 333 698 369.5 665 T 406 560 V 538 Z M 405 152 Q 391 148 367.5 144.5 T 304 141 Q 229 141 188.5 190 T 148 320 Q 148 365 159.5 397 T 190.5 450 T 235.5 481 T 288 491 Q 325 491 356 480.5 T 405 456 V 152 Z +# """) + +# image.fillPath2(p, color(0, 0, 0, 1)) + +# image.writeFile("experiments/trapezoids/g.png") diff --git a/experiments/sweeps2.nim b/experiments/sweeps2.nim new file mode 100644 index 0000000..9ca6554 --- /dev/null +++ b/experiments/sweeps2.nim @@ -0,0 +1,481 @@ + +import algorithm, bumpy, chroma, pixie/images, print, + sequtils, vmath, benchy, fidget2/perf + +import pixie, pixie/paths {.all.} + +when defined(release): + {.push checks: off.} + +proc pixelCover(a0, b0: Vec2): float32 = + ## Returns the amount of area a given segment sweeps to the right + ## in a [0,0 to 1,1] box. + var + a = a0 + b = b0 + aI: Vec2 + bI: Vec2 + area: float32 = 0.0 + + # # Sort A on top. + # if a.y > b.y: + # let tmp = a + # a = b + # b = tmp + + # if (b.y < 0 or a.y > 1) or # Above or bellow, no effect. + # (a.x >= 1 and b.x >= 1) or # To the right, no effect. + # (a.y == b.y): # Horizontal line, no effect. + # return 0 + + if (a.x < 0 and b.x < 0) or # Both to the left. + (a.x == b.x): # Vertical line + # Area of the rectangle: + return (1 - clamp(a.x, 0, 1)) * (min(b.y, 1) - max(a.y, 0)) + + else: + # y = mm*x + bb + let + mm: float32 = (b.y - a.y) / (b.x - a.x) + bb: float32 = a.y - mm * a.x + + if a.x >= 0 and a.x <= 1 and a.y >= 0 and a.y <= 1: + # A is in pixel bounds. + aI = a + else: + aI = vec2((0 - bb) / mm, 0) + if aI.x < 0: + let y = mm * 0 + bb + # Area of the extra rectangle. + area += (min(bb, 1) - max(a.y, 0)).clamp(0, 1) + aI = vec2(0, y.clamp(0, 1)) + elif aI.x > 1: + let y = mm * 1 + bb + aI = vec2(1, y.clamp(0, 1)) + + if b.x >= 0 and b.x <= 1 and b.y >= 0 and b.y <= 1: + # B is in pixel bounds. + bI = b + else: + bI = vec2((1 - bb) / mm, 1) + if bI.x < 0: + let y = mm * 0 + bb + # Area of the extra rectangle. + area += (min(b.y, 1) - max(bb, 0)).clamp(0, 1) + bI = vec2(0, y.clamp(0, 1)) + elif bI.x > 1: + let y = mm * 1 + bb + bI = vec2(1, y.clamp(0, 1)) + + area += ((1 - aI.x) + (1 - bI.x)) / 2 * (bI.y - aI.y) + return area + +proc intersectsInner*(a, b: Segment, at: var Vec2): bool {.inline.} = + ## Checks if the a segment intersects b segment. + ## If it returns true, at will have point of intersection + let + s1 = a.to - a.at + s2 = b.to - b.at + denominator = (-s2.x * s1.y + s1.x * s2.y) + s = (-s1.y * (a.at.x - b.at.x) + s1.x * (a.at.y - b.at.y)) / denominator + t = (s2.x * (a.at.y - b.at.y) - s2.y * (a.at.x - b.at.x)) / denominator + + if s > 0 and s < 1 and t > 0 and t < 1: + #print s, t + at = a.at + (t * s1) + return true + +type + + Trapezoid = object + nw, ne, se, sw: Vec2 + + Line = object + #m, x, b: float32 + atx, tox: float32 + winding: int16 + +proc toLine(s: (Segment, int16)): Line = + var line = Line() + # y = mx + b + line.atx = s[0].at.x + line.tox = s[0].to.x + line.winding = s[1] + # line.m = (s.at.y - s.to.y) / (s.at.x - s.to.x) + # line.b = s.at.y - line.m * s.at.x + return line + +proc roundBy*(v: Vec2, n: float32): Vec2 {.inline.} = + result.x = sign(v.x) * round(abs(v.x) / n) * n + result.y = sign(v.y) * round(abs(v.y) / n) * n + + +proc computeBounds(polygons: seq[seq[Vec2]]): Rect = + ## Compute the bounds of the segments. + var + xMin = float32.high + xMax = float32.low + yMin = float32.high + yMax = float32.low + for segments in polygons: + for v in segments: + xMin = min(xMin, v.x) + xMax = max(xMax, v.x) + yMin = min(yMin, v.y) + yMax = max(yMax, v.y) + + if xMin.isNaN() or xMax.isNaN() or yMin.isNaN() or yMax.isNaN(): + discard + else: + result.x = xMin + result.y = yMin + result.w = xMax - xMin + result.h = yMax - yMin + +proc binaryInsert(arr: var seq[float32], v: float32) = + if arr.len == 0: + arr.add(v) + return + var + L = 0 + R = arr.len - 1 + while L < R: + let m = (L + R) div 2 + if arr[m] ~= v: + return + elif arr[m] < v: + L = m + 1 + else: # arr[m] > v: + R = m - 1 + if arr[L] ~= v: + return + elif arr[L] > v: + #print "insert", v, arr, L, R + arr.insert(v, L) + else: + #print "insert", v, arr, L, R + arr.insert(v, L + 1) + + +proc fillPath2(image: Image, p: Path, color: Color, windingRule = wrNonZero, blendMode = bmNormal) = + const q = 1/256.0 + let rgbx = color.rgbx + var segments = p.commandsToShapes().shapesToSegments() + let + bounds = computeBounds(segments).snapToPixels() + startX = max(0, bounds.x.int) + + # Create sorted segments and quantize. + segments.sort(proc(a, b: (Segment, int16)): int = cmp(a[0].at.y, b[0].at.y)) + + # Compute cut lines + var cutLines: seq[float32] + for s in segments: + cutLines.binaryInsert(s[0].at.y) + cutLines.binaryInsert(s[0].to.y) + + var + sweeps = newSeq[seq[Line]](cutLines.len - 1) # dont add bottom cutLine + lastSeg = 0 + i = 0 + while i < sweeps.len: + + #for i, sweep in sweeps.mpairs: + #print "sweep", i, cutLines[i] + + if lastSeg < segments.len: + + while segments[lastSeg][0].at.y == cutLines[i]: + let s = segments[lastSeg] + + if s[0].at.y != s[0].to.y: + + #print s + if s[0].to.y != cutLines[i + 1]: + #print "needs cut?", s + + #quit("need to cut lines") + var at: Vec2 + var seg = s[0] + for j in i ..< sweeps.len: + let y = cutLines[j + 1] + if intersects(line(vec2(0, y), vec2(1, y)), seg, at): + #print "cutting", j, seg + #print "add cut", j, segment(seg.at, at) + sweeps[j].add(toLine((segment(seg.at, at), s[1]))) + seg = segment(at, seg.to) + else: + if seg.at.y != seg.to.y: + #print "add rest", j, segment(seg.at, seg.to) + sweeps[j].add(toLine(s)) + # else: + # print "micro?" + break + else: + #print "add", s + sweeps[i].add(toLine(s)) + + inc lastSeg + + if lastSeg >= segments.len: + break + inc i + + i = 0 + while i < sweeps.len: + for t in 0 ..< 10: + # 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 + if needsCut: + var + thisSweep = sweeps[i] + sweeps[i].setLen(0) + sweeps.insert(newSeq[Line](), 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: + # Sort the sweep by X + sweeps[i].sort proc(a, b: Line): int = + result = cmp(a.atx, b.atx) + if result == 0: + result = cmp(a.tox, b.tox) + + # Do winding order + var + pen = 0 + prevFill = false + j = 0 + # print "sweep", i, "--------------" + while j < sweeps[i].len: + let a = sweeps[i][j] + # print a.winding + if a.winding == 1: + inc pen + if a.winding == -1: + dec pen + # print j, pen, prevFill, shouldFill(windingRule, pen) + let thisFill = shouldFill(windingRule, pen) + if prevFill == thisFill: + # remove this line + # print "remove", j + sweeps[i].delete(j) + continue + prevFill = thisFill + inc j + + # print sweeps[i] + + inc i + + #print sweeps + # for s in 0 ..< sweeps.len: + # let + # y1 = cutLines[s] + # echo "M -100 ", y1 + # echo "L 300 ", y1 + # for line in sweeps[s]: + # let + # nw = vec2(line.atx, cutLines[s]) + # sw = vec2(line.tox, cutLines[s + 1]) + # echo "M ", nw.x, " ", nw.y + # echo "L ", sw.x, " ", sw.y + + proc computeCoverage( + coverages: var seq[uint8], + y: int, + startX: int, + cutLines: seq[float32], + currCutLine: int, + sweep: seq[Line] + ) = + + # if sweep.len mod 2 != 0: + # return + + let + sweepHeight = cutLines[currCutLine + 1] - cutLines[currCutLine] + yFracTop = ((y.float32 - cutLines[currCutLine]) / sweepHeight).clamp(0, 1) + yFracBottom = ((y.float32 + 1 - cutLines[currCutLine]) / sweepHeight).clamp(0, 1) + var i = 0 + while i < sweep.len: + let + nwX = mix(sweep[i+0].atx, sweep[i+0].tox, yFracTop) + neX = mix(sweep[i+1].atx, sweep[i+1].tox, yFracTop) + + 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 + + minEi = min(neX, seX).int + maxEi = max(neX, seX).ceil.int + + let + nw = vec2(sweep[i+0].atx, cutLines[currCutLine]) + sw = vec2(sweep[i+0].tox, cutLines[currCutLine + 1]) + 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 + + 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: + coverages[x - startX] += midArea8 + + 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 + + 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) + + coverages.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]) + + image.fillCoverage( + rgbx, + startX = startX, + y = scanLine, + coverages, + blendMode + ) + +when defined(release): + {.pop.} + + +template test(name: string, p: Path, a: static int = 1, wr = wrNonZero) = + echo name + var image = newImage(200, 200) + timeIt " sweeps", a: + for i in 0 ..< a: + image.fill(color(0, 0, 0, 0)) + image.fillPath2(p, color(1, 0, 0, 1), windingRule = wr) + image.writeFile("experiments/trapezoids/output_sweep.png") + + var image2 = newImage(200, 200) + timeIt " scanline", a: + for i in 0 ..< a: + image2.fill(color(0, 0, 0, 0)) + image2.fillPath(p, color(1, 0, 0, 1), windingRule = wr) + image2.writeFile("experiments/trapezoids/output_scanline.png") + + let (score, diff) = diff(image, image2) + if score > 0.05: + echo "does not appear ot match" + diff.writeFile("experiments/trapezoids/output_diff.png") + + +var rect = Path() +rect.moveTo(50.5, 50.5) +rect.lineTo(50.5, 150.5) +rect.lineTo(150.5, 150.5) +rect.lineTo(150.5, 50.5) +rect.closePath() + +var rhombus = Path() +rhombus.moveTo(100, 50) +rhombus.lineTo(150, 100) +rhombus.lineTo(100, 150) +rhombus.lineTo(50, 100) +rhombus.closePath() + +var heart = parsePath(""" + M 20 60 + A 40 40 90 0 1 100 60 + A 40 40 90 0 1 180 60 + Q 180 120 100 180 + Q 20 120 20 60 + z +""") + +var cricle = Path() +cricle.arc(100, 100, 50, 0, PI * 2, true) +cricle.closePath() + + +# Half arc (test cut lines) +var halfAarc = parsePath(""" + M 25 25 C 85 25 85 125 25 125 z +""") + +# Hour glass (test cross lines) +var hourGlass = parsePath(""" + M 20 20 L 180 20 L 20 180 L 180 180 z +""") + +# Hole +var hole = parsePath(""" + M 40 40 L 40 160 L 160 160 L 160 40 z + M 120 80 L 120 120 L 80 120 L 80 80 z +""") + +var holeEvenOdd = parsePath(""" + M 40 40 L 40 160 L 160 160 L 160 40 z + M 80 80 L 80 120 L 120 120 L 120 80 z +""") + +## g +var letterG = parsePath(""" + M 406 538 Q 394 546 359.5 558.5 T 279 571 Q 232 571 190.5 556 T 118 509.5 T 69 431 T 51 319 Q 51 262 68 214.5 T 117.5 132.5 T 197 78.5 T 303 59 Q 368 59 416.5 68.5 T 498 86 V 550 Q 498 670 436 724 T 248 778 Q 199 778 155.5 770 T 80 751 L 97 670 Q 125 681 165.5 689.5 T 250 698 Q 333 698 369.5 665 T 406 560 V 538 Z M 405 152 Q 391 148 367.5 144.5 T 304 141 Q 229 141 188.5 190 T 148 320 Q 148 365 159.5 397 T 190.5 450 T 235.5 481 T 288 491 Q 325 491 356 480.5 T 405 456 V 152 Z +""") +letterG.transform(scale(vec2(0.2, 0.2))) + +when defined(bench): + test("rect", rect, 100) + test("rhombus", rhombus, 100) + test("heart", heart, 100) + test("cricle", cricle, 100) + test("halfAarc", halfAarc, 100) + test("hourGlass", hourGlass, 100) + test("hole", hole, 100) + test("holeEvenOdd", holeEvenOdd, 100, wr=wrNonZero) + test("holeEvenOdd", holeEvenOdd, 100, wr=wrEvenOdd) + test("letterG", letterG, 100) +else: + # test("rect", rect) + # test("rhombus", rhombus) + # test("heart", heart) + # 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("letterG", letterG) diff --git a/experiments/trapezoid.nim b/experiments/trapezoid.nim index 1e13226..981c150 100644 --- a/experiments/trapezoid.nim +++ b/experiments/trapezoid.nim @@ -1,6 +1,9 @@ -import algorithm, bumpy, chroma, pixie, pixie/images, pixie/paths, print, - sequtils, vmath +import algorithm, bumpy, chroma, pixie/images, print, + sequtils, vmath, benchy + +import pixie, pixie/paths {.all.} + printColors = false @@ -25,7 +28,7 @@ proc roundBy*(v: Vec2, n: float32): Vec2 {.inline.} = result.x = sign(v.x) * round(abs(v.x) / n) * n result.y = sign(v.y) * round(abs(v.y) / n) * n -proc pathToTrapezoids(p: Path): seq[Trapezoid] = +proc fillPath2(mask: Mask, p: Path) = var polygons = p.commandsToShapes() @@ -39,22 +42,10 @@ proc pathToTrapezoids(p: Path): seq[Trapezoid] = s.at = s.at.roundBy(q) s.to = s.to.roundBy(q) if s.at.y != s.to.y: + if s.at.y > s.to.y: + # make sure segments always are at.y higher + swap(s.at, s.to) segments1.add(s) - #print segments1 - - # Handle segments overlapping each other: - # var segments1: seq[Segment] - # while segments0.len > 0: - # var a = segments0.pop() - # var collision = false - # for b in segments0: - # if a != b: - # var at: Vec2 - # if a.intersectsInner(b, at): - # print "seg2seg intersects!", a, b, at - # quit() - # if not collision: - # segments1.add(a) # There is probably a clever way to insert-sort them. var yScanLines: seq[float32] @@ -64,6 +55,7 @@ proc pathToTrapezoids(p: Path): seq[Trapezoid] = if s.to.y notin yScanLines: yScanLines.add s.to.y yScanLines.sort() + print yScanLines var segments: seq[Segment] while segments1.len > 0: @@ -71,8 +63,9 @@ proc pathToTrapezoids(p: Path): seq[Trapezoid] = var s = segments1.pop() var collision = false for y in yScanLines: + let scanLine = line(vec2(0, y), vec2(1, y)) var at: Vec2 - if intersects(line(vec2(0, y), vec2(1, y)), s, at): + if intersects(scanLine, s, at): at = at.roundBy(q) at.y = y if s.at.y != at.y and s.to.y != at.y: @@ -86,17 +79,16 @@ proc pathToTrapezoids(p: Path): seq[Trapezoid] = break if not collision: + # means its touching, not intersecting segments.add(s) - #print segments - # sort at/to in segments - for s in segments.mitems: - if s.at.y > s.to.y: - swap(s.at, s.to) + # for s in segments.mitems: + # if s.at.y > s.to.y: + # swap(s.at, s.to) - #print segments - #print yScanLines + + #let blender = blendMode.blender() for yScanLine in yScanLines[0..^2]: @@ -107,13 +99,7 @@ proc pathToTrapezoids(p: Path): seq[Trapezoid] = scanSegments.sort(proc(a, b: Segment): int = cmp(a.at.x, b.at.x)) - if scanSegments.len mod 2 != 0: - print "error???" - print yScanLine - print scanSegments - quit() - - # if scanSegments.len == 0: + # if scanSegments.len mod 2 != 0: # print "error???" # print yScanLine # print scanSegments @@ -121,6 +107,7 @@ proc pathToTrapezoids(p: Path): seq[Trapezoid] = # TODO: winding rules will go here + var trapezoids: seq[Trapezoid] for i in 0 ..< scanSegments.len div 2: let a = scanSegments[i*2+0] @@ -131,130 +118,190 @@ proc pathToTrapezoids(p: Path): seq[Trapezoid] = #assert a.at.x < b.at.x #assert a.to.x < b.to.x - result.add( - Trapezoid( - nw: a.at, - ne: b.at, - se: b.to, # + vec2(0,0.7), + trapezoids.add(Trapezoid( + nw: a.at, + ne: b.at, + se: b.to, # + vec2(0,0.7), sw: a.to # + vec2(0,0.7) - ) - ) + )) -proc trapFill(image: Image, t: Trapezoid, color: ColorRGBA) = - # assert t.nw.y == t.ne.y - # assert t.sw.y == t.se.y + var i = 0 + while i < trapezoids.len: - let - height = t.sw.y - t.nw.y - minY = clamp(t.nw.y, 0, image.height.float) - maxY = clamp(t.sw.y, 0, image.height.float) - for y in minY.int ..< maxY.int: - var yRate, minX, maxX: float32 + let t = trapezoids[i] + # print t + let + nw = t.nw + ne = t.ne + se = t.se + sw = t.sw - yRate = clamp((y.float - t.nw.y) / height, 0, 1) - minX = clamp(lerp(t.nw.x, t.sw.x, yRate).round, 0, image.width.float) - maxX = clamp(lerp(t.ne.x, t.se.x, yRate).round, 0, image.width.float) + let + height = sw.y - nw.y + minYf = nw.y + maxYf = sw.y + minYi = minYf.floor.int + maxYi = maxYf.floor.int - for x in minX.int ..< maxX.int: - image.setRgbaUnsafe(x, y, color) + # print t -proc drawTrapezoids(image: Image, trapezoids: seq[Trapezoid]) = + for y in minYi .. maxYi: + let + yFrac = (y.float - nw.y) / height + minXf = mix(nw.x, sw.x, yFrac) + maxXf = mix(ne.x, se.x, yFrac) + minXi = minXf.floor.int + maxXi = maxXf.floor.int + #print yFrac + # if not(minY.int == 58 or maxY.int == 58) or minX > 100: + # continue - for trapezoid in trapezoids: - image.trapFill(trapezoid, rgba(0, 0, 0, 255)) + var ay: float32 + if y == minYi and y == maxYi: + ay = maxYf - minYf + # print "middle", maxYf, minYf, a + #print "double", y, a, minY, maxY, round(a * 255) + elif y == minYi: + ay = (1 - (minYf - float32(minYi))) + # print "min y", minYf, minYi, a + #print "s", y, a, minY, round(a * 255) + elif y == maxYi: + ay = (maxYf - float32(maxYi)) + #print "max y", maxYf, maxYi, a + # print "e", y, a, maxY, round(a * 255) + else: + ay = 1.0 - # for trapezoid in trapezoids: - # var p = newPath() - # p.moveTo(trapezoid.nw) - # p.lineTo(trapezoid.ne) - # p.lineTo(trapezoid.se) - # p.lineTo(trapezoid.sw) - # p.closePath() - # image.fillPath(p, rgba(0, 0, 0, 255)) - # image.strokePath(p, rgba(255, 0, 0, 255)) + for x in minXi .. maxXi: + var ax: float32 + # if x == minXi: + # a2 = (1 - (minXf - float32(minXi))) + # #a2 = 1.0 + # elif x == maxXi: + # a2 = (maxXf - float32(maxXi)) + # #a2 = 1.0 + # else: + # a2 = 1.0 + + if x.float32 < max(nw.x, sw.x): + ax = 0.5 + elif x.float32 > min(ne.x, se.x): + ax = 0.25 + else: + ax = 1.0 + + let backdrop = mask.getValueUnsafe(x, y) + mask.setValueUnsafe(x, y, backdrop + floor(255 * ay * ax).uint8) + # if x == 100 and y == 172: + # print backdrop, round(255 * a * a2).uint8 + # print mask.getValueUnsafe(x, y) + + inc i block: # Rect print "rect" - var image = newImage(200, 200) - image.fill(rgba(255, 255, 255, 255)) + #var image = newImage(200, 200) - var p: Path - p.moveTo(50, 50) - p.lineTo(50, 150) - p.lineTo(150, 150) - p.lineTo(150, 50) - p.closePath() - - var trapezoids = p.pathToTrapezoids() - image.drawTrapezoids(trapezoids) - - image.writeFile("experiments/trapezoids/rect.png") - -block: - # Rhombus - print "rhombus" - var image = newImage(200, 200) - image.fill(rgba(255, 255, 255, 255)) - - var p: Path - p.moveTo(100, 50) - p.lineTo(150, 100) - p.lineTo(100, 150) - p.lineTo(50, 100) - p.closePath() - - var trapezoids = p.pathToTrapezoids() - image.drawTrapezoids(trapezoids) - - image.writeFile("experiments/trapezoids/rhombus.png") - -block: - # heart - print "heart" - var image = newImage(400, 400) - image.fill(rgba(255, 255, 255, 255)) + # var p = Path() + # p.moveTo(50.25, 50.25) + # p.lineTo(50.25, 150.25) + # p.lineTo(150.25, 150.25) + # p.lineTo(150.25, 50.25) + # p.closePath() var p = parsePath(""" - M 40 120 A 80 80 90 0 1 200 120 A 80 80 90 0 1 360 120 - Q 360 240 200 360 Q 40 240 40 120 z + M 20 60 + A 40 40 90 0 1 100 60 + A 40 40 90 0 1 180 60 + Q 180 120 100 180 + Q 20 120 20 60 + z """) - var trapezoids = p.pathToTrapezoids() - image.drawTrapezoids(trapezoids) + # image.fill(rgba(255, 255, 255, 255)) + #image.fillPath2(p, color(0, 0, 0, 1)) - image.writeFile("experiments/trapezoids/heart.png") + var mask = newMask(200, 200) + timeIt "rect trapezoids", 1: + #for i in 0 ..< 100: + mask.fill(0) + mask.fillPath2(p) + #image.fillPath2(p, color(0, 0, 0, 1)) + mask.writeFile("experiments/trapezoids/rect_trapesoid.png") -block: - # l - print "l" - var image = newImage(500, 800) - image.fill(rgba(255, 255, 255, 255)) + var mask2 = newMask(200, 200) + timeIt "rect normal", 1: + #for i in 0 ..< 100: + mask2.fill(0) + mask2.fillPath(p) + mask2.writeFile("experiments/trapezoids/rect_scanline.png") - var p = parsePath(""" - M 236 20 Q 150 22 114 57 T 78 166 V 790 L 171 806 V 181 Q 171 158 175 143 T 188 119 T 212 105.5 T 249 98 Z - """) + let (score, image) = diff(mask.newImage, mask2.newImage) + print score + image.writeFile("experiments/trapezoids/rect_diff.png") - #image.strokePath(p, rgba(0, 0, 0, 255)) - var trapezoids = p.pathToTrapezoids() - image.drawTrapezoids(trapezoids) - image.writeFile("experiments/trapezoids/l.png") +# block: +# # Rhombus +# print "rhombus" +# var image = newImage(200, 200) +# image.fill(rgba(255, 255, 255, 255)) -block: - # g - print "g" - var image = newImage(500, 800) - image.fill(rgba(255, 255, 255, 255)) +# var p = Path() +# p.moveTo(100, 50) +# p.lineTo(150, 100) +# p.lineTo(100, 150) +# p.lineTo(50, 100) +# p.closePath() - var p = parsePath(""" - M 406 538 Q 394 546 359.5 558.5 T 279 571 Q 232 571 190.5 556 T 118 509.5 T 69 431 T 51 319 Q 51 262 68 214.5 T 117.5 132.5 T 197 78.5 T 303 59 Q 368 59 416.5 68.5 T 498 86 V 550 Q 498 670 436 724 T 248 778 Q 199 778 155.5 770 T 80 751 L 97 670 Q 125 681 165.5 689.5 T 250 698 Q 333 698 369.5 665 T 406 560 V 538 Z M 405 152 Q 391 148 367.5 144.5 T 304 141 Q 229 141 188.5 190 T 148 320 Q 148 365 159.5 397 T 190.5 450 T 235.5 481 T 288 491 Q 325 491 356 480.5 T 405 456 V 152 Z - """) +# image.fillPath2(p, color(0, 0, 0, 1)) - #image.strokePath(p, rgba(0, 0, 0, 255)) +# image.writeFile("experiments/trapezoids/rhombus.png") - var trapezoids = p.pathToTrapezoids() - image.drawTrapezoids(trapezoids) +# block: +# # heart +# print "heart" +# var image = newImage(400, 400) +# image.fill(rgba(0, 0, 0, 0)) - image.writeFile("experiments/trapezoids/g.png") +# var p = parsePath(""" +# M 40 120 A 80 80 90 0 1 200 120 A 80 80 90 0 1 360 120 +# Q 360 240 200 360 Q 40 240 40 120 z +# """) + +# var mask = newMask(image) +# mask.fillPath2(p) + +# image.draw(mask, blendMode = bmOverwrite) + +# image.writeFile("experiments/trapezoids/heart.png") + +# block: +# # l +# print "l" +# var image = newImage(500, 800) +# image.fill(rgba(255, 255, 255, 255)) + +# var p = parsePath(""" +# M 236 20 Q 150 22 114 57 T 78 166 V 790 L 171 806 V 181 Q 171 158 175 143 T 188 119 T 212 105.5 T 249 98 Z +# """) + +# image.fillPath2(p, color(0, 0, 0, 1)) + +# image.writeFile("experiments/trapezoids/l.png") + +# block: +# # g +# print "g" +# var image = newImage(500, 800) +# image.fill(rgba(255, 255, 255, 255)) + +# var p = parsePath(""" +# M 406 538 Q 394 546 359.5 558.5 T 279 571 Q 232 571 190.5 556 T 118 509.5 T 69 431 T 51 319 Q 51 262 68 214.5 T 117.5 132.5 T 197 78.5 T 303 59 Q 368 59 416.5 68.5 T 498 86 V 550 Q 498 670 436 724 T 248 778 Q 199 778 155.5 770 T 80 751 L 97 670 Q 125 681 165.5 689.5 T 250 698 Q 333 698 369.5 665 T 406 560 V 538 Z M 405 152 Q 391 148 367.5 144.5 T 304 141 Q 229 141 188.5 190 T 148 320 Q 148 365 159.5 397 T 190.5 450 T 235.5 481 T 288 491 Q 325 491 356 480.5 T 405 456 V 152 Z +# """) + +# image.fillPath2(p, color(0, 0, 0, 1)) + +# image.writeFile("experiments/trapezoids/g.png") diff --git a/experiments/trapezoid0.nim b/experiments/trapezoid0.nim new file mode 100644 index 0000000..1e13226 --- /dev/null +++ b/experiments/trapezoid0.nim @@ -0,0 +1,260 @@ + +import algorithm, bumpy, chroma, pixie, pixie/images, pixie/paths, print, + sequtils, vmath + +printColors = false + +proc intersectsInner*(a, b: Segment, at: var Vec2): bool {.inline.} = + ## Checks if the a segment intersects b segment. + ## If it returns true, at will have point of intersection + let + s1 = a.to - a.at + s2 = b.to - b.at + denominator = (-s2.x * s1.y + s1.x * s2.y) + s = (-s1.y * (a.at.x - b.at.x) + s1.x * (a.at.y - b.at.y)) / denominator + t = (s2.x * (a.at.y - b.at.y) - s2.y * (a.at.x - b.at.x)) / denominator + + if s > 0 and s < 1 and t >= 0 and t <= 1: + at = a.at + (t * s1) + return true + +type Trapezoid = object + nw, ne, se, sw: Vec2 + +proc roundBy*(v: Vec2, n: float32): Vec2 {.inline.} = + result.x = sign(v.x) * round(abs(v.x) / n) * n + result.y = sign(v.y) * round(abs(v.y) / n) * n + +proc pathToTrapezoids(p: Path): seq[Trapezoid] = + + var polygons = p.commandsToShapes() + + const q = 1/256.0 + + # Creates segment q, quantize and remove verticals. + var segments1: seq[Segment] + for shape in polygons: + for s in shape.segments: + var s = s + s.at = s.at.roundBy(q) + s.to = s.to.roundBy(q) + if s.at.y != s.to.y: + segments1.add(s) + #print segments1 + + # Handle segments overlapping each other: + # var segments1: seq[Segment] + # while segments0.len > 0: + # var a = segments0.pop() + # var collision = false + # for b in segments0: + # if a != b: + # var at: Vec2 + # if a.intersectsInner(b, at): + # print "seg2seg intersects!", a, b, at + # quit() + # if not collision: + # segments1.add(a) + + # There is probably a clever way to insert-sort them. + var yScanLines: seq[float32] + for s in segments1: + if s.at.y notin yScanLines: + yScanLines.add s.at.y + if s.to.y notin yScanLines: + yScanLines.add s.to.y + yScanLines.sort() + + var segments: seq[Segment] + while segments1.len > 0: + #print segments1.len, segments.len + var s = segments1.pop() + var collision = false + for y in yScanLines: + var at: Vec2 + if intersects(line(vec2(0, y), vec2(1, y)), s, at): + at = at.roundBy(q) + at.y = y + if s.at.y != at.y and s.to.y != at.y: + #print "seg2yline intersects!", a, y, at + collision = true + var s1 = segment(s.at, at) + var s2 = segment(at, s.to) + #print s.length, "->", s1.length, s2.length + segments1.add(s1) + segments1.add(s2) + break + + if not collision: + segments.add(s) + + #print segments + + # sort at/to in segments + for s in segments.mitems: + if s.at.y > s.to.y: + swap(s.at, s.to) + + #print segments + #print yScanLines + + for yScanLine in yScanLines[0..^2]: + + var scanSegments: seq[Segment] + for s in segments: + if s.at.y == yScanLine: + scanSegments.add(s) + scanSegments.sort(proc(a, b: Segment): int = + cmp(a.at.x, b.at.x)) + + if scanSegments.len mod 2 != 0: + print "error???" + print yScanLine + print scanSegments + quit() + + # if scanSegments.len == 0: + # print "error???" + # print yScanLine + # print scanSegments + # quit() + + # TODO: winding rules will go here + + for i in 0 ..< scanSegments.len div 2: + let + a = scanSegments[i*2+0] + b = scanSegments[i*2+1] + + assert a.at.y == b.at.y + assert a.to.y == b.to.y + #assert a.at.x < b.at.x + #assert a.to.x < b.to.x + + result.add( + Trapezoid( + nw: a.at, + ne: b.at, + se: b.to, # + vec2(0,0.7), + sw: a.to # + vec2(0,0.7) + ) + ) + +proc trapFill(image: Image, t: Trapezoid, color: ColorRGBA) = + # assert t.nw.y == t.ne.y + # assert t.sw.y == t.se.y + + let + height = t.sw.y - t.nw.y + minY = clamp(t.nw.y, 0, image.height.float) + maxY = clamp(t.sw.y, 0, image.height.float) + for y in minY.int ..< maxY.int: + var yRate, minX, maxX: float32 + + yRate = clamp((y.float - t.nw.y) / height, 0, 1) + minX = clamp(lerp(t.nw.x, t.sw.x, yRate).round, 0, image.width.float) + maxX = clamp(lerp(t.ne.x, t.se.x, yRate).round, 0, image.width.float) + + for x in minX.int ..< maxX.int: + image.setRgbaUnsafe(x, y, color) + +proc drawTrapezoids(image: Image, trapezoids: seq[Trapezoid]) = + + for trapezoid in trapezoids: + image.trapFill(trapezoid, rgba(0, 0, 0, 255)) + + # for trapezoid in trapezoids: + # var p = newPath() + # p.moveTo(trapezoid.nw) + # p.lineTo(trapezoid.ne) + # p.lineTo(trapezoid.se) + # p.lineTo(trapezoid.sw) + # p.closePath() + # image.fillPath(p, rgba(0, 0, 0, 255)) + # image.strokePath(p, rgba(255, 0, 0, 255)) + +block: + # Rect + print "rect" + var image = newImage(200, 200) + image.fill(rgba(255, 255, 255, 255)) + + var p: Path + p.moveTo(50, 50) + p.lineTo(50, 150) + p.lineTo(150, 150) + p.lineTo(150, 50) + p.closePath() + + var trapezoids = p.pathToTrapezoids() + image.drawTrapezoids(trapezoids) + + image.writeFile("experiments/trapezoids/rect.png") + +block: + # Rhombus + print "rhombus" + var image = newImage(200, 200) + image.fill(rgba(255, 255, 255, 255)) + + var p: Path + p.moveTo(100, 50) + p.lineTo(150, 100) + p.lineTo(100, 150) + p.lineTo(50, 100) + p.closePath() + + var trapezoids = p.pathToTrapezoids() + image.drawTrapezoids(trapezoids) + + image.writeFile("experiments/trapezoids/rhombus.png") + +block: + # heart + print "heart" + var image = newImage(400, 400) + image.fill(rgba(255, 255, 255, 255)) + + var p = parsePath(""" + M 40 120 A 80 80 90 0 1 200 120 A 80 80 90 0 1 360 120 + Q 360 240 200 360 Q 40 240 40 120 z + """) + + var trapezoids = p.pathToTrapezoids() + image.drawTrapezoids(trapezoids) + + image.writeFile("experiments/trapezoids/heart.png") + +block: + # l + print "l" + var image = newImage(500, 800) + image.fill(rgba(255, 255, 255, 255)) + + var p = parsePath(""" + M 236 20 Q 150 22 114 57 T 78 166 V 790 L 171 806 V 181 Q 171 158 175 143 T 188 119 T 212 105.5 T 249 98 Z + """) + + #image.strokePath(p, rgba(0, 0, 0, 255)) + + var trapezoids = p.pathToTrapezoids() + image.drawTrapezoids(trapezoids) + + image.writeFile("experiments/trapezoids/l.png") + +block: + # g + print "g" + var image = newImage(500, 800) + image.fill(rgba(255, 255, 255, 255)) + + var p = parsePath(""" + M 406 538 Q 394 546 359.5 558.5 T 279 571 Q 232 571 190.5 556 T 118 509.5 T 69 431 T 51 319 Q 51 262 68 214.5 T 117.5 132.5 T 197 78.5 T 303 59 Q 368 59 416.5 68.5 T 498 86 V 550 Q 498 670 436 724 T 248 778 Q 199 778 155.5 770 T 80 751 L 97 670 Q 125 681 165.5 689.5 T 250 698 Q 333 698 369.5 665 T 406 560 V 538 Z M 405 152 Q 391 148 367.5 144.5 T 304 141 Q 229 141 188.5 190 T 148 320 Q 148 365 159.5 397 T 190.5 450 T 235.5 481 T 288 491 Q 325 491 356 480.5 T 405 456 V 152 Z + """) + + #image.strokePath(p, rgba(0, 0, 0, 255)) + + var trapezoids = p.pathToTrapezoids() + image.drawTrapezoids(trapezoids) + + image.writeFile("experiments/trapezoids/g.png") diff --git a/experiments/trapezoid2.nim b/experiments/trapezoid2.nim new file mode 100644 index 0000000..b0741db --- /dev/null +++ b/experiments/trapezoid2.nim @@ -0,0 +1,505 @@ + +import algorithm, bumpy, chroma, pixie/images, print, + sequtils, vmath, benchy + +import pixie, pixie/paths {.all.} + + +printColors = false + +proc pixelCover(a0, b0: Vec2): float32 = + ## Returns the amount of area a given segment sweeps to the right + ## in a [0,0 to 1,1] box. + var + a = a0 + b = b0 + aI: Vec2 + bI: Vec2 + area: float32 = 0.0 + + # # Sort A on top. + # if a.y > b.y: + # let tmp = a + # a = b + # b = tmp + + # if (b.y < 0 or a.y > 1) or # Above or bellow, no effect. + # (a.x >= 1 and b.x >= 1) or # To the right, no effect. + # (a.y == b.y): # Horizontal line, no effect. + # return 0 + + if (a.x < 0 and b.x < 0) or # Both to the left. + (a.x == b.x): # Vertical line + # Area of the rectangle: + return (1 - clamp(a.x, 0, 1)) * (min(b.y, 1) - max(a.y, 0)) + + else: + # y = mm*x + bb + let + mm: float32 = (b.y - a.y) / (b.x - a.x) + bb: float32 = a.y - mm * a.x + + if a.x >= 0 and a.x <= 1 and a.y >= 0 and a.y <= 1: + # A is in pixel bounds. + aI = a + else: + aI = vec2((0 - bb) / mm, 0) + if aI.x < 0: + let y = mm * 0 + bb + # Area of the extra rectangle. + area += (min(bb, 1) - max(a.y, 0)).clamp(0, 1) + aI = vec2(0, y.clamp(0, 1)) + elif aI.x > 1: + let y = mm * 1 + bb + aI = vec2(1, y.clamp(0, 1)) + + if b.x >= 0 and b.x <= 1 and b.y >= 0 and b.y <= 1: + # B is in pixel bounds. + bI = b + else: + bI = vec2((1 - bb) / mm, 1) + if bI.x < 0: + let y = mm * 0 + bb + # Area of the extra rectangle. + area += (min(b.y, 1) - max(bb, 0)).clamp(0, 1) + bI = vec2(0, y.clamp(0, 1)) + elif bI.x > 1: + let y = mm * 1 + bb + bI = vec2(1, y.clamp(0, 1)) + + area += ((1 - aI.x) + (1 - bI.x)) / 2 * (bI.y - aI.y) + return area + +proc intersectsInner*(a, b: Segment, at: var Vec2): bool {.inline.} = + ## Checks if the a segment intersects b segment. + ## If it returns true, at will have point of intersection + let + s1 = a.to - a.at + s2 = b.to - b.at + denominator = (-s2.x * s1.y + s1.x * s2.y) + s = (-s1.y * (a.at.x - b.at.x) + s1.x * (a.at.y - b.at.y)) / denominator + t = (s2.x * (a.at.y - b.at.y) - s2.y * (a.at.x - b.at.x)) / denominator + + if s > 0 and s < 1 and t >= 0 and t <= 1: + at = a.at + (t * s1) + return true + +type + + Trapezoid = object + nw, ne, se, sw: Vec2 + +proc roundBy*(v: Vec2, n: float32): Vec2 {.inline.} = + result.x = sign(v.x) * round(abs(v.x) / n) * n + result.y = sign(v.y) * round(abs(v.y) / n) * n + +proc fillPath2(mask: Mask, p: Path) = + + var polygons = p.commandsToShapes() + + const q = 1/256.0 + + # Creates segment q, quantize and remove horizontal lines. + var segments1: seq[Segment] + for shape in polygons: + for s in shape.segments: + var s = s + s.at = s.at.roundBy(q) + s.to = s.to.roundBy(q) + if s.at.y != s.to.y: + if s.at.y > s.to.y: + # make sure segments always are at.y higher + swap(s.at, s.to) + segments1.add(s) + segments1.sort(proc(a, b: Segment): int = cmp(a.at.y, b.at.y)) + + # Dumb way to compute cutLines + # var cutLines: seq[float32] + # for s in segments1: + # if s.at.y notin cutLines: + # cutLines.add s.at.y + # if s.to.y notin cutLines: + # cutLines.add s.to.y + # cutLines.sort() + + # Compute cutLines + var + cutLines: seq[float32] + last = segments1[0].at.y + bottom = segments1[0].to.y + cutLines.add(last) + for s in segments1: + if s.at.y != last: + last = s.at.y + cutLines.add(last) + if bottom < s.to.y: + bottom = s.to.y + cutLines.add(bottom) + #print cutLines + + var + sweeps = newSeq[seq[Segment]](cutLines.len - 1) # dont add bottom cutLine + lastSeg = 0 + + for i, sweep in sweeps.mpairs: + #print "sweep", i, cutLines[i] + while segments1[lastSeg].at.y == cutLines[i]: + let s = segments1[lastSeg] + #print s + if s.to.y != cutLines[i + 1]: + #print "needs cut?" + quit() + sweep.add(segments1[lastSeg]) + inc lastSeg + if lastSeg >= segments1.len: + # Sort the last sweep by X + break + # Sort the sweep by X + sweep.sort(proc(a, b: Segment): int = cmp(a.at.x, b.at.x)) + + proc fillCoverage(y: int, currCutLine: int, sweep: seq[Segment]) = + + + # var i = 0 + # let + # sweepHeight = cutLines[currCutLine + 1] - cutLines[currCutLine] + # yFracTop = (y.float - cutLines[currCutLine]) / sweepHeight + # yFracBottom = (y.float + 1 - cutLines[currCutLine]) / sweepHeight + # #print "cover", y, sweepHeight, yFrac + # while i < sweep.len: + # #print "fill", sweep[i].at.x, "..", sweep[i+1].at.x + # let + # minXf1 = mix(sweep[i+0].at.x, sweep[i+0].to.x, yFracTop) + # maxXf1 = mix(sweep[i+1].at.x, sweep[i+1].to.x, yFracTop) + + # minXf2 = mix(sweep[i+0].at.x, sweep[i+0].to.x, yFracBottom) + # maxXf2 = mix(sweep[i+1].at.x, sweep[i+1].to.x, yFracBottom) + + # minXi1 = minXf1.floor.int + # maxXi1 = maxXf1.floor.int + + # minXi2 = minXf2.floor.int + # maxXi2 = maxXf2.floor.int + + # for x in min(minXi1, minXi2) .. max(maxXi1, maxXi2): + # var a = 1.0f + # # if x < max(minXi1, minXi2): + # # a = 0.1 + # # elif x > min(maxXi1, maxXi2): + # # a = 0.1 + # # else: + # # a = 0.5 + # let backdrop = mask.getValueUnsafe(x, y) + # mask.setValueUnsafe(x, y, backdrop + (a * 255).uint8) + # i += 2 + + # x10 slower + # for x in 0 ..< mask.width: + # for i, seg in sweep: + # var area = pixelCover(seg.at - vec2(x.float32, y.float32), seg.to - vec2(x.float32, y.float32)) + # if i mod 2 == 1: + # area = -area + # let backdrop = mask.getValueUnsafe(x, y) + # mask.setValueUnsafe(x, y, backdrop + (area * 255).uint8) + + let quality = 5 + for m in 0 ..< quality: + let + sweepHeight = cutLines[currCutLine + 1] - cutLines[currCutLine] + yFrac = (y.float32 + (m.float32 / quality.float32) - cutLines[currCutLine]) / sweepHeight + if yFrac < 0.0 or yFrac >= 1.0: + continue + var i = 0 + while i < sweep.len: + let + minXf1 = mix(sweep[i+0].at.x, sweep[i+0].to.x, yFrac) + maxXf1 = mix(sweep[i+1].at.x, sweep[i+1].to.x, yFrac) + minXi1 = minXf1.int + maxXi1 = maxXf1.int + for x in minXi1 ..< maxXi1: + let backdrop = mask.getValueUnsafe(x, y) + mask.setValueUnsafe(x, y, backdrop + (255 div quality).uint8) + # if x == 100 and y == 165: + # print backdrop, 255 div quality + # print mask.getValueUnsafe(x, y) + i += 2 + + # let + # sweepHeight = cutLines[currCutLine + 1] - cutLines[currCutLine] + # yFrac = (y.float32 - cutLines[currCutLine]) / sweepHeight + # var i = 0 + # while i < sweep.len: + # let + # minXf1 = mix(sweep[i+0].at.x, sweep[i+0].to.x, yFrac) + # maxXf1 = mix(sweep[i+1].at.x, sweep[i+1].to.x, yFrac) + # minXi1 = minXf1.floor.int + # maxXi1 = maxXf1.floor.int + # for x in minXi1 .. maxXi1: + # mask.setValueUnsafe(x, y, 255) + # i += 2 + + var + currCutLine = 0 + for scanLine in cutLines[0].int ..< cutLines[^1].ceil.int: + print scanLine, "..<", scanLine + 1 + print " ", currCutLine, cutLines[currCutLine], "..<", cutLines[currCutLine + 1] + fillCoverage(scanLine, currCutLine, sweeps[currCutLine]) + while cutLines[currCutLine + 1] < scanLine.float + 1.0: + inc currCutLine + print " ", currCutLine, cutLines[currCutLine], "..<", cutLines[currCutLine + 1] + fillCoverage(scanLine, currCutLine, sweeps[currCutLine]) + + # print sweeps[^1] + # print cutLines + + + + + # var segments: seq[Segment] + # while segments1.len > 0: + # #print segments1.len, segments.len + # var s = segments1.pop() + # var collision = false + # for y in cutLines: + # let scanLine = line(vec2(0, y), vec2(1, y)) + # var at: Vec2 + # if intersects(scanLine, s, at): + # at = at.roundBy(q) + # at.y = y + # if s.at.y != at.y and s.to.y != at.y: + # #print "seg2yline intersects!", a, y, at + # collision = true + # var s1 = segment(s.at, at) + # var s2 = segment(at, s.to) + # #print s.length, "->", s1.length, s2.length + # segments1.add(s1) + # segments1.add(s2) + # break + + # if not collision: + # # means its touching, not intersecting + # segments.add(s) + + # # sort at/to in segments + # # for s in segments.mitems: + # # if s.at.y > s.to.y: + # # swap(s.at, s.to) + + + # #let blender = blendMode.blender() + + # for yScanLine in cutLines[0..^2]: + + # var scanSegments: seq[Segment] + # for s in segments: + # if s.at.y == yScanLine: + # scanSegments.add(s) + # scanSegments.sort(proc(a, b: Segment): int = + # cmp(a.at.x, b.at.x)) + + # # if scanSegments.len mod 2 != 0: + # # print "error???" + # # print yScanLine + # # print scanSegments + # # quit() + + # # TODO: winding rules will go here + + # var trapezoids: seq[Trapezoid] + # for i in 0 ..< scanSegments.len div 2: + # let + # a = scanSegments[i*2+0] + # b = scanSegments[i*2+1] + + # assert a.at.y == b.at.y + # assert a.to.y == b.to.y + # #assert a.at.x < b.at.x + # #assert a.to.x < b.to.x + + # trapezoids.add(Trapezoid( + # nw: a.at, + # ne: b.at, + # se: b.to, # + vec2(0,0.7), + # sw: a.to # + vec2(0,0.7) + # )) + + # var i = 0 + # while i < trapezoids.len: + + # let t = trapezoids[i] + # # print t + # let + # nw = t.nw + # ne = t.ne + # se = t.se + # sw = t.sw + + # let + # height = sw.y - nw.y + # minYf = nw.y + # maxYf = sw.y + # minYi = minYf.floor.int + # maxYi = maxYf.floor.int + + # # print t + + # for y in minYi .. maxYi: + # let + # yFrac = (y.float - nw.y) / height + # minXf = mix(nw.x, sw.x, yFrac) + # maxXf = mix(ne.x, se.x, yFrac) + # minXi = minXf.floor.int + # maxXi = maxXf.floor.int + # #print yFrac + # # if not(minY.int == 58 or maxY.int == 58) or minX > 100: + # # continue + + # var ay: float32 + # if y == minYi and y == maxYi: + # ay = maxYf - minYf + # # print "middle", maxYf, minYf, a + # #print "double", y, a, minY, maxY, round(a * 255) + # elif y == minYi: + # ay = (1 - (minYf - float32(minYi))) + # # print "min y", minYf, minYi, a + # #print "s", y, a, minY, round(a * 255) + # elif y == maxYi: + # ay = (maxYf - float32(maxYi)) + # #print "max y", maxYf, maxYi, a + # # print "e", y, a, maxY, round(a * 255) + # else: + # ay = 1.0 + + # for x in minXi .. maxXi: + # var ax: float32 + # # if x == minXi: + # # a2 = (1 - (minXf - float32(minXi))) + # # #a2 = 1.0 + # # elif x == maxXi: + # # a2 = (maxXf - float32(maxXi)) + # # #a2 = 1.0 + # # else: + # # a2 = 1.0 + + # if x.float32 < max(nw.x, sw.x): + # ax = 0.5 + # elif x.float32 > min(ne.x, se.x): + # ax = 0.25 + # else: + # ax = 1.0 + + # let backdrop = mask.getValueUnsafe(x, y) + # mask.setValueUnsafe(x, y, backdrop + floor(255 * ay * ax).uint8) + # # if x == 100 and y == 172: + # # print backdrop, round(255 * a * a2).uint8 + # # print mask.getValueUnsafe(x, y) + + # inc i + +block: + # Rect + print "rect" + #var image = newImage(200, 200) + + # var p = Path() + # p.moveTo(50.25, 50.25) + # p.lineTo(50.25, 150.25) + # p.lineTo(150.25, 150.25) + # p.lineTo(150.25, 50.25) + # p.closePath() + + var p = parsePath(""" + M 20 60 + A 40 40 90 0 1 100 60 + A 40 40 90 0 1 180 60 + Q 180 120 100 180 + Q 20 120 20 60 + z + """) + + # image.fill(rgba(255, 255, 255, 255)) + #image.fillPath2(p, color(0, 0, 0, 1)) + + var mask = newMask(200, 200) + timeIt "rect trapezoids", 1: + #for i in 0 ..< 100: + mask.fill(0) + mask.fillPath2(p) + #image.fillPath2(p, color(0, 0, 0, 1)) + mask.writeFile("experiments/trapezoids/rect_trapesoid.png") + + var mask2 = newMask(200, 200) + timeIt "rect normal", 1: + #for i in 0 ..< 100: + mask2.fill(0) + mask2.fillPath(p) + mask2.writeFile("experiments/trapezoids/rect_scanline.png") + + let (score, image) = diff(mask.newImage, mask2.newImage) + print score + image.writeFile("experiments/trapezoids/rect_diff.png") + + + +# block: +# # Rhombus +# print "rhombus" +# var image = newImage(200, 200) +# image.fill(rgba(255, 255, 255, 255)) + +# var p = Path() +# p.moveTo(100, 50) +# p.lineTo(150, 100) +# p.lineTo(100, 150) +# p.lineTo(50, 100) +# p.closePath() + +# image.fillPath2(p, color(0, 0, 0, 1)) + +# image.writeFile("experiments/trapezoids/rhombus.png") + +# block: +# # heart +# print "heart" +# var image = newImage(400, 400) +# image.fill(rgba(0, 0, 0, 0)) + +# var p = parsePath(""" +# M 40 120 A 80 80 90 0 1 200 120 A 80 80 90 0 1 360 120 +# Q 360 240 200 360 Q 40 240 40 120 z +# """) + +# var mask = newMask(image) +# mask.fillPath2(p) + +# image.draw(mask, blendMode = bmOverwrite) + +# image.writeFile("experiments/trapezoids/heart.png") + +# block: +# # l +# print "l" +# var image = newImage(500, 800) +# image.fill(rgba(255, 255, 255, 255)) + +# var p = parsePath(""" +# M 236 20 Q 150 22 114 57 T 78 166 V 790 L 171 806 V 181 Q 171 158 175 143 T 188 119 T 212 105.5 T 249 98 Z +# """) + +# image.fillPath2(p, color(0, 0, 0, 1)) + +# image.writeFile("experiments/trapezoids/l.png") + +# block: +# # g +# print "g" +# var image = newImage(500, 800) +# image.fill(rgba(255, 255, 255, 255)) + +# var p = parsePath(""" +# M 406 538 Q 394 546 359.5 558.5 T 279 571 Q 232 571 190.5 556 T 118 509.5 T 69 431 T 51 319 Q 51 262 68 214.5 T 117.5 132.5 T 197 78.5 T 303 59 Q 368 59 416.5 68.5 T 498 86 V 550 Q 498 670 436 724 T 248 778 Q 199 778 155.5 770 T 80 751 L 97 670 Q 125 681 165.5 689.5 T 250 698 Q 333 698 369.5 665 T 406 560 V 538 Z M 405 152 Q 391 148 367.5 144.5 T 304 141 Q 229 141 188.5 190 T 148 320 Q 148 365 159.5 397 T 190.5 450 T 235.5 481 T 288 491 Q 325 491 356 480.5 T 405 456 V 152 Z +# """) + +# image.fillPath2(p, color(0, 0, 0, 1)) + +# image.writeFile("experiments/trapezoids/g.png") diff --git a/experiments/trapezoids/g.png b/experiments/trapezoids/g.png index fbfafa0f4f8d84a70aa006635672ead7ba412551..5b1c9b980e7a95a405da92c929ca50a9bb7020aa 100644 GIT binary patch literal 28721 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 diff --git a/experiments/trapezoids/heart.png b/experiments/trapezoids/heart.png index 88c664995e57aaf65b778925dd0b67c1662cc57c..6e16b15f81f2e2716da99f24fee75348223551df 100644 GIT binary patch literal 1588 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 BIZyxq literal 7739 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 diff --git a/experiments/trapezoids/output_diff.png b/experiments/trapezoids/output_diff.png new file mode 100644 index 0000000000000000000000000000000000000000..daf009a462c65966ba927af6a3acf9ec339a253b GIT binary patch literal 3680 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 F<)2K_itqpc literal 0 HcmV?d00001 diff --git a/experiments/trapezoids/output_scanline.png b/experiments/trapezoids/output_scanline.png new file mode 100644 index 0000000000000000000000000000000000000000..768cc115583435960308adf383435caf22233924 GIT binary patch literal 4767 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 literal 0 HcmV?d00001 diff --git a/experiments/trapezoids/rect.png b/experiments/trapezoids/rect.png index 36f2913c76ef3d004960747655f1eeac488fa6ca..ac52c7a505082c39b143d1936f4f5cbed2b52a5f 100644 GIT binary patch literal 2357 zcmai$c|6mPAII04%-I}m?yo+`F)Jc-rHzf{YUGMZL>T%KQzAKvIf@aHa+GTODJA8K zaHJWd#?#kZC8y;dtF4B43q$P=YYK1R6K{t& z+zL9<|9X1V@EuFc-RdCi@%uFEpI4H9RuH*8V~cA=#9wl-#*Kdp0f)Mfh3zhVj%0*Y zB29VYuQv*Dp<}bmyG5|+N%_XGAAH#hC(Z4xrT!T1JsymGu-_Cv%Kh!5=4QSb|K~=g z`?hAE@CyFn3WIpI(6gR%!*$xBEjnb2;-{Ba*4mSFP@7;Y;OgbjB^hgSk(8JvL1Z8uNAO z0w;geGA2gP^$thwdXY&5`)P;w)T@>61K~z%zJ0+>@wYR6ZLAkc$lEr zU40GRs7SHe8%{Htt6x7OEn|&)e3e`}ZJ*g1kp5t=!P%1ZV?6;`~j| z%;JUIX&Ve4&c_3fb$1frQSYhO;=TCX0CTHTH*o9=payf`P-4og<&gQc~;#2PCnhP06=!9M&!$E+b>TZ&#Mj(e#U{)DCV z5#86o{1x?kGEl{P66>DcsT2F4=b}u}k{i!gBy^@NE8sOWd%km0f?X=p3@Kz$9}1Wd zt4xDau}O^%kpi#)i+BX`$O1t}2FuXxQOxQhAnMqWY7;}tK>+5}%6XFXIsl0cEkc1G z=~ZLqm;ByU|21)cl?QwnlxqIGESbn*hb+lVl__oJ>818p zh)f!}-7~8*uv?KY1Psbu_me)($MdC81ojI@5nZh4q!K!dlGWNf5KbD88_x`qG66jA zD9S$?l+rN|$DJb<9Aea7g!#~r1?2l$s_@!;<5(1Ux{upH)_<%2{j3Bi&pulVH5id? zHdqJ6xhsrTJ>R!tyb4Sb*A?$x*g?y#0x*k62To&TcJ$^zp$+8l^Zeo%xmSvBgF8Su zgzL$6A9Uio{KeH|sPH-oX%3}Dw*i+Ya(wJq&&&pd1i+9#lsp4QaL)@DGcbPRg{t3< zA`sC(?IWv3G?5Zdp)qiB)H^*Nx@t4f>r=GgK=v9-R~Uz9AOe>M+vCm+`QFuI8>f-Xc+{tzFkh zW(>NlFxurHt)yk;Mt-z~haXIcyMa?L{W$dKLd&ZVoFgP4OF#)5K>*s+Uemmar%U3r21&7xvDN2>H0??E)#WDoS)>ZFkL0$XQIMarLs0ITL1Rb3lw zBUYcN@J-!-@=Ze*ymk1mCgY{s=+tM>&Rbq=nL0b(sbZo)@2W9>Ve~T!UGOnSPR7*3LFftD`_O1jMJ1!vrM=63V*7qOF6t0i z=GE-8@u7#0rzePzdl}m{L38ZkrW;XuU?*camd{Nj`iB>!9TIW$*I+6Ja!;j9yTdsL znc*&0+O+t>`JOlL`!5qRVW6`vS?#zXKM}c$I5A67j1#8DsK0l zqT%=7W1@$Tbchr{Rra$sTbxQZn@2#!&=`A?5!tf7XA!FnHXNT_-#0Yt4Y8KtB1k@B z_SHt^#LU**-m9LNizDe+LlB5alMEYXWabT5Sjr0^Vz7&g(Rl6d#`g6JcrwADwPr2k zfL$(Z3h4D{`c#+FQ4(&0t3V{rw|ST7@c@%OvJL9s)aP$eNU`5u?@;N=K(6G1PEer{ z$@aT%P!t|g9=FsyC!L*8)4MJ%bvOfK0r>)c!{_#}qZ}Xs*OYU411(w2gp@EKa%r&* z&01Rlg;mVR{~;ttHvnC>#%nZe>whsT7(o%r|^_RlORSR76sY^n*-{M-IRAM6gl`iT~eO`2E#0 VVNz(B@$FwfbQ153tF}Iu@?RbWQcM5< literal 921 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fjPm`#WAFU@$H?1Tulxl4i^u} z%kSTl5xYmBE3b_2j9W;_oNn%eZ;N)uh<&(FcDss6PKbktg^9WGfP;d80BxBCF3Zbr zUW`4b zj=?Xq??p-e+S2_W*>hhjly7?fo8Rw$p=?SdXUlm3j~?@Z3F|!mBM~z#ExHc>nJsST zNFZhY*(mIvo4xjHiT;mZrp88x1O)*J4i+|=Fk=`W_Evt~7N?vB%yJB#u6{1-oD!M< DoJS3< diff --git a/experiments/trapezoids/rhombus.png b/experiments/trapezoids/rhombus.png index 5f7e184311673bfeaf92b1eeb164f833da0420df..26fdf01215de27335c0e0d57224fb79d52c49e69 100644 GIT binary patch delta 1021 zcmYk5e@qi+7{^)dM!R*;a)?EarrcfVI+9Ilkz0j&j4Qp8&IHCt9Aa?kbS`$J$T}EE zsV3M8yWUl)Ba6j^R^#kC1tPEovMr3Z3}t{y0B6|IW^Z;^xQZ6tpDwF(XN4Vg*(Nq}$bh zR078`M3uL3VRGI>>{Lqh`EDzJo`~Qob~uljem_ZS?KL&KX(RTb(C5ywm~6;Zy?py{ z4+Zvo zy+|C2?@t<^5Z(6rKIsL<#~aGzpN{C`+1U3$2HxcQO><| z)K7T?By5X)krCOjI|jVlkv@&sa;LZrTX+tktw(6nG%-2%Aix@TIPgSYq5CRNqPGp{ zRBy;RX~@VLAu92fx>TC@`rs>nE2_e3JBB=Lyo>|RK+ew|w_pp;x*fjp>^17L2$LB0 z4o$Jf913wKj?ieIh9R-d8JXUuIw^Nj({*{Y$w_3e=Z7lchze9%YV_kkuYp|u>MjQw zz$B3;wLPFCkmsr^qD@PHzCY3gN9F-NX5Pfzv2B3v$F;&}H#Om0`;6!iN6KKEAnNKx zZ#4a-Uz04zgtq_BboHelVye}^6awB$ z>cnLzlX@!?oC-!UBX;6mlu?YIz_=DKcypR{tQGNE=l1FldJ#;;M-CF+5}2;AD7a~! zvSE^%tm*<|q|C6KN10O0y%N;5%ew+p(ptB z2Y}WD6ix{ONreKFcps1g{uGmGSeBk)4uPY}rg`P3frcvf62f+Bv)WVY|K-M` S-QR7bfMw9_Cxfpyoca%^6Sh78 delta 1024 zcmYjQeM}o=7@y@FEmqcR8+6cMy&fNBb0Zx}y8%6*(CaoZ?G_wUHj$2xMA2gd83D?T z46dfT+!b^*j3S|7QxiXm!J#ledZBcTF%Z*@v4CwCs)!6CGfJ}f_CMd`&6E6|-|zQ4 z&ztvoB=|$Xpf3*_bh*bHpIW;=MYqeJZ!`AZxZbi^JSlX%zWq&!C}st%wdIaAmk+M2 zZZ(yWy|-)Dd5dV*0Ho=ffu8yQ74ab+uUvOTxl&)|H?{v*%@XmJbD?twU%EVXW`oGr zLUUHBKH1*NcSUJ~le)QRGNWM+eH2|2xCT-|d`IA7s{$ZFWAS&zfcXkk_vLE;#ATKgM zo#a@nD~puQt3a7E0vD4ATun3<6IMn5zWHh*Xvp1A3}F`KL&Jn%gKsmmv4v!1Y|Te`ptX zljz4k8jXUz6~lVfS`0$3Ov`-TBvtWUslu^OM2yu=;6pJw+4gjtFN358uQ(6~A|kpo zkD#7tgl@7_Qsywm8JEJWFo9Cj2kOxLod_Mg6z_Wv(EDJ;?lckcb>>Ob10i&)P3lW0 zU8f6vZXXz5-CAfBUldAk#QwPO1HfZ+=$b7~uX;mn#p%qqWIl#O5zXyu1nSvor(8GK zAl?y+!iMZYFDrZj$Tlk}i^72b-NRdAy{ZW49``C~^QV}Y9jhHt+kfL3bnXW>0zC!{ zLg$Ak*rN$R=dGj_j)Oq<_jICO)dFNsB;FLV8TMro&63GoNGZ3`i9PAji1&Cc;B6;x zj_Aey>`{t|#T4cTAuF|3?Dn58^Vct0rta^5;GBBswtO60_Ju#T>X+xpta1+LHe_U0 zjSIvq^6m6IG;p_2o-lqE#A5mWMlZnQ$+2)B0((~>x95-!p<5vLu^E=C(2~+S6=)ze zUw-i~sa1Uw#&fel+6|dbOyQi-iKSJ;R`u^9JV{@O(3MS)X%=Rw!F}ZMk^^F5K81;z zr*tx}XfpRO$Ori$ED2Qk3jFkJmI^lVYL-95x!DdZ{+`&XUV!oZ8Q?ww&7A~(@LdwE0@j0#Vx@+KaA()|Py$%RXkqfmqdQ>mtzV7lN_JMtI#ISQH7;kzwb{V< Tzy*>1&