Add dashes. (#202)
* Add dashes. * Miter limits as ratio everywhere. * Remove some fill and stroke overloads.
This commit is contained in:
parent
08dac8e241
commit
6fb1323101
13 changed files with 153 additions and 117 deletions
src/pixie
tests
|
@ -193,7 +193,11 @@ proc clearRect*(ctx: Context, rect: Rect) =
|
|||
## Erases the pixels in a rectangular area.
|
||||
var path: Path
|
||||
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.} =
|
||||
## Erases the pixels in a rectangular area.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
## 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
|
||||
|
||||
const
|
||||
|
|
|
@ -34,7 +34,10 @@ converter parseSomePaint*(paint: SomePaint): Paint {.inline.} =
|
|||
when type(paint) is string:
|
||||
Paint(kind: pkSolid, color: parseHtmlColor(paint).rgbx())
|
||||
elif type(paint) is SomeColor:
|
||||
Paint(kind: pkSolid, color: paint.rgbx())
|
||||
when type(paint) is ColorRGBX:
|
||||
Paint(kind: pkSolid, color: paint)
|
||||
else:
|
||||
Paint(kind: pkSolid, color: paint.rgbx())
|
||||
elif type(paint) is Paint:
|
||||
paint
|
||||
|
||||
|
|
|
@ -41,12 +41,15 @@ const epsilon = 0.0001 * PI ## Tiny value used for some computations.
|
|||
when defined(release):
|
||||
{.push checks: off.}
|
||||
|
||||
proc maxScale(m: Mat3): float32 =
|
||||
## What is the largest scale factor of this matrix?
|
||||
max(
|
||||
vec2(m[0, 0], m[0, 1]).length,
|
||||
vec2(m[1, 0], m[1, 1]).length
|
||||
)
|
||||
proc pixelScale(transform: Vec2 | Mat3): float32 =
|
||||
## What is the largest scale factor of this transform?
|
||||
when type(transform) is Vec2:
|
||||
return 1.0
|
||||
else:
|
||||
max(
|
||||
vec2(transform[0, 0], transform[0, 1]).length,
|
||||
vec2(transform[1, 0], transform[1, 1]).length
|
||||
)
|
||||
|
||||
proc isRelative(kind: PathCommandKind): bool =
|
||||
kind in {
|
||||
|
@ -1317,16 +1320,27 @@ proc fillShapes(
|
|||
mask.setValueUnsafe(x, y, blended)
|
||||
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(
|
||||
shapes: seq[seq[Vec2]],
|
||||
strokeWidth: float32,
|
||||
lineCap: LineCap,
|
||||
lineJoin: LineJoin,
|
||||
miterAngleLimit = degToRad(28.96)
|
||||
miterLimit: float32,
|
||||
dashes: seq[float32]
|
||||
): seq[seq[Vec2]] =
|
||||
if strokeWidth == 0:
|
||||
return
|
||||
|
||||
let miterAngleLimit = miterLimitToAngle(miterLimit)
|
||||
|
||||
let halfStroke = strokeWidth / 2
|
||||
|
||||
proc makeCircle(at: Vec2): seq[Vec2] =
|
||||
|
@ -1413,7 +1427,25 @@ proc strokeShapes(
|
|||
pos = shape[i]
|
||||
prevPos = shape[i - 1]
|
||||
|
||||
shapeStroke.add(makeRect(prevPos, pos))
|
||||
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))
|
||||
|
||||
# If we need a line join
|
||||
if i < shape.len - 1:
|
||||
|
@ -1459,53 +1491,14 @@ proc transform(shapes: var seq[seq[Vec2]], transform: Vec2 | Mat3) =
|
|||
for segment in shape.mitems:
|
||||
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*(
|
||||
mask: Mask,
|
||||
path: SomePath,
|
||||
windingRule = wrNonZero
|
||||
) {.inline.} =
|
||||
## Fills a path.
|
||||
mask.fillShapes(parseSomePath(path), windingRule)
|
||||
|
||||
proc fillPath*(
|
||||
mask: Mask,
|
||||
path: SomePath,
|
||||
transform: Vec2 | Mat3,
|
||||
transform: Vec2 | Mat3 = vec2(),
|
||||
windingRule = wrNonZero
|
||||
) =
|
||||
## Fills a path.
|
||||
when type(transform) is Mat3:
|
||||
let pixelScale = transform.maxScale()
|
||||
else:
|
||||
let pixelScale = 1.0
|
||||
var shapes = parseSomePath(path, pixelScale)
|
||||
var shapes = parseSomePath(path, transform.pixelScale())
|
||||
shapes.transform(transform)
|
||||
mask.fillShapes(shapes, windingRule)
|
||||
|
||||
|
@ -1518,7 +1511,9 @@ proc fillPath*(
|
|||
) =
|
||||
## Fills a path.
|
||||
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
|
||||
|
||||
let
|
||||
|
@ -1545,69 +1540,23 @@ proc fillPath*(
|
|||
image.draw(fill, blendMode = paint.blendMode)
|
||||
|
||||
proc strokePath*(
|
||||
image: Image,
|
||||
mask: Mask,
|
||||
path: SomePath,
|
||||
color: SomeColor,
|
||||
transform: Vec2 | Mat3 = vec2(),
|
||||
strokeWidth = 1.0,
|
||||
lineCap = lcButt,
|
||||
lineJoin = ljMiter,
|
||||
blendMode = bmNormal
|
||||
miterLimit: float32 = 4,
|
||||
dashes: seq[float32] = @[],
|
||||
) =
|
||||
## 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(
|
||||
parseSomePath(path, pixelScale), strokeWidth, lineCap, lineJoin
|
||||
)
|
||||
strokeShapes.transform(transform)
|
||||
image.fillShapes(strokeShapes, color, wrNonZero, blendMode)
|
||||
|
||||
proc strokePath*(
|
||||
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
|
||||
parseSomePath(path, transform.pixelScale()),
|
||||
strokeWidth,
|
||||
lineCap,
|
||||
lineJoin,
|
||||
miterLimit,
|
||||
dashes
|
||||
)
|
||||
strokeShapes.transform(transform)
|
||||
mask.fillShapes(strokeShapes, wrNonZero)
|
||||
|
@ -1619,20 +1568,38 @@ proc strokePath*(
|
|||
transform: Vec2 | Mat3 = vec2(),
|
||||
strokeWidth = 1.0,
|
||||
lineCap = lcButt,
|
||||
lineJoin = ljMiter
|
||||
lineJoin = ljMiter,
|
||||
miterLimit: float32 = 4,
|
||||
dashes: seq[float32] = @[]
|
||||
) =
|
||||
## Fills a path.
|
||||
## Strokes a path.
|
||||
if paint.kind == pkSolid:
|
||||
image.strokePath(
|
||||
path, paint.color, transform, strokeWidth, lineCap, lineJoin
|
||||
var strokeShapes = strokeShapes(
|
||||
parseSomePath(path, transform.pixelScale()),
|
||||
strokeWidth,
|
||||
lineCap,
|
||||
lineJoin,
|
||||
miterLimit,
|
||||
dashes
|
||||
)
|
||||
strokeShapes.transform(transform)
|
||||
image.fillShapes(
|
||||
strokeShapes, paint.color, wrNonZero, blendMode = paint.blendMode)
|
||||
return
|
||||
|
||||
let
|
||||
mask = newMask(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:
|
||||
of pkSolid:
|
||||
|
|
BIN
tests/images/paths/dashes.png
Normal file
BIN
tests/images/paths/dashes.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 824 B |
BIN
tests/images/paths/miterLimit_10deg_2.00num.png
Normal file
BIN
tests/images/paths/miterLimit_10deg_2.00num.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 597 B |
BIN
tests/images/paths/miterLimit_145deg_2.00num.png
Normal file
BIN
tests/images/paths/miterLimit_145deg_2.00num.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 625 B |
BIN
tests/images/paths/miterLimit_145deg_3.32num.png
Normal file
BIN
tests/images/paths/miterLimit_145deg_3.32num.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 625 B |
BIN
tests/images/paths/miterLimit_145deg_3.33num.png
Normal file
BIN
tests/images/paths/miterLimit_145deg_3.33num.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 642 B |
BIN
tests/images/paths/miterLimit_155deg_2.00num.png
Normal file
BIN
tests/images/paths/miterLimit_155deg_2.00num.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 557 B |
BIN
tests/images/paths/miterLimit_165deg_10.00num.png
Normal file
BIN
tests/images/paths/miterLimit_165deg_10.00num.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 700 B |
BIN
tests/images/paths/miterLimit_165deg_2.00num.png
Normal file
BIN
tests/images/paths/miterLimit_165deg_2.00num.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 531 B |
|
@ -1,4 +1,4 @@
|
|||
import chroma, pixie, pixie/fileformats/png
|
||||
import chroma, pixie, pixie/fileformats/png, strformat
|
||||
|
||||
block:
|
||||
let pathStr = """
|
||||
|
@ -47,7 +47,7 @@ block:
|
|||
image = newImage(100, 100)
|
||||
pathStr = "M 10 10 L 90 90"
|
||||
color = rgba(255, 0, 0, 255)
|
||||
image.strokePath(pathStr, color, 10)
|
||||
image.strokePath(pathStr, color, strokeWidth=10)
|
||||
image.writeFile("tests/images/paths/pathStroke1.png")
|
||||
|
||||
block:
|
||||
|
@ -55,7 +55,7 @@ block:
|
|||
image = newImage(100, 100)
|
||||
pathStr = "M 10 10 L 50 60 90 90"
|
||||
color = rgba(255, 0, 0, 255)
|
||||
image.strokePath(pathStr, color, 10)
|
||||
image.strokePath(pathStr, color, strokeWidth=10)
|
||||
image.writeFile("tests/images/paths/pathStroke2.png")
|
||||
|
||||
block:
|
||||
|
@ -256,6 +256,68 @@ block:
|
|||
|
||||
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
|
||||
|
||||
block:
|
||||
|
|
Loading…
Reference in a new issue