diff --git a/src/pixie.nim b/src/pixie.nim index d8221cb..822f11a 100644 --- a/src/pixie.nim +++ b/src/pixie.nim @@ -1,8 +1,7 @@ import bumpy, chroma, flatty/binny, os, pixie/blends, pixie/common, pixie/context, pixie/fileformats/bmp, pixie/fileformats/gif, pixie/fileformats/jpg, pixie/fileformats/png, pixie/fileformats/svg, - pixie/fonts, pixie/images, pixie/masks, pixie/paints, pixie/paths, strutils, - vmath + pixie/fonts, pixie/images, pixie/masks, pixie/paints, pixie/paths, strutils, vmath export blends, bumpy, chroma, common, context, fonts, images, masks, paints, paths, vmath diff --git a/src/pixie/context.nim b/src/pixie/context.nim index 1ed8788..1f46bab 100644 --- a/src/pixie/context.nim +++ b/src/pixie/context.nim @@ -11,11 +11,13 @@ type image*: Image fillStyle*, strokeStyle*: Paint lineWidth*: float32 + miterLimit*: float32 lineCap*: LineCap lineJoin*: LineJoin font*: Font textAlign*: HAlignMode path: Path + lineDash: seq[float32] mat: Mat3 mask: Mask layer: Image @@ -24,10 +26,12 @@ type ContextState = object fillStyle, strokeStyle: Paint lineWidth: float32 + miterLimit: float32 lineCap: LineCap lineJoin: LineJoin font: Font textAlign: HAlignMode + lineDash: seq[float32] mat: Mat3 mask: Mask layer: Image @@ -41,6 +45,7 @@ proc newContext*(image: Image): Context = result.image = image result.mat = mat3() result.lineWidth = 1 + result.miterLimit = 10 result.fillStyle = Paint(kind: pkSolid, color: rgbx(0, 0, 0, 255)) result.strokeStyle = Paint(kind: pkSolid, color: rgbx(0, 0, 0, 255)) @@ -52,26 +57,31 @@ proc state(ctx: Context): ContextState = result.fillStyle = ctx.fillStyle result.strokeStyle = ctx.strokeStyle result.lineWidth = ctx.lineWidth + result.miterLimit = ctx.miterLimit result.lineCap = ctx.lineCap result.lineJoin = ctx.lineJoin result.font = ctx.font result.textAlign = ctx.textAlign + result.lineDash = ctx.lineDash result.mat = ctx.mat result.mask = if ctx.mask != nil: ctx.mask.copy() else: nil proc save*(ctx: Context) {.inline.} = - ## Saves the entire state of the canvas by pushing the current state onto + ## Saves the entire state of the context by pushing the current state onto ## a stack. ctx.stateStack.add(ctx.state()) proc saveLayer*(ctx: Context) = + ## Saves the entire state of the context by pushing the current state onto + ## a stack and allocates a new image layer for subsequent drawing. Calling + ## restore blends the current layer image onto the prior layer or root image. var state = ctx.state() state.layer = ctx.layer ctx.stateStack.add(state) ctx.layer = newImage(ctx.image.width, ctx.image.height) proc restore*(ctx: Context) = - ## Restores the most recently saved canvas state by popping the top entry + ## Restores the most recently saved context state by popping the top entry ## in the drawing state stack. If there is no saved state, this method does ## nothing. if ctx.stateStack.len == 0: @@ -85,10 +95,12 @@ proc restore*(ctx: Context) = ctx.fillStyle = state.fillStyle ctx.strokeStyle = state.strokeStyle ctx.lineWidth = state.lineWidth + ctx.miterLimit = state.miterLimit ctx.lineCap = state.lineCap ctx.lineJoin = state.lineJoin ctx.font = state.font ctx.textAlign = state.textAlign + ctx.lineDash = state.lineDash ctx.mat = state.mat ctx.mask = state.mask ctx.layer = state.layer @@ -118,7 +130,9 @@ proc stroke(ctx: Context, image: Image, path: Path) {.inline.} = ctx.mat, ctx.lineWidth, ctx.lineCap, - ctx.lineJoin + ctx.lineJoin, + ctx.miterLimit, + ctx.lineDash ) proc fillText(ctx: Context, image: Image, text: string, at: Vec2) {.inline.} = @@ -155,7 +169,9 @@ proc strokeText(ctx: Context, image: Image, text: string, at: Vec2) {.inline.} = ctx.lineWidth, hAlign = ctx.textAlign, lineCap = ctx.lineCap, - lineJoin = ctx.lineJoin + lineJoin = ctx.lineJoin, + miterLimit = ctx.miterLimit, + dashes = ctx.lineDash ) proc beginPath*(ctx: Context) {.inline.} = @@ -289,13 +305,13 @@ proc clearRect*(ctx: Context, rect: Rect) = if ctx.layer != nil: ctx.layer.fillPath( path, - Paint(kind: pkSolid, color:rgbx(0, 0, 0, 0), blendMode: bmOverwrite), + Paint(kind: pkSolid, color: rgbx(0, 0, 0, 0), blendMode: bmOverwrite), ctx.mat ) else: ctx.image.fillPath( path, - Paint(kind: pkSolid, color:rgbx(0, 0, 0, 0), blendMode: bmOverwrite), + Paint(kind: pkSolid, color: rgbx(0, 0, 0, 0), blendMode: bmOverwrite), ctx.mat ) @@ -368,6 +384,12 @@ proc measureText*(ctx: Context, text: string): TextMetrics = let bounds = typeset(ctx.font, text).computeBounds() result.width = bounds.x +proc getLineDash*(ctx: Context): seq[float32] {.inline.} = + ctx.lineDash + +proc setLineDash*(ctx: Context, lineDash: seq[float32]) {.inline.} = + ctx.lineDash = lineDash + proc getTransform*(ctx: Context): Mat3 {.inline.} = ## Retrieves the current transform matrix being applied to the context. ctx.mat @@ -399,12 +421,12 @@ proc translate*(ctx: Context, x, y: float32) {.inline.} = ctx.mat = ctx.mat * translate(vec2(x, y)) proc scale*(ctx: Context, v: Vec2) {.inline.} = - ## Adds a scaling transformation to the canvas units horizontally and/or + ## Adds a scaling transformation to the context units horizontally and/or ## vertically. ctx.mat = ctx.mat * scale(v) proc scale*(ctx: Context, x, y: float32) {.inline.} = - ## Adds a scaling transformation to the canvas units horizontally and/or + ## Adds a scaling transformation to the context units horizontally and/or ## vertically. ctx.mat = ctx.mat * scale(vec2(x, y)) diff --git a/src/pixie/demo.nim b/src/pixie/demo.nim index 284e947..7520da8 100644 --- a/src/pixie/demo.nim +++ b/src/pixie/demo.nim @@ -1,7 +1,6 @@ -import opengl, pixie, pixie/context -import staticglfw except Image -export pixie -export staticglfw except Image +import opengl, pixie, pixie/context, staticglfw except Image + +export pixie, staticglfw except Image var dpi: float32 = 1.0 diff --git a/src/pixie/fileformats/svg.nim b/src/pixie/fileformats/svg.nim index c460ed5..35e0b00 100644 --- a/src/pixie/fileformats/svg.nim +++ b/src/pixie/fileformats/svg.nim @@ -1,7 +1,7 @@ ## Load SVG files. -import chroma, pixie/common, pixie/images, pixie/paths, pixie/paints, strutils, vmath, - xmlparser, xmltree +import chroma, pixie/common, pixie/images, pixie/paints, pixie/paths, strutils, + vmath, xmlparser, xmltree const xmlSignature* = " @@ -232,9 +268,9 @@ proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) = ctx = decodeCtx(ctxStack[^1], node) path = parsePath(d) if ctx.fill != ColorRGBX(): - img.fillPath(path, ctx.fill, ctx.transform, ctx.fillRule) + img.fill(ctx, path) if ctx.shouldStroke: - img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) + img.stroke(ctx, path) of "line": let @@ -247,12 +283,9 @@ proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) = var path: Path path.moveTo(x1, y1) path.lineTo(x2, y2) - path.closePath() - if ctx.fill != ColorRGBX(): - img.fillPath(path, ctx.fill, ctx.transform) if ctx.shouldStroke: - img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) + img.stroke(ctx, path) of "polyline", "polygon": let @@ -282,13 +315,15 @@ proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) = path.lineTo(vecs[i]) # The difference between polyline and polygon is whether we close the path + # and fill or not if node.tag == "polygon": path.closePath() - if ctx.fill != ColorRGBX(): - img.fillPath(path, ctx.fill, ctx.transform) + if ctx.fill != ColorRGBX(): + img.fill(ctx, path) + if ctx.shouldStroke: - img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) + img.stroke(ctx, path) of "rect": let @@ -324,9 +359,9 @@ proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) = path.rect(x, y, width, height) if ctx.fill != ColorRGBX(): - img.fillPath(path, ctx.fill, ctx.transform) + img.fill(ctx, path) if ctx.shouldStroke: - img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) + img.stroke(ctx, path) of "circle", "ellipse": let @@ -346,9 +381,9 @@ proc draw(img: Image, node: XmlNode, ctxStack: var seq[Ctx]) = path.ellipse(cx, cy, rx, ry) if ctx.fill != ColorRGBX(): - img.fillPath(path, ctx.fill, ctx.transform) + img.fill(ctx, path) if ctx.shouldStroke: - img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) + img.stroke(ctx, path) else: raise newException(PixieError, "Unsupported SVG tag: " & node.tag & ".") diff --git a/src/pixie/fonts.nim b/src/pixie/fonts.nim index 02a2254..4159620 100644 --- a/src/pixie/fonts.nim +++ b/src/pixie/fonts.nim @@ -442,7 +442,9 @@ proc strokeText*( transform: Vec2 | Mat3 = vec2(0, 0), strokeWidth = 1.0, lineCap = lcButt, - lineJoin = ljMiter + lineJoin = ljMiter, + miterLimit = defaultMiterLimit, + dashes: seq[float32] = @[] ) = ## Strokes the text arrangement. for spanIndex, (start, stop) in arrangement.spans: @@ -455,10 +457,25 @@ proc strokeText*( ) when type(target) is Image: target.strokePath( - path, font.paint, transform, strokeWidth, lineCap, lineJoin + path, + font.paint, + transform, + strokeWidth, + lineCap, + lineJoin, + miterLimit, + dashes ) else: # target is Mask - target.strokePath(path, transform, strokeWidth, lineCap, lineJoin) + target.strokePath( + path, + transform, + strokeWidth, + lineCap, + lineJoin, + miterLimit, + dashes + ) proc strokeText*( target: Image | Mask, @@ -470,7 +487,9 @@ proc strokeText*( hAlign = haLeft, vAlign = vaTop, lineCap = lcButt, - lineJoin = ljMiter + lineJoin = ljMiter, + miterLimit = defaultMiterLimit, + dashes: seq[float32] = @[] ) {.inline.} = ## Typesets and strokes the text. Optional parameters: ## transform: translation or matrix to apply @@ -485,5 +504,7 @@ proc strokeText*( transform, strokeWidth, lineCap, - lineJoin + lineJoin, + miterLimit, + dashes ) diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index af95fd6..f027f84 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -36,7 +36,9 @@ type SomePath* = Path | string | seq[seq[Vec2]] -const epsilon = 0.0001 * PI ## Tiny value used for some computations. +const + epsilon = 0.0001 * PI ## Tiny value used for some computations. + defaultMiterLimit*: float32 = 4 when defined(release): {.push checks: off.} @@ -1422,15 +1424,16 @@ proc strokeShapes( shape[0] )) + var dashes = dashes + if dashes.len mod 2 != 0: + dashes.add(dashes) + for i in 1 ..< shape.len: let pos = shape[i] 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 @@ -1546,8 +1549,8 @@ proc strokePath*( strokeWidth = 1.0, lineCap = lcButt, lineJoin = ljMiter, - miterLimit: float32 = 4, - dashes: seq[float32] = @[], + miterLimit = defaultMiterLimit, + dashes: seq[float32] = @[] ) = ## Strokes a path. var strokeShapes = strokeShapes( @@ -1569,7 +1572,7 @@ proc strokePath*( strokeWidth = 1.0, lineCap = lcButt, lineJoin = ljMiter, - miterLimit: float32 = 4, + miterLimit = defaultMiterLimit, dashes: seq[float32] = @[] ) = ## Strokes a path. diff --git a/tests/images/context/setLineDash_1.png b/tests/images/context/setLineDash_1.png new file mode 100644 index 0000000..e57dbf8 Binary files /dev/null and b/tests/images/context/setLineDash_1.png differ diff --git a/tests/images/paths/dashes.png b/tests/images/paths/dashes.png index 5389bd8..3681bb0 100644 Binary files a/tests/images/paths/dashes.png and b/tests/images/paths/dashes.png differ diff --git a/tests/images/svg/dashes.png b/tests/images/svg/dashes.png new file mode 100644 index 0000000..3e1f667 Binary files /dev/null and b/tests/images/svg/dashes.png differ diff --git a/tests/images/svg/dashes.svg b/tests/images/svg/dashes.svg new file mode 100644 index 0000000..cb340e7 --- /dev/null +++ b/tests/images/svg/dashes.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/tests/images/svg/flat-color-icons.png b/tests/images/svg/flat-color-icons.png index 661bd07..1af1b33 100644 Binary files a/tests/images/svg/flat-color-icons.png and b/tests/images/svg/flat-color-icons.png differ diff --git a/tests/images/svg/ionicons.png b/tests/images/svg/ionicons.png index 2b2838a..7d74d34 100644 Binary files a/tests/images/svg/ionicons.png and b/tests/images/svg/ionicons.png differ diff --git a/tests/images/svg/miterlimit.png b/tests/images/svg/miterlimit.png new file mode 100644 index 0000000..fde660e Binary files /dev/null and b/tests/images/svg/miterlimit.png differ diff --git a/tests/images/svg/miterlimit.svg b/tests/images/svg/miterlimit.svg new file mode 100644 index 0000000..82b9a32 --- /dev/null +++ b/tests/images/svg/miterlimit.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/tests/test_context.nim b/tests/test_context.nim index 8c1f499..b3d3152 100644 --- a/tests/test_context.nim +++ b/tests/test_context.nim @@ -408,7 +408,6 @@ block: ctx.image.writeFile("tests/images/context/clip_1e.png") - block: let ctx = newContext(newImage(300, 150)) @@ -491,3 +490,28 @@ block: let metrics = ctx.measureText("Hello world") doAssert metrics.width == 61 + +block: + let + image = newImage(300, 150) + ctx = newContext(image) + + var y = 15.float32 + + proc drawDashedLine(pattern: seq[float32]) = + ctx.beginPath(); + ctx.setLineDash(pattern); + ctx.moveTo(0, y); + ctx.lineTo(300, y); + ctx.stroke(); + y += 20; + + drawDashedLine(@[]); + drawDashedLine(@[1.float32, 1]); + drawDashedLine(@[10.float32, 10]); + drawDashedLine(@[20.float32, 5]); + drawDashedLine(@[15.float32, 3, 3, 3]); + drawDashedLine(@[20.float32, 3, 3, 3, 3, 3, 3, 3]); + drawDashedLine(@[12.float32, 3, 3]); + + image.writeFile("tests/images/context/setLineDash_1.png") diff --git a/tests/test_paths.nim b/tests/test_paths.nim index 93d2d8e..8d2f96e 100644 --- a/tests/test_paths.nim +++ b/tests/test_paths.nim @@ -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, strokeWidth=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, strokeWidth=10) + image.strokePath(pathStr, color, strokeWidth = 10) image.writeFile("tests/images/paths/pathStroke2.png") block: @@ -268,12 +268,12 @@ block: image.strokePath( path, rgba(0, 0, 0, 255), vec2(5, 25), 10, lcButt, ljBevel, - dashes = @[2.float32,2] + dashes = @[2.float32, 2] ) image.strokePath( path, rgba(0, 0, 0, 255), vec2(5, 45), 10, lcButt, ljBevel, - dashes = @[4.float32,4] + dashes = @[4.float32, 4] ) image.strokePath( diff --git a/tests/test_svg.nim b/tests/test_svg.nim index 8acb6d8..ac620aa 100644 --- a/tests/test_svg.nim +++ b/tests/test_svg.nim @@ -11,7 +11,9 @@ const files = [ "triangle01", "quad01", "Ghostscript_Tiger", - "scale" + "scale", + "miterlimit", + "dashes" ] for file in files: