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