diff --git a/src/pixie/common.nim b/src/pixie/common.nim index 8c87416..d2e7bbb 100644 --- a/src/pixie/common.nim +++ b/src/pixie/common.nim @@ -1,4 +1,4 @@ -import chroma, vmath +import bumpy, chroma, vmath type PixieError* = object of ValueError ## Raised if an operation fails. @@ -16,9 +16,20 @@ proc lerp*(a, b: ColorRGBX, t: float32): ColorRGBX {.inline.} = result.b = ((a.b.uint32 * (255 - x) + b.b.uint32 * x) div 255).uint8 result.a = ((a.a.uint32 * (255 - x) + b.a.uint32 * x) div 255).uint8 -func lerp*(a, b: Color, v: float32): Color {.inline.} = +proc lerp*(a, b: Color, v: float32): Color {.inline.} = ## Linearly interpolate between a and b using t. result.r = lerp(a.r, b.r, v) result.g = lerp(a.g, b.g, v) result.b = lerp(a.b, b.b, v) result.a = lerp(a.a, b.a, v) + +proc snapToPixels*(rect: Rect): Rect = + let + xMin = rect.x + xMax = rect.x + rect.w + yMin = rect.y + yMax = rect.y + rect.h + result.x = floor(xMin) + result.w = ceil(xMax) - result.x + result.y = floor(yMin) + result.h = ceil(yMax) - result.y diff --git a/src/pixie/contexts.nim b/src/pixie/contexts.nim index 560992a..36fb48c 100644 --- a/src/pixie/contexts.nim +++ b/src/pixie/contexts.nim @@ -17,9 +17,9 @@ type lineJoin*: LineJoin font*: string ## File path to a .ttf or .otf file. fontSize*: float32 - textAlign*: HAlignMode + textAlign*: HorizontalAlignment + lineDash*: seq[float32] path: Path - lineDash: seq[float32] mat: Mat3 mask: Mask layer: Image @@ -35,7 +35,7 @@ type lineJoin: LineJoin font: string fontSize*: float32 - textAlign: HAlignMode + textAlign: HorizontalAlignment lineDash: seq[float32] mat: Mat3 mask: Mask @@ -289,6 +289,22 @@ proc quadraticCurveTo*(ctx: Context, ctrl, to: Vec2) {.inline.} = ## Bézier curve. ctx.path.quadraticCurveTo(ctrl, to) +proc arc*(ctx: Context, x, y, r, a0, a1: float32, ccw: bool = false) = + ## Draws a circular arc. + ctx.path.arc(x, y, r, a0, a1, ccw) + +proc arc*(ctx: Context, pos: Vec2, r: float32, a: Vec2, ccw: bool = false) = + ## Adds a circular arc to the current sub-path. + ctx.path.arc(pos, r, a, ccw) + +proc arcTo*(ctx: Context, x1, y1, x2, y2, radius: float32) = + ## Draws a circular arc using the given control points and radius. + ctx.path.arcTo(x1, y1, x2, y2, radius) + +proc arcTo*(ctx: Context, a, b: Vec2, r: float32) = + ## Adds a circular arc using the given control points and radius. + ctx.path.arcTo(a, b, r) + proc closePath*(ctx: Context) {.inline.} = ## Attempts to add a straight line from the current point to the start of ## the current sub-path. If the shape has already been closed or has only @@ -326,6 +342,8 @@ proc fill*(ctx: Context, windingRule = wrNonZero) {.inline.} = ## Fills the current path with the current fillStyle. ctx.fill(ctx.path, windingRule) +proc clip*(ctx: Context, windingRule = wrNonZero) {.inline.} + proc clip*(ctx: Context, path: Path, windingRule = wrNonZero) = ## Turns the path into the current clipping region. The previous clipping ## region, if any, is intersected with the current or given path to create @@ -342,6 +360,8 @@ proc clip*(ctx: Context, windingRule = wrNonZero) {.inline.} = ## to create the new clipping region. ctx.clip(ctx.path, windingRule) +proc stroke*(ctx: Context) {.inline.} + proc stroke*(ctx: Context, path: Path) = ## Strokes (outlines) the current or given path with the current strokeStyle. if ctx.mask != nil and ctx.layer == nil: @@ -491,7 +511,110 @@ proc resetTransform*(ctx: Context) {.inline.} = ## Resets the current transform to the identity matrix. ctx.mat = mat3() +proc drawImage*(ctx: Context, image: Image, dx, dy, dWidth, dHeight: float32) = + ## Draws a source image onto the destination image. + let + imageMat = ctx.mat * translate(vec2(dx, dy)) * scale(vec2( + dWidth / image.width.float32, + dHeight / image.height.float32 + )) + savedFillStyle = ctx.fillStyle + + ctx.fillStyle = newPaint(pkImage) + ctx.fillStyle.image = image + ctx.fillStyle.imageMat = imageMat + + let path = newPath() + path.rect(rect(dx, dy, dWidth, dHeight)) + ctx.fill(path) + + ctx.fillStyle = savedFillStyle + +proc drawImage*(ctx: Context, image: Image, dx, dy: float32) = + ## Draws a source image onto the destination image. + ctx.drawImage(image, dx, dx, image.width.float32, image.height.float32) + +proc drawImage*(ctx: Context, image: Image, pos: Vec2) = + ## Draws a source image onto the destination image. + ctx.drawImage(image, pos.x, pos.y) + +proc drawImage*(ctx: Context, image: Image, rect: Rect) = + ## Draws a source image onto the destination image. + ctx.drawImage(image, rect.x, rect.y, rect.w, rect.h) + +proc drawImage*( + ctx: Context, + image: Image, + sx, sy, sWidth, sHeight, + dx, dy, dWidth, dHeight: float32 +) = + ## Draws a source image onto the destination image. + let image = image.subImage(sx.int, sy.int, sWidth.int, sHeight.int) + ctx.drawImage(image, dx, dx, image.width.float32, image.height.float32) + +proc drawImage*(ctx: Context, image: Image, src, dest: Rect) = + ## Draws a source image onto the destination image. + ctx.drawImage( + image, + src.x, src.y, src.w, src.h, + dest.x, dest.y, dest.w, dest.h + ) + +proc isPointInPath*( + ctx: Context, path: Path, pos: Vec2, windingRule = wrNonZero +): bool = + ## Returns whether or not the specified point is contained in the current path. + path.fillOverlaps(pos, ctx.mat, windingRule) + +proc isPointInPath*( + ctx: Context, path: Path, x, y: float32, windingRule = wrNonZero +): bool {.inline.} = + ## Returns whether or not the specified point is contained in the current path. + ctx.isPointInPath(path, vec2(x, y), windingRule) + +proc isPointInPath*( + ctx: Context, pos: Vec2, windingRule = wrNonZero +): bool {.inline.} = + ## Returns whether or not the specified point is contained in the current path. + ctx.isPointInPath(ctx.path, pos, windingRule) + +proc isPointInPath*( + ctx: Context, x, y: float32, windingRule = wrNonZero +): bool {.inline.} = + ## Returns whether or not the specified point is contained in the current path. + ctx.isPointInPath(ctx.path, vec2(x, y), windingRule) + +proc isPointInStroke*(ctx: Context, path: Path, pos: Vec2): bool = + ## Returns whether or not the specified point is inside the area contained + ## by the stroking of a path. + path.strokeOverlaps( + pos, + ctx.mat, + ctx.lineWidth, + ctx.lineCap, + ctx.lineJoin, + ctx.miterLimit, + ctx.lineDash + ) + +proc isPointInStroke*(ctx: Context, path: Path, x, y: float32): bool {.inline.} = + ## Returns whether or not the specified point is inside the area contained + ## by the stroking of a path. + ctx.isPointInStroke(path, vec2(x, y)) + +proc isPointInStroke*(ctx: Context, pos: Vec2): bool {.inline.} = + ## Returns whether or not the specified point is inside the area contained + ## by the stroking of a path. + ctx.isPointInStroke(ctx.path, pos) + +proc isPointInStroke*(ctx: Context, x, y: float32): bool {.inline.} = + ## Returns whether or not the specified point is inside the area contained + ## by the stroking of a path. + ctx.isPointInStroke(ctx.path, vec2(x, y)) + +# # Additional procs that are not part of the JS API +# proc roundedRect*(ctx: Context, x, y, w, h, nw, ne, se, sw: float32) {.inline.} = ## Adds a rounded rectangle to the current path. @@ -566,12 +689,6 @@ proc fillCircle*(ctx: Context, circle: Circle) = path.circle(circle) ctx.fill(path) -proc fillCircle*(ctx: Context, center: Vec2, radius: float32) = - ## Draws a circle that is filled according to the current fillStyle. - let path = newPath() - path.ellipse(center, radius, radius) - ctx.fill(path) - proc strokeCircle*(ctx: Context, circle: Circle) = ## Draws a circle that is stroked (outlined) according to the current ## strokeStyle and other context settings. @@ -579,13 +696,6 @@ proc strokeCircle*(ctx: Context, circle: Circle) = path.circle(circle) ctx.stroke(path) -proc strokeCircle*(ctx: Context, center: Vec2, radius: float32) = - ## Draws a circle that is stroked (outlined) according to the current - ## strokeStyle and other context settings. - let path = newPath() - path.ellipse(center, radius, radius) - ctx.stroke(path) - proc fillPolygon*(ctx: Context, pos: Vec2, size: float32, sides: int) = ## Draws an n-sided regular polygon at (x, y) of size that is filled according ## to the current fillStyle. @@ -599,120 +709,3 @@ proc strokePolygon*(ctx: Context, pos: Vec2, size: float32, sides: int) = let path = newPath() path.polygon(pos, size, sides) ctx.stroke(path) - -proc drawImage*(ctx: Context, image: Image, dx, dy, dWidth, dHeight: float32) = - ## Draws a source image onto the destination image. - let - imageMat = ctx.mat * translate(vec2(dx, dy)) * scale(vec2( - dWidth / image.width.float32, - dHeight / image.height.float32 - )) - savedFillStyle = ctx.fillStyle - - ctx.fillStyle = newPaint(pkImage) - ctx.fillStyle.image = image - ctx.fillStyle.imageMat = imageMat - - let path = newPath() - path.rect(rect(dx, dy, dWidth, dHeight)) - ctx.fill(path) - - ctx.fillStyle = savedFillStyle - -proc drawImage*(ctx: Context, image: Image, dx, dy: float32) = - ## Draws a source image onto the destination image. - ctx.drawImage(image, dx, dx, image.width.float32, image.height.float32) - -proc drawImage*(ctx: Context, image: Image, pos: Vec2) = - ## Draws a source image onto the destination image. - ctx.drawImage(image, pos.x, pos.y) - -proc drawImage*(ctx: Context, image: Image, rect: Rect) = - ## Draws a source image onto the destination image. - ctx.drawImage(image, rect.x, rect.y, rect.w, rect.h) - -proc drawImage*( - ctx: Context, - image: Image, - sx, sy, sWidth, sHeight, - dx, dy, dWidth, dHeight: float32 -) = - ## Draws a source image onto the destination image. - let image = image.subImage(sx.int, sy.int, sWidth.int, sHeight.int) - ctx.drawImage(image, dx, dx, image.width.float32, image.height.float32) - -proc drawImage*(ctx: Context, image: Image, src, dest: Rect) = - ## Draws a source image onto the destination image. - ctx.drawImage( - image, - src.x, src.y, src.w, src.h, - dest.x, dest.y, dest.w, dest.h - ) - -proc arc*(ctx: Context, x, y, r, a0, a1: float32, ccw: bool = false) = - ## Draws a circular arc. - ctx.path.arc(x, y, r, a0, a1, ccw) - -proc arc*(ctx: Context, pos: Vec2, r: float32, a: Vec2, ccw: bool = false) = - ## Adds a circular arc to the current sub-path. - ctx.path.arc(pos, r, a, ccw) - -proc arcTo*(ctx: Context, x1, y1, x2, y2, radius: float32) = - ## Draws a circular arc using the given control points and radius. - ctx.path.arcTo(x1, y1, x2, y2, radius) - -proc arcTo*(ctx: Context, a, b: Vec2, r: float32) = - ## Adds a circular arc using the given control points and radius. - ctx.path.arcTo(a, b, r) - -proc isPointInPath*( - ctx: Context, path: Path, pos: Vec2, windingRule = wrNonZero -): bool = - ## Returns whether or not the specified point is contained in the current path. - path.fillOverlaps(pos, ctx.mat, windingRule) - -proc isPointInPath*( - ctx: Context, path: Path, x, y: float32, windingRule = wrNonZero -): bool {.inline.} = - ## Returns whether or not the specified point is contained in the current path. - ctx.isPointInPath(path, vec2(x, y), windingRule) - -proc isPointInPath*( - ctx: Context, pos: Vec2, windingRule = wrNonZero -): bool {.inline.} = - ## Returns whether or not the specified point is contained in the current path. - ctx.isPointInPath(ctx.path, pos, windingRule) - -proc isPointInPath*( - ctx: Context, x, y: float32, windingRule = wrNonZero -): bool {.inline.} = - ## Returns whether or not the specified point is contained in the current path. - ctx.isPointInPath(ctx.path, vec2(x, y), windingRule) - -proc isPointInStroke*(ctx: Context, path: Path, pos: Vec2): bool = - ## Returns whether or not the specified point is inside the area contained - ## by the stroking of a path. - path.strokeOverlaps( - pos, - ctx.mat, - ctx.lineWidth, - ctx.lineCap, - ctx.lineJoin, - ctx.miterLimit, - ctx.lineDash - ) - -proc isPointInStroke*(ctx: Context, path: Path, x, y: float32): bool {.inline.} = - ## Returns whether or not the specified point is inside the area contained - ## by the stroking of a path. - ctx.isPointInStroke(path, vec2(x, y)) - -proc isPointInStroke*(ctx: Context, pos: Vec2): bool {.inline.} = - ## Returns whether or not the specified point is inside the area contained - ## by the stroking of a path. - ctx.isPointInStroke(ctx.path, pos) - -proc isPointInStroke*(ctx: Context, x, y: float32): bool {.inline.} = - ## Returns whether or not the specified point is inside the area contained - ## by the stroking of a path. - ctx.isPointInStroke(ctx.path, vec2(x, y)) diff --git a/src/pixie/fonts.nim b/src/pixie/fonts.nim index b5cb207..4271404 100644 --- a/src/pixie/fonts.nim +++ b/src/pixie/fonts.nim @@ -35,12 +35,12 @@ type positions*: seq[Vec2] ## The positions of the glyphs for each rune. selectionRects*: seq[Rect] ## The selection rects for each glyph. - HAlignMode* = enum + HorizontalAlignment* = enum haLeft haCenter haRight - VAlignMode* = enum + VerticalAlignment* = enum vaTop vaMiddle vaBottom diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 1c85246..ed312f8 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -1021,7 +1021,13 @@ proc requiresAntiAliasing(segments: seq[(Segment, int16)]): bool = # AA is required if all segments are not vertical or have fractional > 0 return true -proc computePixelBounds(segments: seq[(Segment, int16)]): Rect = +proc transform(shapes: var seq[seq[Vec2]], transform: Mat3) = + if transform != mat3(): + for shape in shapes.mitems: + for vec in shape.mitems: + vec = transform * vec + +proc computeBounds(segments: seq[(Segment, int16)]): Rect = ## Compute the bounds of the segments. var xMin = float32.high @@ -1035,11 +1041,6 @@ proc computePixelBounds(segments: seq[(Segment, int16)]): Rect = yMin = min(yMin, segment.at.y) yMax = max(yMax, segment.to.y) - xMin = floor(xMin) - xMax = ceil(xMax) - yMin = floor(yMin) - yMax = ceil(yMax) - if xMin.isNaN() or xMax.isNaN() or yMin.isNaN() or yMax.isNaN(): discard else: @@ -1048,9 +1049,11 @@ proc computePixelBounds(segments: seq[(Segment, int16)]): Rect = result.w = xMax - xMin result.h = yMax - yMin -proc computePixelBounds*(path: Path): Rect = +proc computeBounds*(path: Path, transform = mat3()): Rect = ## Compute the bounds of the path. - path.commandsToShapes().shapesToSegments().computePixelBounds() + var shapes = path.commandsToShapes() + shapes.transform(transform) + computeBounds(shapes.shapesToSegments()) proc partitionSegments( segments: seq[(Segment, int16)], top, height: int @@ -1482,7 +1485,7 @@ proc fillShapes( rgbx = color.asRgbx() segments = shapes.shapesToSegments() aa = segments.requiresAntiAliasing() - bounds = computePixelBounds(segments) + bounds = computeBounds(segments).snapToPixels() startX = max(0, bounds.x.int) startY = max(0, bounds.y.int) pathHeight = min(image.height, (bounds.y + bounds.h).int) @@ -1539,7 +1542,7 @@ proc fillShapes( let segments = shapes.shapesToSegments() aa = segments.requiresAntiAliasing() - bounds = computePixelBounds(segments) + bounds = computeBounds(segments).snapToPixels() startX = max(0, bounds.x.int) startY = max(0, bounds.y.int) pathHeight = min(mask.height, (bounds.y + bounds.h).int) @@ -1729,12 +1732,6 @@ proc parseSomePath( elif type(path) is Path: path.commandsToShapes(closeSubpaths, pixelScale) -proc transform(shapes: var seq[seq[Vec2]], transform: Mat3) = - if transform != mat3(): - for shape in shapes.mitems: - for segment in shape.mitems: - segment = transform * segment - proc fillPath*( mask: Mask, path: SomePath,