Add dashes. (#202)
* Add dashes. * Miter limits as ratio everywhere. * Remove some fill and stroke overloads.
|
@ -193,7 +193,11 @@ proc clearRect*(ctx: Context, rect: Rect) =
|
||||||
## Erases the pixels in a rectangular area.
|
## Erases the pixels in a rectangular area.
|
||||||
var path: Path
|
var path: Path
|
||||||
path.rect(rect)
|
path.rect(rect)
|
||||||
ctx.image.fillPath(path, rgbx(0, 0, 0, 0), ctx.mat, blendMode = bmOverwrite)
|
ctx.image.fillPath(
|
||||||
|
path,
|
||||||
|
Paint(kind: pkSolid, color:rgbx(0, 0, 0, 0), blendMode: bmOverwrite),
|
||||||
|
ctx.mat
|
||||||
|
)
|
||||||
|
|
||||||
proc clearRect*(ctx: Context, x, y, width, height: float32) {.inline.} =
|
proc clearRect*(ctx: Context, x, y, width, height: float32) {.inline.} =
|
||||||
## Erases the pixels in a rectangular area.
|
## Erases the pixels in a rectangular area.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
## Load SVG files.
|
## Load SVG files.
|
||||||
|
|
||||||
import chroma, pixie/common, pixie/images, pixie/paths, strutils, vmath,
|
import chroma, pixie/common, pixie/images, pixie/paths, pixie/paints, strutils, vmath,
|
||||||
xmlparser, xmltree
|
xmlparser, xmltree
|
||||||
|
|
||||||
const
|
const
|
||||||
|
|
|
@ -34,6 +34,9 @@ converter parseSomePaint*(paint: SomePaint): Paint {.inline.} =
|
||||||
when type(paint) is string:
|
when type(paint) is string:
|
||||||
Paint(kind: pkSolid, color: parseHtmlColor(paint).rgbx())
|
Paint(kind: pkSolid, color: parseHtmlColor(paint).rgbx())
|
||||||
elif type(paint) is SomeColor:
|
elif type(paint) is SomeColor:
|
||||||
|
when type(paint) is ColorRGBX:
|
||||||
|
Paint(kind: pkSolid, color: paint)
|
||||||
|
else:
|
||||||
Paint(kind: pkSolid, color: paint.rgbx())
|
Paint(kind: pkSolid, color: paint.rgbx())
|
||||||
elif type(paint) is Paint:
|
elif type(paint) is Paint:
|
||||||
paint
|
paint
|
||||||
|
|
|
@ -41,11 +41,14 @@ const epsilon = 0.0001 * PI ## Tiny value used for some computations.
|
||||||
when defined(release):
|
when defined(release):
|
||||||
{.push checks: off.}
|
{.push checks: off.}
|
||||||
|
|
||||||
proc maxScale(m: Mat3): float32 =
|
proc pixelScale(transform: Vec2 | Mat3): float32 =
|
||||||
## What is the largest scale factor of this matrix?
|
## What is the largest scale factor of this transform?
|
||||||
|
when type(transform) is Vec2:
|
||||||
|
return 1.0
|
||||||
|
else:
|
||||||
max(
|
max(
|
||||||
vec2(m[0, 0], m[0, 1]).length,
|
vec2(transform[0, 0], transform[0, 1]).length,
|
||||||
vec2(m[1, 0], m[1, 1]).length
|
vec2(transform[1, 0], transform[1, 1]).length
|
||||||
)
|
)
|
||||||
|
|
||||||
proc isRelative(kind: PathCommandKind): bool =
|
proc isRelative(kind: PathCommandKind): bool =
|
||||||
|
@ -1317,16 +1320,27 @@ proc fillShapes(
|
||||||
mask.setValueUnsafe(x, y, blended)
|
mask.setValueUnsafe(x, y, blended)
|
||||||
inc x
|
inc x
|
||||||
|
|
||||||
|
proc miterLimitToAngle*(limit: float32): float32 =
|
||||||
|
## Converts milter-limit-ratio to miter-limit-angle.
|
||||||
|
arcsin(1 / limit) * 2
|
||||||
|
|
||||||
|
proc angleToMiterLimit*(angle: float32): float32 =
|
||||||
|
## Converts miter-limit-angle to milter-limit-ratio.
|
||||||
|
1 / sin(angle / 2)
|
||||||
|
|
||||||
proc strokeShapes(
|
proc strokeShapes(
|
||||||
shapes: seq[seq[Vec2]],
|
shapes: seq[seq[Vec2]],
|
||||||
strokeWidth: float32,
|
strokeWidth: float32,
|
||||||
lineCap: LineCap,
|
lineCap: LineCap,
|
||||||
lineJoin: LineJoin,
|
lineJoin: LineJoin,
|
||||||
miterAngleLimit = degToRad(28.96)
|
miterLimit: float32,
|
||||||
|
dashes: seq[float32]
|
||||||
): seq[seq[Vec2]] =
|
): seq[seq[Vec2]] =
|
||||||
if strokeWidth == 0:
|
if strokeWidth == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
let miterAngleLimit = miterLimitToAngle(miterLimit)
|
||||||
|
|
||||||
let halfStroke = strokeWidth / 2
|
let halfStroke = strokeWidth / 2
|
||||||
|
|
||||||
proc makeCircle(at: Vec2): seq[Vec2] =
|
proc makeCircle(at: Vec2): seq[Vec2] =
|
||||||
|
@ -1413,6 +1427,24 @@ proc strokeShapes(
|
||||||
pos = shape[i]
|
pos = shape[i]
|
||||||
prevPos = shape[i - 1]
|
prevPos = shape[i - 1]
|
||||||
|
|
||||||
|
if dashes.len > 0:
|
||||||
|
var dashes = dashes
|
||||||
|
if dashes.len mod 2 != 0:
|
||||||
|
dashes.add(dashes[^1])
|
||||||
|
var distance = dist(prevPos, pos)
|
||||||
|
let dir = dir(pos, prevPos)
|
||||||
|
var currPos = prevPos
|
||||||
|
block dashLoop:
|
||||||
|
while true:
|
||||||
|
for i, d in dashes:
|
||||||
|
if i mod 2 == 0:
|
||||||
|
let d = min(distance, d)
|
||||||
|
shapeStroke.add(makeRect(currPos, currPos + dir * d))
|
||||||
|
currPos += dir * d
|
||||||
|
distance -= d
|
||||||
|
if distance <= 0:
|
||||||
|
break dashLoop
|
||||||
|
else:
|
||||||
shapeStroke.add(makeRect(prevPos, pos))
|
shapeStroke.add(makeRect(prevPos, pos))
|
||||||
|
|
||||||
# If we need a line join
|
# If we need a line join
|
||||||
|
@ -1459,53 +1491,14 @@ proc transform(shapes: var seq[seq[Vec2]], transform: Vec2 | Mat3) =
|
||||||
for segment in shape.mitems:
|
for segment in shape.mitems:
|
||||||
segment = transform * segment
|
segment = transform * segment
|
||||||
|
|
||||||
proc fillPath*(
|
|
||||||
image: Image,
|
|
||||||
path: SomePath,
|
|
||||||
color: SomeColor,
|
|
||||||
windingRule = wrNonZero,
|
|
||||||
blendMode = bmNormal
|
|
||||||
) {.inline.} =
|
|
||||||
## Fills a path.
|
|
||||||
image.fillShapes(parseSomePath(path), color, windingRule, blendMode)
|
|
||||||
|
|
||||||
proc fillPath*(
|
|
||||||
image: Image,
|
|
||||||
path: SomePath,
|
|
||||||
color: SomeColor,
|
|
||||||
transform: Vec2 | Mat3,
|
|
||||||
windingRule = wrNonZero,
|
|
||||||
blendMode = bmNormal
|
|
||||||
) =
|
|
||||||
## Fills a path.
|
|
||||||
when type(transform) is Mat3:
|
|
||||||
let pixelScale = transform.maxScale()
|
|
||||||
else:
|
|
||||||
let pixelScale = 1.0
|
|
||||||
var shapes = parseSomePath(path, pixelScale)
|
|
||||||
shapes.transform(transform)
|
|
||||||
image.fillShapes(shapes, color, windingRule, blendMode)
|
|
||||||
|
|
||||||
proc fillPath*(
|
proc fillPath*(
|
||||||
mask: Mask,
|
mask: Mask,
|
||||||
path: SomePath,
|
path: SomePath,
|
||||||
windingRule = wrNonZero
|
transform: Vec2 | Mat3 = vec2(),
|
||||||
) {.inline.} =
|
|
||||||
## Fills a path.
|
|
||||||
mask.fillShapes(parseSomePath(path), windingRule)
|
|
||||||
|
|
||||||
proc fillPath*(
|
|
||||||
mask: Mask,
|
|
||||||
path: SomePath,
|
|
||||||
transform: Vec2 | Mat3,
|
|
||||||
windingRule = wrNonZero
|
windingRule = wrNonZero
|
||||||
) =
|
) =
|
||||||
## Fills a path.
|
## Fills a path.
|
||||||
when type(transform) is Mat3:
|
var shapes = parseSomePath(path, transform.pixelScale())
|
||||||
let pixelScale = transform.maxScale()
|
|
||||||
else:
|
|
||||||
let pixelScale = 1.0
|
|
||||||
var shapes = parseSomePath(path, pixelScale)
|
|
||||||
shapes.transform(transform)
|
shapes.transform(transform)
|
||||||
mask.fillShapes(shapes, windingRule)
|
mask.fillShapes(shapes, windingRule)
|
||||||
|
|
||||||
|
@ -1518,7 +1511,9 @@ proc fillPath*(
|
||||||
) =
|
) =
|
||||||
## Fills a path.
|
## Fills a path.
|
||||||
if paint.kind == pkSolid:
|
if paint.kind == pkSolid:
|
||||||
image.fillPath(path, paint.color, transform, windingRule)
|
var shapes = parseSomePath(path, transform.pixelScale())
|
||||||
|
shapes.transform(transform)
|
||||||
|
image.fillShapes(shapes, paint.color, windingRule, paint.blendMode)
|
||||||
return
|
return
|
||||||
|
|
||||||
let
|
let
|
||||||
|
@ -1545,69 +1540,23 @@ proc fillPath*(
|
||||||
image.draw(fill, blendMode = paint.blendMode)
|
image.draw(fill, blendMode = paint.blendMode)
|
||||||
|
|
||||||
proc strokePath*(
|
proc strokePath*(
|
||||||
image: Image,
|
mask: Mask,
|
||||||
path: SomePath,
|
path: SomePath,
|
||||||
color: SomeColor,
|
transform: Vec2 | Mat3 = vec2(),
|
||||||
strokeWidth = 1.0,
|
strokeWidth = 1.0,
|
||||||
lineCap = lcButt,
|
lineCap = lcButt,
|
||||||
lineJoin = ljMiter,
|
lineJoin = ljMiter,
|
||||||
blendMode = bmNormal
|
miterLimit: float32 = 4,
|
||||||
|
dashes: seq[float32] = @[],
|
||||||
) =
|
) =
|
||||||
## Strokes a path.
|
## Strokes a path.
|
||||||
let strokeShapes = strokeShapes(
|
|
||||||
parseSomePath(path), strokeWidth, lineCap, lineJoin
|
|
||||||
)
|
|
||||||
image.fillShapes(strokeShapes, color, wrNonZero, blendMode)
|
|
||||||
|
|
||||||
proc strokePath*(
|
|
||||||
image: Image,
|
|
||||||
path: SomePath,
|
|
||||||
color: SomeColor,
|
|
||||||
transform: Vec2 | Mat3,
|
|
||||||
strokeWidth = 1.0,
|
|
||||||
lineCap = lcButt,
|
|
||||||
lineJoin = ljMiter,
|
|
||||||
blendMode = bmNormal
|
|
||||||
) =
|
|
||||||
## Strokes a path.
|
|
||||||
when type(transform) is Mat3:
|
|
||||||
let pixelScale = transform.maxScale()
|
|
||||||
else:
|
|
||||||
let pixelScale = 1.0
|
|
||||||
var strokeShapes = strokeShapes(
|
var strokeShapes = strokeShapes(
|
||||||
parseSomePath(path, pixelScale), strokeWidth, lineCap, lineJoin
|
parseSomePath(path, transform.pixelScale()),
|
||||||
)
|
strokeWidth,
|
||||||
strokeShapes.transform(transform)
|
lineCap,
|
||||||
image.fillShapes(strokeShapes, color, wrNonZero, blendMode)
|
lineJoin,
|
||||||
|
miterLimit,
|
||||||
proc strokePath*(
|
dashes
|
||||||
mask: Mask,
|
|
||||||
path: SomePath,
|
|
||||||
strokeWidth = 1.0,
|
|
||||||
lineCap = lcButt,
|
|
||||||
lineJoin = ljMiter
|
|
||||||
) =
|
|
||||||
## Strokes a path.
|
|
||||||
let strokeShapes = strokeShapes(
|
|
||||||
parseSomePath(path), strokeWidth, lineCap, lineJoin
|
|
||||||
)
|
|
||||||
mask.fillShapes(strokeShapes, wrNonZero)
|
|
||||||
|
|
||||||
proc strokePath*(
|
|
||||||
mask: Mask,
|
|
||||||
path: SomePath,
|
|
||||||
transform: Vec2 | Mat3,
|
|
||||||
strokeWidth = 1.0,
|
|
||||||
lineCap = lcButt,
|
|
||||||
lineJoin = ljMiter
|
|
||||||
) =
|
|
||||||
## Strokes a path.
|
|
||||||
when type(transform) is Mat3:
|
|
||||||
let pixelScale = transform.maxScale()
|
|
||||||
else:
|
|
||||||
let pixelScale = 1.0
|
|
||||||
var strokeShapes = strokeShapes(
|
|
||||||
parseSomePath(path, pixelScale), strokeWidth, lineCap, lineJoin
|
|
||||||
)
|
)
|
||||||
strokeShapes.transform(transform)
|
strokeShapes.transform(transform)
|
||||||
mask.fillShapes(strokeShapes, wrNonZero)
|
mask.fillShapes(strokeShapes, wrNonZero)
|
||||||
|
@ -1619,20 +1568,38 @@ proc strokePath*(
|
||||||
transform: Vec2 | Mat3 = vec2(),
|
transform: Vec2 | Mat3 = vec2(),
|
||||||
strokeWidth = 1.0,
|
strokeWidth = 1.0,
|
||||||
lineCap = lcButt,
|
lineCap = lcButt,
|
||||||
lineJoin = ljMiter
|
lineJoin = ljMiter,
|
||||||
|
miterLimit: float32 = 4,
|
||||||
|
dashes: seq[float32] = @[]
|
||||||
) =
|
) =
|
||||||
## Fills a path.
|
## Strokes a path.
|
||||||
if paint.kind == pkSolid:
|
if paint.kind == pkSolid:
|
||||||
image.strokePath(
|
var strokeShapes = strokeShapes(
|
||||||
path, paint.color, transform, strokeWidth, lineCap, lineJoin
|
parseSomePath(path, transform.pixelScale()),
|
||||||
|
strokeWidth,
|
||||||
|
lineCap,
|
||||||
|
lineJoin,
|
||||||
|
miterLimit,
|
||||||
|
dashes
|
||||||
)
|
)
|
||||||
|
strokeShapes.transform(transform)
|
||||||
|
image.fillShapes(
|
||||||
|
strokeShapes, paint.color, wrNonZero, blendMode = paint.blendMode)
|
||||||
return
|
return
|
||||||
|
|
||||||
let
|
let
|
||||||
mask = newMask(image.width, image.height)
|
mask = newMask(image.width, image.height)
|
||||||
fill = newImage(image.width, image.height)
|
fill = newImage(image.width, image.height)
|
||||||
|
|
||||||
mask.strokePath(parseSomePath(path), transform)
|
mask.strokePath(
|
||||||
|
parseSomePath(path),
|
||||||
|
transform,
|
||||||
|
strokeWidth,
|
||||||
|
lineCap,
|
||||||
|
lineJoin,
|
||||||
|
miterLimit,
|
||||||
|
dashes
|
||||||
|
)
|
||||||
|
|
||||||
case paint.kind:
|
case paint.kind:
|
||||||
of pkSolid:
|
of pkSolid:
|
||||||
|
|
BIN
tests/images/paths/dashes.png
Normal file
After Width: | Height: | Size: 824 B |
BIN
tests/images/paths/miterLimit_10deg_2.00num.png
Normal file
After Width: | Height: | Size: 597 B |
BIN
tests/images/paths/miterLimit_145deg_2.00num.png
Normal file
After Width: | Height: | Size: 625 B |
BIN
tests/images/paths/miterLimit_145deg_3.32num.png
Normal file
After Width: | Height: | Size: 625 B |
BIN
tests/images/paths/miterLimit_145deg_3.33num.png
Normal file
After Width: | Height: | Size: 642 B |
BIN
tests/images/paths/miterLimit_155deg_2.00num.png
Normal file
After Width: | Height: | Size: 557 B |
BIN
tests/images/paths/miterLimit_165deg_10.00num.png
Normal file
After Width: | Height: | Size: 700 B |
BIN
tests/images/paths/miterLimit_165deg_2.00num.png
Normal file
After Width: | Height: | Size: 531 B |
|
@ -1,4 +1,4 @@
|
||||||
import chroma, pixie, pixie/fileformats/png
|
import chroma, pixie, pixie/fileformats/png, strformat
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let pathStr = """
|
let pathStr = """
|
||||||
|
@ -47,7 +47,7 @@ block:
|
||||||
image = newImage(100, 100)
|
image = newImage(100, 100)
|
||||||
pathStr = "M 10 10 L 90 90"
|
pathStr = "M 10 10 L 90 90"
|
||||||
color = rgba(255, 0, 0, 255)
|
color = rgba(255, 0, 0, 255)
|
||||||
image.strokePath(pathStr, color, 10)
|
image.strokePath(pathStr, color, strokeWidth=10)
|
||||||
image.writeFile("tests/images/paths/pathStroke1.png")
|
image.writeFile("tests/images/paths/pathStroke1.png")
|
||||||
|
|
||||||
block:
|
block:
|
||||||
|
@ -55,7 +55,7 @@ block:
|
||||||
image = newImage(100, 100)
|
image = newImage(100, 100)
|
||||||
pathStr = "M 10 10 L 50 60 90 90"
|
pathStr = "M 10 10 L 50 60 90 90"
|
||||||
color = rgba(255, 0, 0, 255)
|
color = rgba(255, 0, 0, 255)
|
||||||
image.strokePath(pathStr, color, 10)
|
image.strokePath(pathStr, color, strokeWidth=10)
|
||||||
image.writeFile("tests/images/paths/pathStroke2.png")
|
image.writeFile("tests/images/paths/pathStroke2.png")
|
||||||
|
|
||||||
block:
|
block:
|
||||||
|
@ -256,6 +256,68 @@ block:
|
||||||
|
|
||||||
image.writeFile("tests/images/paths/lcSquare.png")
|
image.writeFile("tests/images/paths/lcSquare.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
image = newImage(60, 120)
|
||||||
|
path = parsePath("M 0 0 L 50 0")
|
||||||
|
image.fill(rgba(255, 255, 255, 255))
|
||||||
|
|
||||||
|
image.strokePath(
|
||||||
|
path, rgba(0, 0, 0, 255), vec2(5, 5), 10, lcButt, ljBevel,
|
||||||
|
)
|
||||||
|
|
||||||
|
image.strokePath(
|
||||||
|
path, rgba(0, 0, 0, 255), vec2(5, 25), 10, lcButt, ljBevel,
|
||||||
|
dashes = @[2.float32,2]
|
||||||
|
)
|
||||||
|
|
||||||
|
image.strokePath(
|
||||||
|
path, rgba(0, 0, 0, 255), vec2(5, 45), 10, lcButt, ljBevel,
|
||||||
|
dashes = @[4.float32,4]
|
||||||
|
)
|
||||||
|
|
||||||
|
image.strokePath(
|
||||||
|
path, rgba(0, 0, 0, 255), vec2(5, 65), 10, lcButt, ljBevel,
|
||||||
|
dashes = @[2.float32, 4, 6, 2]
|
||||||
|
)
|
||||||
|
|
||||||
|
image.strokePath(
|
||||||
|
path, rgba(0, 0, 0, 255), vec2(5, 85), 10, lcButt, ljBevel,
|
||||||
|
dashes = @[1.float32]
|
||||||
|
)
|
||||||
|
|
||||||
|
image.strokePath(
|
||||||
|
path, rgba(0, 0, 0, 255), vec2(5, 105), 10, lcButt, ljBevel,
|
||||||
|
dashes = @[1.float32, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||||
|
)
|
||||||
|
|
||||||
|
image.writeFile("tests/images/paths/dashes.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
proc miterTest(angle, limit: float32) =
|
||||||
|
let
|
||||||
|
image = newImage(60, 60)
|
||||||
|
image.fill(rgba(255, 255, 255, 255))
|
||||||
|
var path: Path
|
||||||
|
path.moveTo(-20, 0)
|
||||||
|
path.lineTo(0, 0)
|
||||||
|
let th = angle.float32.degToRad() + PI/2
|
||||||
|
path.lineTo(sin(th)*20, cos(th)*20)
|
||||||
|
|
||||||
|
image.strokePath(
|
||||||
|
path, rgba(0, 0, 0, 255), vec2(30, 30), 8, lcButt, ljMiter,
|
||||||
|
miterLimit = limit
|
||||||
|
)
|
||||||
|
image.writeFile(&"tests/images/paths/miterLimit_{angle.int}deg_{limit:0.2f}num.png")
|
||||||
|
|
||||||
|
miterTest(10, 2)
|
||||||
|
miterTest(145, 2)
|
||||||
|
miterTest(155, 2)
|
||||||
|
miterTest(165, 2)
|
||||||
|
miterTest(165, 10)
|
||||||
|
miterTest(145, 3.32)
|
||||||
|
miterTest(145, 3.33)
|
||||||
|
|
||||||
# Potential error cases, ensure they do not crash
|
# Potential error cases, ensure they do not crash
|
||||||
|
|
||||||
block:
|
block:
|
||||||
|
|