From b73b88bb634ff3fc3c5e2f6883194fe5d581b2cd Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Sun, 23 May 2021 18:56:21 -0500 Subject: [PATCH] layers --- src/pixie/context.nim | 294 ++++++++++++++++------------- tests/images/context/clip_1d.png | Bin 0 -> 3664 bytes tests/images/context/clip_1e.png | Bin 0 -> 618 bytes tests/images/context/clip_text.png | Bin 0 -> 4569 bytes tests/test_context.nim | 73 +++++++ 5 files changed, 237 insertions(+), 130 deletions(-) create mode 100644 tests/images/context/clip_1d.png create mode 100644 tests/images/context/clip_1e.png create mode 100644 tests/images/context/clip_text.png diff --git a/src/pixie/context.nim b/src/pixie/context.nim index a081e57..1ed8788 100644 --- a/src/pixie/context.nim +++ b/src/pixie/context.nim @@ -18,6 +18,7 @@ type path: Path mat: Mat3 mask: Mask + layer: Image stateStack: seq[ContextState] ContextState = object @@ -29,6 +30,10 @@ type textAlign: HAlignMode mat: Mat3 mask: Mask + layer: Image + + TextMetrics* = object + width*: float32 proc newContext*(image: Image): Context = ## Create a new Context that will draw to the parameter image. @@ -43,6 +48,116 @@ proc newContext*(width, height: int): Context {.inline.} = ## Create a new Context that will draw to a new image of width and height. newContext(newImage(width, height)) +proc state(ctx: Context): ContextState = + result.fillStyle = ctx.fillStyle + result.strokeStyle = ctx.strokeStyle + result.lineWidth = ctx.lineWidth + result.lineCap = ctx.lineCap + result.lineJoin = ctx.lineJoin + result.font = ctx.font + result.textAlign = ctx.textAlign + 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 + ## a stack. + ctx.stateStack.add(ctx.state()) + +proc saveLayer*(ctx: Context) = + 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 + ## in the drawing state stack. If there is no saved state, this method does + ## nothing. + if ctx.stateStack.len == 0: + return + + let + poppedLayer = ctx.layer + poppedMask = ctx.mask + + let state = ctx.stateStack.pop() + ctx.fillStyle = state.fillStyle + ctx.strokeStyle = state.strokeStyle + ctx.lineWidth = state.lineWidth + ctx.lineCap = state.lineCap + ctx.lineJoin = state.lineJoin + ctx.font = state.font + ctx.textAlign = state.textAlign + ctx.mat = state.mat + ctx.mask = state.mask + ctx.layer = state.layer + + if poppedLayer != nil: # If there is a layer being popped + if poppedMask != nil: # If there is a mask, apply it + poppedLayer.draw(poppedMask) + if ctx.layer != nil: # If we popped to another layer, draw to it + ctx.layer.draw(poppedLayer) + else: # Otherwise draw to the root image + ctx.image.draw(poppedLayer) + +proc fill( + ctx: Context, image: Image, path: Path, windingRule: WindingRule +) {.inline.} = + image.fillPath( + path, + ctx.fillStyle, + ctx.mat, + windingRule + ) + +proc stroke(ctx: Context, image: Image, path: Path) {.inline.} = + image.strokePath( + path, + ctx.strokeStyle, + ctx.mat, + ctx.lineWidth, + ctx.lineCap, + ctx.lineJoin + ) + +proc fillText(ctx: Context, image: Image, text: string, at: Vec2) {.inline.} = + if ctx.font.typeface == nil: + raise newException(PixieError, "No font has been set on this Context") + + # Canvas positions text relative to the alphabetic baseline by default + var at = at + at.y -= round(ctx.font.typeface.ascent * ctx.font.scale) + + ctx.font.paint = ctx.fillStyle + + image.fillText( + ctx.font, + text, + ctx.mat * translate(at), + hAlign = ctx.textAlign + ) + +proc strokeText(ctx: Context, image: Image, text: string, at: Vec2) {.inline.} = + if ctx.font.typeface == nil: + raise newException(PixieError, "No font has been set on this Context") + + # Canvas positions text relative to the alphabetic baseline by default + var at = at + at.y -= round(ctx.font.typeface.ascent * ctx.font.scale) + + ctx.font.paint = ctx.strokeStyle + + image.strokeText( + ctx.font, + text, + ctx.mat * translate(at), + ctx.lineWidth, + hAlign = ctx.textAlign, + lineCap = ctx.lineCap, + lineJoin = ctx.lineJoin + ) + proc beginPath*(ctx: Context) {.inline.} = ## Starts a new path by emptying the list of sub-paths. ctx.path = Path() @@ -121,23 +236,14 @@ proc ellipse*(ctx: Context, x, y, rx, ry: float32) {.inline.} = proc fill*(ctx: Context, path: Path, windingRule = wrNonZero) {.inline.} = ## Fills the path with the current fillStyle. - if ctx.mask != nil: - let tmp = newImage(ctx.image.width, ctx.image.height) - tmp.fillPath( - path, - ctx.fillStyle, - ctx.mat, - windingRule - ) - tmp.draw(ctx.mask) - ctx.image.draw(tmp) + if ctx.mask != nil and ctx.layer == nil: + ctx.saveLayer() + ctx.fill(ctx.layer, path, windingRule) + ctx.restore() + elif ctx.layer != nil: + ctx.fill(ctx.layer, path, windingRule) else: - ctx.image.fillPath( - path, - ctx.fillStyle, - ctx.mat, - windingRule - ) + ctx.fill(ctx.image, path, windingRule) proc fill*(ctx: Context, windingRule = wrNonZero) {.inline.} = ## Fills the current path with the current fillStyle. @@ -163,27 +269,14 @@ proc clip*(ctx: Context, windingRule = wrNonZero) {.inline.} = proc stroke*(ctx: Context, path: Path) {.inline.} = ## Strokes (outlines) the current or given path with the current strokeStyle. - if ctx.mask != nil: - let tmp = newImage(ctx.image.width, ctx.image.height) - tmp.strokePath( - path, - ctx.strokeStyle, - ctx.mat, - ctx.lineWidth, - ctx.lineCap, - ctx.lineJoin - ) - tmp.draw(ctx.mask) - ctx.image.draw(tmp) + if ctx.mask != nil and ctx.layer == nil: + ctx.saveLayer() + ctx.stroke(ctx.layer, path) + ctx.restore() + elif ctx.layer != nil: + ctx.stroke(ctx.layer, path) else: - ctx.image.strokePath( - path, - ctx.strokeStyle, - ctx.mat, - ctx.lineWidth, - ctx.lineCap, - ctx.lineJoin - ) + ctx.stroke(ctx.image, path) proc stroke*(ctx: Context) {.inline.} = ## Strokes (outlines) the current or given path with the current strokeStyle. @@ -193,11 +286,18 @@ proc clearRect*(ctx: Context, rect: Rect) = ## Erases the pixels in a rectangular area. var path: Path path.rect(rect) - ctx.image.fillPath( - path, - Paint(kind: pkSolid, color:rgbx(0, 0, 0, 0), blendMode: bmOverwrite), - ctx.mat - ) + if ctx.layer != nil: + ctx.layer.fillPath( + path, + 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), + ctx.mat + ) proc clearRect*(ctx: Context, x, y, width, height: float32) {.inline.} = ## Erases the pixels in a rectangular area. @@ -228,33 +328,14 @@ proc strokeRect*(ctx: Context, x, y, width, height: float32) {.inline.} = proc fillText*(ctx: Context, text: string, at: Vec2) = ## Draws a text string at the specified coordinates, filling the string's ## characters with the current fillStyle - - if ctx.font.typeface == nil: - raise newException(PixieError, "No font has been set on this Context") - - # Canvas positions text relative to the alphabetic baseline by default - var at = at - at.y -= round(ctx.font.typeface.ascent * ctx.font.scale) - - ctx.font.paint = ctx.fillStyle - - if ctx.mask != nil: - let tmp = newImage(ctx.image.width, ctx.image.height) - tmp.fillText( - ctx.font, - text, - ctx.mat * translate(at), - hAlign = ctx.textAlign - ) - tmp.draw(ctx.mask) - ctx.image.draw(tmp) + if ctx.mask != nil and ctx.layer == nil: + ctx.saveLayer() + ctx.fillText(ctx.layer, text, at) + ctx.restore() + elif ctx.layer != nil: + ctx.fillText(ctx.layer, text, at) else: - ctx.image.fillText( - ctx.font, - text, - ctx.mat * translate(at), - hAlign = ctx.textAlign - ) + ctx.fillText(ctx.image, text, at) proc fillText*(ctx: Context, text: string, x, y: float32) {.inline.} = ## Draws the outlines of the characters of a text string at the specified @@ -264,45 +345,29 @@ proc fillText*(ctx: Context, text: string, x, y: float32) {.inline.} = proc strokeText*(ctx: Context, text: string, at: Vec2) = ## Draws the outlines of the characters of a text string at the specified ## coordinates. - - if ctx.font.typeface == nil: - raise newException(PixieError, "No font has been set on this Context") - - # Canvas positions text relative to the alphabetic baseline by default - var at = at - at.y -= round(ctx.font.typeface.ascent * ctx.font.scale) - - ctx.font.paint = ctx.strokeStyle - - if ctx.mask != nil: - let tmp = newImage(ctx.image.width, ctx.image.height) - tmp.strokeText( - ctx.font, - text, - ctx.mat * translate(at), - ctx.lineWidth, - hAlign = ctx.textAlign, - lineCap = ctx.lineCap, - lineJoin = ctx.lineJoin - ) - tmp.draw(ctx.mask) - ctx.image.draw(tmp) + if ctx.mask != nil and ctx.layer == nil: + ctx.saveLayer() + ctx.strokeText(ctx.layer, text, at) + ctx.restore() + elif ctx.layer != nil: + ctx.strokeText(ctx.layer, text, at) else: - ctx.image.strokeText( - ctx.font, - text, - ctx.mat * translate(at), - ctx.lineWidth, - hAlign = ctx.textAlign, - lineCap = ctx.lineCap, - lineJoin = ctx.lineJoin - ) + ctx.strokeText(ctx.image, text, at) proc strokeText*(ctx: Context, text: string, x, y: float32) {.inline.} = ## Draws the outlines of the characters of a text string at the specified ## coordinates. ctx.strokeText(text, vec2(x, y)) +proc measureText*(ctx: Context, text: string): TextMetrics = + ## Returns a TextMetrics object that contains information about the measured + ## text (such as its width, for example). + if ctx.font.typeface == nil: + raise newException(PixieError, "No font has been set on this Context") + + let bounds = typeset(ctx.font, text).computeBounds() + result.width = bounds.x + proc getTransform*(ctx: Context): Mat3 {.inline.} = ## Retrieves the current transform matrix being applied to the context. ctx.mat @@ -351,37 +416,6 @@ proc resetTransform*(ctx: Context) {.inline.} = ## Resets the current transform to the identity matrix. ctx.mat = mat3() -proc save*(ctx: Context) = - ## Saves the entire state of the canvas by pushing the current state onto - ## a stack. - var state: ContextState - state.fillStyle = ctx.fillStyle - state.strokeStyle = ctx.strokeStyle - state.lineWidth = ctx.lineWidth - state.lineCap = ctx.lineCap - state.lineJoin = ctx.lineJoin - state.font = ctx.font - state.textAlign = ctx.textAlign - state.mat = ctx.mat - state.mask = if ctx.mask != nil: ctx.mask.copy() else: nil - ctx.stateStack.add(state) - -proc restore*(ctx: Context) = - ## Restores the most recently saved canvas 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: - let state = ctx.stateStack.pop() - ctx.fillStyle = state.fillStyle - ctx.strokeStyle = state.strokeStyle - ctx.lineWidth = state.lineWidth - ctx.lineCap = state.lineCap - ctx.lineJoin = state.lineJoin - ctx.font = state.font - ctx.textAlign = state.textAlign - ctx.mat = state.mat - ctx.mask = state.mask - # Additional procs that are not part of the JS API proc roundedRect*(ctx: Context, x, y, w, h, nw, ne, se, sw: float32) {.inline.} = diff --git a/tests/images/context/clip_1d.png b/tests/images/context/clip_1d.png new file mode 100644 index 0000000000000000000000000000000000000000..bdee0a9680ee5fe888ed0fdace99749ce6a154c3 GIT binary patch literal 3664 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 OAqG!ZKbLh*2~7a=hPXQb literal 0 HcmV?d00001 diff --git a/tests/images/context/clip_text.png b/tests/images/context/clip_text.png new file mode 100644 index 0000000000000000000000000000000000000000..b12da69b7c2721de831d1695b08dfcb1ec3626c6 GIT binary patch literal 4569 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