linecap, linejoin, miterAngleLimit
|
@ -31,6 +31,8 @@ type
|
||||||
|
|
||||||
SomePath* = Path | string | seq[seq[Vec2]]
|
SomePath* = Path | string | seq[seq[Vec2]]
|
||||||
|
|
||||||
|
const epsilon = 0.0001 * PI
|
||||||
|
|
||||||
when defined(release):
|
when defined(release):
|
||||||
{.push checks: off.}
|
{.push checks: off.}
|
||||||
|
|
||||||
|
@ -972,11 +974,10 @@ template computeCoverages(
|
||||||
windingRule: WindingRule
|
windingRule: WindingRule
|
||||||
) =
|
) =
|
||||||
const
|
const
|
||||||
ep = 0.0001 * PI
|
|
||||||
quality = 5 # Must divide 255 cleanly (1, 3, 5, 15, 17, 51, 85)
|
quality = 5 # Must divide 255 cleanly (1, 3, 5, 15, 17, 51, 85)
|
||||||
sampleCoverage = (255 div quality).uint8
|
sampleCoverage = (255 div quality).uint8
|
||||||
offset = 1 / quality.float32
|
offset = 1 / quality.float32
|
||||||
initialOffset = offset / 2 + ep
|
initialOffset = offset / 2 + epsilon
|
||||||
|
|
||||||
let
|
let
|
||||||
partition =
|
partition =
|
||||||
|
@ -1233,43 +1234,122 @@ proc fillShapes(
|
||||||
|
|
||||||
proc strokeShapes(
|
proc strokeShapes(
|
||||||
shapes: seq[seq[Vec2]],
|
shapes: seq[seq[Vec2]],
|
||||||
strokeWidth: float32
|
strokeWidth: float32,
|
||||||
|
lineCap: LineCap,
|
||||||
|
lineJoin: LineJoin,
|
||||||
|
miterAngleLimit = degToRad(28.96)
|
||||||
): seq[seq[Vec2]] =
|
): seq[seq[Vec2]] =
|
||||||
if strokeWidth == 0:
|
if strokeWidth == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
let halfStroke = strokeWidth / 2
|
||||||
|
|
||||||
|
proc makeCircle(at: Vec2): seq[Vec2] =
|
||||||
|
var path: Path
|
||||||
|
path.ellipse(at, halfStroke, halfStroke)
|
||||||
|
path.commandsToShapes()[0]
|
||||||
|
|
||||||
|
proc makeRect(at, to: Vec2): seq[Vec2] =
|
||||||
|
# Rectangle corners
|
||||||
let
|
let
|
||||||
widthLeft = strokeWidth / 2
|
tangent = (to - at).normalize()
|
||||||
widthRight = strokeWidth / 2
|
normal = vec2(tangent.y, tangent.x)
|
||||||
|
a = vec2(
|
||||||
|
at.x + normal.x * halfStroke,
|
||||||
|
at.y - normal.y * halfStroke
|
||||||
|
)
|
||||||
|
b = vec2(
|
||||||
|
to.x + normal.x * halfStroke,
|
||||||
|
to.y - normal.y * halfStroke
|
||||||
|
)
|
||||||
|
c = vec2(
|
||||||
|
to.x - normal.x * halfStroke,
|
||||||
|
to.y + normal.y * halfStroke
|
||||||
|
)
|
||||||
|
d = vec2(
|
||||||
|
at.x - normal.x * halfStroke,
|
||||||
|
at.y + normal.y * halfStroke
|
||||||
|
)
|
||||||
|
|
||||||
|
@[a, b, c, d, a]
|
||||||
|
|
||||||
for shape in shapes:
|
for shape in shapes:
|
||||||
|
var shapeStroke: seq[seq[Vec2]]
|
||||||
|
|
||||||
|
proc makeJoin(prevPos, pos, nextPos: Vec2): seq[Vec2] =
|
||||||
|
let angle = fixAngle(-angle(prevPos - pos) - -angle(nextPos - pos))
|
||||||
|
if abs(abs(angle) - PI) > epsilon:
|
||||||
var
|
var
|
||||||
strokeShape: seq[Vec2]
|
a = (pos - prevPos).normalize() * halfStroke
|
||||||
back: seq[Vec2]
|
b = (pos - nextPos).normalize() * halfStroke
|
||||||
for segment in shape.segments:
|
if angle >= 0:
|
||||||
|
a = vec2(-a.y, a.x)
|
||||||
|
b = vec2(b.y, -b.x)
|
||||||
|
else:
|
||||||
|
a = vec2(a.y, -a.x)
|
||||||
|
b = vec2(-b.y, b.x)
|
||||||
|
|
||||||
|
var lineJoin = lineJoin
|
||||||
|
if lineJoin == ljMiter and abs(angle) < miterAngleLimit:
|
||||||
|
lineJoin = ljBevel
|
||||||
|
|
||||||
|
case lineJoin:
|
||||||
|
of ljMiter:
|
||||||
let
|
let
|
||||||
tangent = (segment.at - segment.to).normalize()
|
la = line(prevPos + a, pos + a)
|
||||||
normal = vec2(-tangent.y, tangent.x)
|
lb = line(nextPos + b, pos + b)
|
||||||
left = segment(
|
var at: Vec2
|
||||||
segment.at - normal * widthLeft,
|
if la.intersects(lb, at):
|
||||||
segment.to - normal * widthLeft
|
return @[pos + a, at, pos + b, pos, pos + a]
|
||||||
)
|
|
||||||
right = segment(
|
|
||||||
segment.at + normal * widthRight,
|
|
||||||
segment.to + normal * widthRight
|
|
||||||
)
|
|
||||||
|
|
||||||
strokeShape.add([right.at, right.to])
|
of ljBevel:
|
||||||
back.add([left.at, left.to])
|
return @[a + pos, b + pos, pos, a + pos]
|
||||||
|
|
||||||
# Add the back side reversed
|
of ljRound:
|
||||||
for i in 1 .. back.len:
|
return makeCircle(prevPos)
|
||||||
strokeShape.add(back[^i])
|
|
||||||
|
|
||||||
strokeShape.add(strokeShape[0])
|
if shape[0] != shape[^1]:
|
||||||
|
# This shape does not end at the same point it starts so draw the
|
||||||
|
# first line cap.
|
||||||
|
case lineCap:
|
||||||
|
of lcButt:
|
||||||
|
discard
|
||||||
|
of lcRound:
|
||||||
|
shapeStroke.add(makeCircle(shape[0]))
|
||||||
|
of lcSquare:
|
||||||
|
let tangent = (shape[1] - shape[0]).normalize()
|
||||||
|
shapeStroke.add(makeRect(
|
||||||
|
shape[0] - tangent * halfStroke,
|
||||||
|
shape[0]
|
||||||
|
))
|
||||||
|
|
||||||
if strokeShape.len > 0:
|
for i in 1 ..< shape.len:
|
||||||
result.add(strokeShape)
|
let
|
||||||
|
pos = shape[i]
|
||||||
|
prevPos = shape[i - 1]
|
||||||
|
|
||||||
|
shapeStroke.add(makeRect(prevPos, pos))
|
||||||
|
|
||||||
|
# If we need a line join
|
||||||
|
if i < shape.len - 1:
|
||||||
|
shapeStroke.add(makeJoin(prevPos, pos, shape[i + 1]))
|
||||||
|
|
||||||
|
if shape[0] == shape[^1]:
|
||||||
|
shapeStroke.add(makeJoin(shape[^2], shape[^1], shape[1]))
|
||||||
|
else:
|
||||||
|
case lineCap:
|
||||||
|
of lcButt:
|
||||||
|
discard
|
||||||
|
of lcRound:
|
||||||
|
shapeStroke.add(makeCircle(shape[^1]))
|
||||||
|
of lcSquare:
|
||||||
|
let tangent = (shape[^1] - shape[^2]).normalize()
|
||||||
|
shapeStroke.add(makeRect(
|
||||||
|
shape[^1] + tangent * halfStroke,
|
||||||
|
shape[^1]
|
||||||
|
))
|
||||||
|
|
||||||
|
result.add(shapeStroke)
|
||||||
|
|
||||||
proc parseSomePath(
|
proc parseSomePath(
|
||||||
path: SomePath, pixelScale: float32 = 1.0
|
path: SomePath, pixelScale: float32 = 1.0
|
||||||
|
@ -1338,9 +1418,13 @@ proc strokePath*(
|
||||||
path: SomePath,
|
path: SomePath,
|
||||||
color: ColorRGBA,
|
color: ColorRGBA,
|
||||||
strokeWidth = 1.0,
|
strokeWidth = 1.0,
|
||||||
|
lineCap = lcButt,
|
||||||
|
lineJoin = ljMiter,
|
||||||
blendMode = bmNormal
|
blendMode = bmNormal
|
||||||
) =
|
) =
|
||||||
let strokeShapes = strokeShapes(parseSomePath(path), strokeWidth)
|
let strokeShapes = strokeShapes(
|
||||||
|
parseSomePath(path), strokeWidth, lineCap, lineJoin
|
||||||
|
)
|
||||||
image.fillShapes(strokeShapes, color, wrNonZero, blendMode)
|
image.fillShapes(strokeShapes, color, wrNonZero, blendMode)
|
||||||
|
|
||||||
proc strokePath*(
|
proc strokePath*(
|
||||||
|
@ -1349,13 +1433,17 @@ proc strokePath*(
|
||||||
color: ColorRGBA,
|
color: ColorRGBA,
|
||||||
transform: Vec2 | Mat3,
|
transform: Vec2 | Mat3,
|
||||||
strokeWidth = 1.0,
|
strokeWidth = 1.0,
|
||||||
|
lineCap = lcButt,
|
||||||
|
lineJoin = ljMiter,
|
||||||
blendMode = bmNormal
|
blendMode = bmNormal
|
||||||
) =
|
) =
|
||||||
when type(transform) is Mat3:
|
when type(transform) is Mat3:
|
||||||
let pixelScale = transform.maxScale()
|
let pixelScale = transform.maxScale()
|
||||||
else:
|
else:
|
||||||
let pixelScale = 1.0
|
let pixelScale = 1.0
|
||||||
var strokeShapes = strokeShapes(parseSomePath(path, pixelScale), strokeWidth)
|
var strokeShapes = strokeShapes(
|
||||||
|
parseSomePath(path, pixelScale), strokeWidth, lineCap, lineJoin
|
||||||
|
)
|
||||||
for shape in strokeShapes.mitems:
|
for shape in strokeShapes.mitems:
|
||||||
for segment in shape.mitems:
|
for segment in shape.mitems:
|
||||||
when type(transform) is Vec2:
|
when type(transform) is Vec2:
|
||||||
|
@ -1367,18 +1455,26 @@ proc strokePath*(
|
||||||
proc strokePath*(
|
proc strokePath*(
|
||||||
mask: Mask,
|
mask: Mask,
|
||||||
path: SomePath,
|
path: SomePath,
|
||||||
strokeWidth = 1.0
|
strokeWidth = 1.0,
|
||||||
|
lineCap = lcButt,
|
||||||
|
lineJoin = ljMiter
|
||||||
) =
|
) =
|
||||||
let strokeShapes = strokeShapes(parseSomePath(path), strokeWidth)
|
let strokeShapes = strokeShapes(
|
||||||
|
parseSomePath(path), strokeWidth, lineCap, lineJoin
|
||||||
|
)
|
||||||
mask.fillShapes(strokeShapes, wrNonZero)
|
mask.fillShapes(strokeShapes, wrNonZero)
|
||||||
|
|
||||||
proc strokePath*(
|
proc strokePath*(
|
||||||
mask: Mask,
|
mask: Mask,
|
||||||
path: SomePath,
|
path: SomePath,
|
||||||
|
transform: Vec2 | Mat3,
|
||||||
strokeWidth = 1.0,
|
strokeWidth = 1.0,
|
||||||
transform: Vec2 | Mat3
|
lineCap = lcButt,
|
||||||
|
lineJoin = ljMiter
|
||||||
) =
|
) =
|
||||||
var strokeShapes = strokeShapes(parseSomePath(path), strokeWidth)
|
var strokeShapes = strokeShapes(
|
||||||
|
parseSomePath(path), strokeWidth, lineCap, lineJoin
|
||||||
|
)
|
||||||
for shape in strokeShapes.mitems:
|
for shape in strokeShapes.mitems:
|
||||||
for segment in shape.mitems:
|
for segment in shape.mitems:
|
||||||
when type(transform) is Vec2:
|
when type(transform) is Vec2:
|
||||||
|
|
Before Width: | Height: | Size: 905 B After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 224 B |
Before Width: | Height: | Size: 831 B After Width: | Height: | Size: 1.1 KiB |
BIN
tests/images/paths/boxBevel.png
Normal file
After Width: | Height: | Size: 354 B |
BIN
tests/images/paths/boxMiter.png
Normal file
After Width: | Height: | Size: 260 B |
BIN
tests/images/paths/boxRound.png
Normal file
After Width: | Height: | Size: 770 B |
BIN
tests/images/paths/lcButt.png
Normal file
After Width: | Height: | Size: 303 B |
BIN
tests/images/paths/lcRound.png
Normal file
After Width: | Height: | Size: 542 B |
BIN
tests/images/paths/lcSquare.png
Normal file
After Width: | Height: | Size: 303 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 387 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 356 KiB After Width: | Height: | Size: 358 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
@ -214,3 +214,57 @@ block:
|
||||||
strokeWidth = 0.01)
|
strokeWidth = 0.01)
|
||||||
|
|
||||||
image.writeFile("tests/images/paths/pixelScale.png")
|
image.writeFile("tests/images/paths/pixelScale.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
image = newImage(60, 60)
|
||||||
|
path = parsePath("M 3 3 L 20 3 L 20 20 L 3 20 Z")
|
||||||
|
image.fill(rgba(255, 255, 255, 255))
|
||||||
|
image.strokePath(path, rgba(0, 0, 0, 255), vec2(10, 10), 10, lcRound, ljRound)
|
||||||
|
|
||||||
|
image.writeFile("tests/images/paths/boxRound.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
image = newImage(60, 60)
|
||||||
|
path = parsePath("M 3 3 L 20 3 L 20 20 L 3 20 Z")
|
||||||
|
image.fill(rgba(255, 255, 255, 255))
|
||||||
|
image.strokePath(path, rgba(0, 0, 0, 255), vec2(10, 10), 10, lcRound, ljBevel)
|
||||||
|
|
||||||
|
image.writeFile("tests/images/paths/boxBevel.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
image = newImage(60, 60)
|
||||||
|
path = parsePath("M 3 3 L 20 3 L 20 20 L 3 20 Z")
|
||||||
|
image.fill(rgba(255, 255, 255, 255))
|
||||||
|
image.strokePath(path, rgba(0, 0, 0, 255), vec2(10, 10), 10, lcRound, ljMiter)
|
||||||
|
|
||||||
|
image.writeFile("tests/images/paths/boxMiter.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
image = newImage(60, 60)
|
||||||
|
path = parsePath("M 3 3 L 20 3 L 20 20 L 3 20")
|
||||||
|
image.fill(rgba(255, 255, 255, 255))
|
||||||
|
image.strokePath(path, rgba(0, 0, 0, 255), vec2(10, 10), 10, lcButt, ljBevel)
|
||||||
|
|
||||||
|
image.writeFile("tests/images/paths/lcButt.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
image = newImage(60, 60)
|
||||||
|
path = parsePath("M 3 3 L 20 3 L 20 20 L 3 20")
|
||||||
|
image.fill(rgba(255, 255, 255, 255))
|
||||||
|
image.strokePath(path, rgba(0, 0, 0, 255), vec2(10, 10), 10, lcRound, ljBevel)
|
||||||
|
|
||||||
|
image.writeFile("tests/images/paths/lcRound.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
image = newImage(60, 60)
|
||||||
|
path = parsePath("M 3 3 L 20 3 L 20 20 L 3 20")
|
||||||
|
image.fill(rgba(255, 255, 255, 255))
|
||||||
|
image.strokePath(path, rgba(0, 0, 0, 255), vec2(10, 10), 10, lcSquare, ljBevel)
|
||||||
|
|
||||||
|
image.writeFile("tests/images/paths/lcSquare.png")
|
||||||
|
|