pixie/experiments/sweeps.nim
2022-02-13 20:02:37 -08:00

750 lines
21 KiB
Nim

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 = OverwriteBlend)
# 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")