Merge pull request #330 from treeform/sweeps2

Sweeps: Fix jugged outline and streaking.
This commit is contained in:
treeform 2021-12-04 18:14:21 -08:00 committed by GitHub
commit bc631f649c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 240 additions and 70 deletions

View file

@ -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))

View file

@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -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

View file

@ -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
)