|
@ -188,98 +188,6 @@ proc `$`*(path: Path): string =
|
||||||
if i != path.commands.len - 1 or j != command.numbers.len - 1:
|
if i != path.commands.len - 1 or j != command.numbers.len - 1:
|
||||||
result.add " "
|
result.add " "
|
||||||
|
|
||||||
## See https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
|
||||||
|
|
||||||
type ArcParams = object
|
|
||||||
s: float32
|
|
||||||
rx, ry: float32
|
|
||||||
rotation: float32
|
|
||||||
cx, cy: float32
|
|
||||||
theta, delta: float32
|
|
||||||
|
|
||||||
proc svgAngle (ux, uy, vx, vy: float32): float32 =
|
|
||||||
var u = vec2(ux, uy)
|
|
||||||
var v = vec2(vx, vy)
|
|
||||||
# (F.6.5.4)
|
|
||||||
var dot = dot(u,v)
|
|
||||||
var len = length(u) * length(v)
|
|
||||||
var ang = arccos( clamp(dot / len,-1,1) ) # floating point precision, slightly over values appear
|
|
||||||
if (u.x*v.y - u.y*v.x) < 0:
|
|
||||||
ang = -ang
|
|
||||||
return ang
|
|
||||||
|
|
||||||
proc endpointToCenterArcParams(
|
|
||||||
ax, ay, rx, ry, rotation, large, sweep, bx, by: float32
|
|
||||||
): ArcParams =
|
|
||||||
|
|
||||||
var r = vec2(rx, ry)
|
|
||||||
var p1 = vec2(ax, ay)
|
|
||||||
var p2 = vec2(bx, by)
|
|
||||||
var xAngle = rotation/180*PI
|
|
||||||
var flagA = large == 1.0
|
|
||||||
var flagS = sweep == 1.0
|
|
||||||
var rX = abs(r.x)
|
|
||||||
var rY = abs(r.y)
|
|
||||||
|
|
||||||
# (F.6.5.1)
|
|
||||||
var dx2 = (p1.x - p2.x) / 2.0
|
|
||||||
var dy2 = (p1.y - p2.y) / 2.0
|
|
||||||
var x1p = cos(xAngle)*dx2 + sin(xAngle)*dy2
|
|
||||||
var y1p = -sin(xAngle)*dx2 + cos(xAngle)*dy2
|
|
||||||
|
|
||||||
# (F.6.5.2)
|
|
||||||
var rxs = rX * rX
|
|
||||||
var rys = rY * rY
|
|
||||||
var x1ps = x1p * x1p
|
|
||||||
var y1ps = y1p * y1p
|
|
||||||
# check if the radius is too small `pq < 0`, when `dq > rxs * rys` (see below)
|
|
||||||
# cr is the ratio (dq : rxs * rys)
|
|
||||||
var cr = x1ps/rxs + y1ps/rys
|
|
||||||
var s = 1.0
|
|
||||||
if cr > 1:
|
|
||||||
# scale up rX,rY equally so cr == 1
|
|
||||||
s = sqrt(cr)
|
|
||||||
rX = s * rX
|
|
||||||
rY = s * rY
|
|
||||||
rxs = rX * rX
|
|
||||||
rys = rY * rY
|
|
||||||
|
|
||||||
var dq = (rxs * y1ps + rys * x1ps)
|
|
||||||
var pq = (rxs*rys - dq) / dq
|
|
||||||
var q = sqrt(max(0,pq)) # use Max to account for float precision
|
|
||||||
if flagA == flagS:
|
|
||||||
q = -q
|
|
||||||
var cxp = q * rX * y1p / rY
|
|
||||||
var cyp = - q * rY * x1p / rX
|
|
||||||
|
|
||||||
# (F.6.5.3)
|
|
||||||
var cx = cos(xAngle)*cxp - sin(xAngle)*cyp + (p1.x + p2.x)/2
|
|
||||||
var cy = sin(xAngle)*cxp + cos(xAngle)*cyp + (p1.y + p2.y)/2
|
|
||||||
|
|
||||||
# (F.6.5.5)
|
|
||||||
var theta = svgAngle( 1,0, (x1p-cxp) / rX, (y1p - cyp)/rY )
|
|
||||||
# (F.6.5.6)
|
|
||||||
var delta = svgAngle(
|
|
||||||
(x1p - cxp)/rX, (y1p - cyp)/rY,
|
|
||||||
(-x1p - cxp)/rX, (-y1p-cyp)/rY)
|
|
||||||
delta = delta mod (PI * 2)
|
|
||||||
|
|
||||||
if not flagS:
|
|
||||||
delta -= 2 * PI
|
|
||||||
|
|
||||||
# normalize the delta
|
|
||||||
while delta > PI*2:
|
|
||||||
delta -= PI*2
|
|
||||||
while delta < -PI*2:
|
|
||||||
delta += PI*2
|
|
||||||
|
|
||||||
r = vec2(rX, rY)
|
|
||||||
|
|
||||||
return ArcParams(
|
|
||||||
s: s, rx: rX, rY: ry, rotation: xAngle, cx: cx, cy: cy,
|
|
||||||
theta: theta, delta: delta
|
|
||||||
)
|
|
||||||
|
|
||||||
proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] =
|
proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] =
|
||||||
## Converts SVG-like commands to simpler polygon
|
## Converts SVG-like commands to simpler polygon
|
||||||
|
|
||||||
|
@ -325,25 +233,141 @@ proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] =
|
||||||
|
|
||||||
discretize(1, 1)
|
discretize(1, 1)
|
||||||
|
|
||||||
proc drawQuad(p0, p1, p2: Vec2) =
|
proc drawQuad(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
|
||||||
|
|
||||||
|
var prev = at
|
||||||
|
|
||||||
|
proc discretize(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 >= 0.25:
|
||||||
|
# Error too large, double precision for this step
|
||||||
|
discretize(i * 2 - 1, steps * 2)
|
||||||
|
discretize(i * 2, steps * 2)
|
||||||
|
else:
|
||||||
|
drawLine(prev, next)
|
||||||
|
prev = next
|
||||||
|
|
||||||
|
discretize(1, 1)
|
||||||
|
|
||||||
|
proc drawArc(
|
||||||
|
at, radii: Vec2,
|
||||||
|
rotation: float32,
|
||||||
|
large, sweep: bool,
|
||||||
|
to: Vec2
|
||||||
|
) =
|
||||||
|
type ArcParams = object
|
||||||
|
radii: Vec2
|
||||||
|
rotMat: Mat3
|
||||||
|
center: Vec2
|
||||||
|
theta, delta: float32
|
||||||
|
|
||||||
|
proc endpointToCenterArcParams(
|
||||||
|
at, radii: Vec2, rotation: float32, large, sweep: bool, to: Vec2
|
||||||
|
): ArcParams =
|
||||||
|
var
|
||||||
|
radii = vec2(abs(radii.x), abs(radii.y))
|
||||||
|
radiiSq = vec2(radii.x * radii.x, radii.y * radii.y)
|
||||||
|
|
||||||
|
let
|
||||||
|
radians = rotation / 180 * PI
|
||||||
|
d = vec2((at.x - to.x) / 2.0, (at.y - to.y) / 2.0)
|
||||||
|
p = vec2(
|
||||||
|
cos(radians) * d.x + sin(radians) * d.y,
|
||||||
|
-sin(radians) * d.x + cos(radians) * d.y
|
||||||
|
)
|
||||||
|
pSq = vec2(p.x * p.x, p.y * p.y)
|
||||||
|
|
||||||
|
let cr = pSq.x / radiiSq.x + pSq.y / radiiSq.y
|
||||||
|
if cr > 1:
|
||||||
|
radii *= sqrt(cr)
|
||||||
|
radiiSq = vec2(radii.x * radii.x, radii.y * radii.y)
|
||||||
|
|
||||||
|
let
|
||||||
|
dq = radiiSq.x * pSq.y + radiiSq.y * pSq.x
|
||||||
|
pq = (radiiSq.x * radiiSq.y - dq) / dq
|
||||||
|
|
||||||
|
var q = sqrt(max(0, pq))
|
||||||
|
if large == sweep:
|
||||||
|
q = -q
|
||||||
|
|
||||||
|
proc svgAngle(u, v: Vec2): float32 =
|
||||||
|
let
|
||||||
|
dot = dot(u,v)
|
||||||
|
len = length(u) * length(v)
|
||||||
|
result = arccos(clamp(dot / len, -1, 1))
|
||||||
|
if (u.x * v.y - u.y * v.x) < 0:
|
||||||
|
result = -result
|
||||||
|
|
||||||
|
let
|
||||||
|
cp = vec2(q * radii.x * p.y / radii.y, -q * radii.y * p.x / radii.x)
|
||||||
|
center = vec2(
|
||||||
|
cos(radians) * cp.x - sin(radians) * cp.y + (at.x + to.x) / 2,
|
||||||
|
sin(radians) * cp.x + cos(radians) * cp.y + (at.y + to.y) / 2
|
||||||
|
)
|
||||||
|
theta = svgAngle(vec2(1, 0), vec2((p.x-cp.x) / radii.x, (p.y - cp.y) / radii.y))
|
||||||
|
|
||||||
|
var delta = svgAngle(
|
||||||
|
vec2((p.x - cp.x) / radii.x, (p.y - cp.y) / radii.y),
|
||||||
|
vec2((-p.x - cp.x) / radii.x, (-p.y - cp.y) / radii.y)
|
||||||
|
)
|
||||||
|
delta = delta mod (PI * 2)
|
||||||
|
|
||||||
|
if not sweep:
|
||||||
|
delta -= 2 * PI
|
||||||
|
|
||||||
|
# Normalize the delta
|
||||||
|
while delta > PI * 2:
|
||||||
|
delta -= PI * 2
|
||||||
|
while delta < -PI * 2:
|
||||||
|
delta += PI * 2
|
||||||
|
|
||||||
|
ArcParams(
|
||||||
|
radii: radii,
|
||||||
|
rotMat: rotationMat3(-radians),
|
||||||
|
center: center,
|
||||||
|
theta: theta,
|
||||||
|
delta: delta
|
||||||
|
)
|
||||||
|
|
||||||
|
proc compute(arc: ArcParams, a: float32): Vec2 =
|
||||||
|
result = vec2(cos(a) * arc.radii.x, sin(a) * arc.radii.y)
|
||||||
|
result = arc.rotMat * result + arc.center
|
||||||
|
|
||||||
|
var prev = at
|
||||||
|
|
||||||
|
proc discretize(arc: ArcParams, i, steps: int) =
|
||||||
|
let
|
||||||
|
step = arc.delta / steps.float32
|
||||||
|
aPrev = arc.theta + step * (i - 1).float32
|
||||||
|
a = arc.theta + step * i.float32
|
||||||
|
next = arc.compute(a)
|
||||||
|
halfway = arc.compute(aPrev + (a - aPrev) / 2)
|
||||||
|
midpoint = (prev + next) / 2
|
||||||
|
error = (midpoint - halfway).length
|
||||||
|
|
||||||
|
if error >= 0.25:
|
||||||
|
# Error too large, try again with doubled precision
|
||||||
|
discretize(arc, i * 2 - 1, steps * 2)
|
||||||
|
discretize(arc, i * 2, steps * 2)
|
||||||
|
else:
|
||||||
|
drawLine(prev, next)
|
||||||
|
prev = next
|
||||||
|
|
||||||
|
let arc = endpointToCenterArcParams(at, radii, rotation, large, sweep, to)
|
||||||
|
discretize(arc, 1, 1)
|
||||||
|
|
||||||
for command in commands:
|
for command in commands:
|
||||||
case command.kind
|
case command.kind
|
||||||
|
@ -409,34 +433,20 @@ proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] =
|
||||||
at = to
|
at = to
|
||||||
|
|
||||||
of Arc:
|
of Arc:
|
||||||
var arc = endpointToCenterArcParams(
|
let
|
||||||
at.x,
|
radii = vec2(command.numbers[0], command.numbers[1])
|
||||||
at.y,
|
rotation = command.numbers[2]
|
||||||
command.numbers[0],
|
large = command.numbers[3] == 1
|
||||||
command.numbers[1],
|
sweep = command.numbers[4] == 1
|
||||||
command.numbers[2],
|
to = vec2(command.numbers[5], command.numbers[6])
|
||||||
command.numbers[3],
|
drawArc(at, radii, rotation, large, sweep, to)
|
||||||
command.numbers[4],
|
at = to
|
||||||
command.numbers[5],
|
|
||||||
command.numbers[6],
|
|
||||||
)
|
|
||||||
let steps = int(abs(arc.delta)/PI*180/5)
|
|
||||||
let step = arc.delta / steps.float32
|
|
||||||
var a = arc.theta
|
|
||||||
var rotMat = rotationMat3(-arc.rotation)
|
|
||||||
for i in 0 .. steps:
|
|
||||||
polygon.add(rotMat * vec2(
|
|
||||||
cos(a)*arc.rx,
|
|
||||||
sin(a)*arc.ry) + vec2(arc.cx, arc.cy)
|
|
||||||
)
|
|
||||||
a += step
|
|
||||||
at = polygon[^1]
|
|
||||||
|
|
||||||
of Close:
|
of Close:
|
||||||
assert command.numbers.len == 0
|
assert command.numbers.len == 0
|
||||||
if at != start:
|
if at != start:
|
||||||
if prevCommand == Quad or prevCommand == TQuad:
|
if prevCommand == Quad or prevCommand == TQuad:
|
||||||
drawQuad(at, ctr, start)
|
drawQuad(at, ctr, start)
|
||||||
else:
|
else:
|
||||||
drawLine(at, start)
|
drawLine(at, start)
|
||||||
if polygon.len > 0:
|
if polygon.len > 0:
|
||||||
|
@ -865,56 +875,52 @@ proc arc*(path: Path) =
|
||||||
proc arcTo*(path: Path, x1, y1, x2, y2, r: float32) =
|
proc arcTo*(path: Path, x1, y1, x2, y2, r: float32) =
|
||||||
## Adds a circular arc to the path with the given control points and radius, connected to the previous point by a straight line.
|
## Adds a circular arc to the path with the given control points and radius, connected to the previous point by a straight line.
|
||||||
|
|
||||||
var
|
const epsilon = 1e-6.float32
|
||||||
x0 = path.at.x
|
|
||||||
y0 = path.at.y
|
|
||||||
x21 = x2 - x1
|
|
||||||
y21 = y2 - y1
|
|
||||||
x01 = x0 - x1
|
|
||||||
y01 = y0 - y1
|
|
||||||
l01_2 = x01 * x01 + y01 * y01
|
|
||||||
|
|
||||||
const epsilon: float32 = 1e-6
|
if path.commands.len == 0:
|
||||||
|
# Is this path empty? Move to (x1,y1).
|
||||||
|
path.moveTo(x1, y1)
|
||||||
|
|
||||||
var r = r
|
var r = r
|
||||||
if r < 0:
|
if r < 0:
|
||||||
# Is the radius negative? Flip it.
|
# Is the radius negative? Flip it.
|
||||||
r = -r
|
r = -r
|
||||||
|
|
||||||
if path.commands.len == 0:
|
var
|
||||||
# Is this path empty? Move to (x1,y1).
|
x21 = x2 - x1
|
||||||
path.commands.add(PathCommand(kind: Move, numbers: @[x1,y1]))
|
y21 = y2 - y1
|
||||||
|
x01 = path.at.x - x1
|
||||||
|
y01 = path.at.y - y1
|
||||||
|
l01_2 = x01 * x01 + y01 * y01
|
||||||
|
|
||||||
elif not (l01_2 > epsilon):
|
if not (l01_2 > epsilon):
|
||||||
# Or, is (x1,y1) coincident with (x0,y0)? Do nothing.
|
# Is (x1,y1) coincident with (x0,y0)? Do nothing.
|
||||||
discard
|
discard
|
||||||
|
|
||||||
elif not (abs(y01 * x21 - y21 * x01) > epsilon) or r == 0:
|
elif not (abs(y01 * x21 - y21 * x01) > epsilon) or r == 0:
|
||||||
# // Or, are (x0,y0), (x1,y1) and (x2,y2) collinear?
|
# // Or, are (x0,y0), (x1,y1) and (x2,y2) colinear?
|
||||||
# // Equivalently, is (x1,y1) coincident with (x2,y2)?
|
# // Equivalently, is (x1,y1) coincident with (x2,y2)?
|
||||||
# // Or, is the radius zero? Line to (x1,y1).
|
# // Or, is the radius zero? Line to (x1,y1).
|
||||||
|
|
||||||
path.commands.add(PathCommand(kind: Line, numbers: @[x1, y1]))
|
path.lineTo(x1, y1)
|
||||||
path.at.x = x1
|
|
||||||
path.at.y = y1
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Otherwise, draw an arc!
|
# Draw an arc
|
||||||
var
|
var
|
||||||
x20 = x2 - x0
|
x20 = x2 - path.at.x
|
||||||
y20 = y2 - y0
|
y20 = y2 - path.at.y
|
||||||
l21_2 = x21 * x21 + y21 * y21
|
l21_2 = x21 * x21 + y21 * y21
|
||||||
l20_2 = x20 * x20 + y20 * y20
|
l20_2 = x20 * x20 + y20 * y20
|
||||||
l21 = sqrt(l21_2)
|
l21 = sqrt(l21_2)
|
||||||
l01 = sqrt(l01_2)
|
l01 = sqrt(l01_2)
|
||||||
l:float32 = r * tan((PI - arccos((l21_2 + l01_2 - l20_2) / (2 * l21 * l01))) / 2)
|
l = r * tan((PI - arccos((l21_2 + l01_2 - l20_2) / (2 * l21 * l01))) / 2).float32
|
||||||
t01 = l / l01
|
t01 = l / l01
|
||||||
t21 = l / l21
|
t21 = l / l21
|
||||||
|
|
||||||
# If the start tangent is not coincident with (x0,y0), line to.
|
# If the start tangent is not coincident with path.at, line to.
|
||||||
if abs(t01 - 1) > epsilon:
|
if abs(t01 - 1) > epsilon:
|
||||||
path.commands.add(PathCommand(kind: Line, numbers: @[x1 + t01 * x01, y1 + t01 * y01]))
|
path.lineTo(x1 + t01 * x01, y1 + t01 * y01)
|
||||||
discard
|
|
||||||
path.at.x = x1 + t21 * x21
|
path.at.x = x1 + t21 * x21
|
||||||
path.at.y = y1 + t21 * y21
|
path.at.y = y1 + t21 * y21
|
||||||
path.commands.add(PathCommand(
|
path.commands.add(PathCommand(
|
||||||
|
|
BIN
tests/images/paths/pathBlackRectangleZ.png
Normal file
After Width: | Height: | Size: 350 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 975 B After Width: | Height: | Size: 990 B |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 990 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 810 B After Width: | Height: | Size: 832 B |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
@ -37,10 +37,10 @@ block:
|
||||||
let path = parsePath(pathStr)
|
let path = parsePath(pathStr)
|
||||||
doAssert $path == "M1 2 L3 4 H5 V6 C0 0 0 0 0 0 Q1 1 1 1 T2 2 A7 7 7 7 7 7 7 Z"
|
doAssert $path == "M1 2 L3 4 H5 V6 C0 0 0 0 0 0 Q1 1 1 1 T2 2 A7 7 7 7 7 7 7 Z"
|
||||||
|
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let pathStr = "M 0.1E-10 0.1e10 L2+2 L3-3 L0.1E+10-1"
|
let
|
||||||
let path = parsePath(pathStr)
|
pathStr = "M 0.1E-10 0.1e10 L2+2 L3-3 L0.1E+10-1"
|
||||||
|
path = parsePath(pathStr)
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let image = newImage(100, 100)
|
let image = newImage(100, 100)
|
||||||
|
@ -72,6 +72,14 @@ block:
|
||||||
image.fillPath(pathStr, color)
|
image.fillPath(pathStr, color)
|
||||||
image.writeFile("tests/images/paths/pathBlackRectangle.png")
|
image.writeFile("tests/images/paths/pathBlackRectangle.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
image = newImage(100, 100)
|
||||||
|
pathStr = "M 10 10 H 90 V 90 H 10 Z"
|
||||||
|
color = rgba(0, 0, 0, 255)
|
||||||
|
image.fillPath(parsePath(pathStr), color)
|
||||||
|
image.writeFile("tests/images/paths/pathBlackRectangleZ.png")
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let image = newImage(100, 100)
|
let image = newImage(100, 100)
|
||||||
image.fillPath(
|
image.fillPath(
|
||||||
|
|