diff --git a/src/pixie/context.nim b/src/pixie/context.nim index bf6a3cc..a081e57 100644 --- a/src/pixie/context.nim +++ b/src/pixie/context.nim @@ -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. diff --git a/src/pixie/fileformats/svg.nim b/src/pixie/fileformats/svg.nim index 666b6e8..c460ed5 100644 --- a/src/pixie/fileformats/svg.nim +++ b/src/pixie/fileformats/svg.nim @@ -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 diff --git a/src/pixie/paints.nim b/src/pixie/paints.nim index f3bc2a5..93ed8a7 100644 --- a/src/pixie/paints.nim +++ b/src/pixie/paints.nim @@ -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 diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 49b09c0..af95fd6 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -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: diff --git a/tests/images/paths/dashes.png b/tests/images/paths/dashes.png new file mode 100644 index 0000000..5389bd8 Binary files /dev/null and b/tests/images/paths/dashes.png differ diff --git a/tests/images/paths/miterLimit_10deg_2.00num.png b/tests/images/paths/miterLimit_10deg_2.00num.png new file mode 100644 index 0000000..2359b83 Binary files /dev/null and b/tests/images/paths/miterLimit_10deg_2.00num.png differ diff --git a/tests/images/paths/miterLimit_145deg_2.00num.png b/tests/images/paths/miterLimit_145deg_2.00num.png new file mode 100644 index 0000000..5938bc2 Binary files /dev/null and b/tests/images/paths/miterLimit_145deg_2.00num.png differ diff --git a/tests/images/paths/miterLimit_145deg_3.32num.png b/tests/images/paths/miterLimit_145deg_3.32num.png new file mode 100644 index 0000000..5938bc2 Binary files /dev/null and b/tests/images/paths/miterLimit_145deg_3.32num.png differ diff --git a/tests/images/paths/miterLimit_145deg_3.33num.png b/tests/images/paths/miterLimit_145deg_3.33num.png new file mode 100644 index 0000000..237608a Binary files /dev/null and b/tests/images/paths/miterLimit_145deg_3.33num.png differ diff --git a/tests/images/paths/miterLimit_155deg_2.00num.png b/tests/images/paths/miterLimit_155deg_2.00num.png new file mode 100644 index 0000000..76493bb Binary files /dev/null and b/tests/images/paths/miterLimit_155deg_2.00num.png differ diff --git a/tests/images/paths/miterLimit_165deg_10.00num.png b/tests/images/paths/miterLimit_165deg_10.00num.png new file mode 100644 index 0000000..c65f635 Binary files /dev/null and b/tests/images/paths/miterLimit_165deg_10.00num.png differ diff --git a/tests/images/paths/miterLimit_165deg_2.00num.png b/tests/images/paths/miterLimit_165deg_2.00num.png new file mode 100644 index 0000000..1a96656 Binary files /dev/null and b/tests/images/paths/miterLimit_165deg_2.00num.png differ diff --git a/tests/test_paths.nim b/tests/test_paths.nim index 1dfa970..93d2d8e 100644 --- a/tests/test_paths.nim +++ b/tests/test_paths.nim @@ -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: