Merge pull request #57 from guzba/master

arc stuff, quad discretize
This commit is contained in:
treeform 2021-01-22 09:00:35 -08:00 committed by GitHub
commit 2f70c99377
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 175 additions and 161 deletions

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 B

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 981 B

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 810 B

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View file

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