diff --git a/src/pixie/internal.nim b/src/pixie/internal.nim index ecd0544..ff8ff8e 100644 --- a/src/pixie/internal.nim +++ b/src/pixie/internal.nim @@ -23,6 +23,18 @@ proc gaussianKernel*(radius: int): seq[uint32] = for i, f in floats: result[i] = round(f * 255 * 1024).uint32 +proc applyOpacity*(color: ColorRGBX, opacity: float32): ColorRGBX = + if opacity == 0: + rgbx(0, 0, 0, 0) + else: + let + x = round(opacity * 255).uint32 + r = ((color.r * x) div 255).uint8 + g = ((color.g * x) div 255).uint8 + b = ((color.b * x) div 255).uint8 + a = ((color.a * x) div 255).uint8 + rgbx(r, g, b, a) + proc toStraightAlpha*(data: var seq[ColorRGBA | ColorRGBX]) = ## Converts an image from premultiplied alpha to straight alpha. ## This is expensive for large images. diff --git a/src/pixie/paints.nim b/src/pixie/paints.nim index 93ed8a7..62a12d4 100644 --- a/src/pixie/paints.nim +++ b/src/pixie/paints.nim @@ -1,4 +1,4 @@ -import blends, chroma, common, images, vmath +import blends, chroma, common, images, internal, options, vmath type PaintKind* = enum @@ -21,6 +21,7 @@ type gradientHandlePositions*: seq[Vec2] ## Gradient positions (image space). gradientStops*: seq[ColorStop] ## Color stops (gradient space). blendMode*: BlendMode ## Blend mode. + opacityOption: Option[float32] ColorStop* = object ## Color stop on a gradient curve. @@ -41,37 +42,55 @@ converter parseSomePaint*(paint: SomePaint): Paint {.inline.} = elif type(paint) is Paint: paint +proc opacity*(paint: Paint): float32 = + ## Paint opacity (applies with color or image opacity). + if paint.opacityOption.isSome: + paint.opacityOption.get() + else: + 1 + +proc `opacity=`*(paint: var Paint, opacity: float32) = + ## Set the paint opacity (applies with color or image opacity). + if opacity >= 0 and opacity <= 1: + paint.opacityOption = some(opacity) + else: + raise newException(PixieError, "Invalid opacity: " & $opacity) + proc toLineSpace(at, to, point: Vec2): float32 {.inline.} = ## Convert position on to where it would fall on a line between at and to. let d = to - at det = d.x * d.x + d.y * d.y - return (d.y * (point.y - at.y) + d.x * (point.x - at.x)) / det + (d.y * (point.y - at.y) + d.x * (point.x - at.x)) / det -proc gradientPut(image: Image, x, y: int, a: float32, stops: seq[ColorStop]) = - ## Put an gradient color based on the "a" - were are we related to a line. +proc gradientPut( + image: Image, paint: Paint, x, y: int, t: float32, stops: seq[ColorStop] +) = + ## Put an gradient color based on `t` - where are we related to a line. var index = -1 for i, stop in stops: - if stop.position < a: + if stop.position < t: index = i - if stop.position > a: + if stop.position > t: break - var color: Color + var color: ColorRGBX if index == -1: # first stop solid - color = stops[0].color.color + color = stops[0].color elif index + 1 >= stops.len: # last stop solid - color = stops[index].color.color + color = stops[index].color else: let gs1 = stops[index] - gs2 = stops[index+1] - color = mix( - gs1.color.color, - gs2.color.color, - (a - gs1.position) / (gs2.position - gs1.position) + gs2 = stops[index + 1] + color = lerp( + gs1.color, + gs2.color, + (t - gs1.position) / (gs2.position - gs1.position) ) + if paint.opacity != 0: + color = color.applyOpacity(paint.opacity) image.setRgbaUnsafe(x, y, color.rgba.rgbx()) proc fillGradientLinear*(image: Image, paint: Paint) = @@ -86,6 +105,9 @@ proc fillGradientLinear*(image: Image, paint: Paint) = if paint.gradientStops.len == 0: raise newException(PixieError, "Gradient must have at least 1 color stop") + if paint.opacity == 0: + return + let at = paint.gradientHandlePositions[0] to = paint.gradientHandlePositions[1] @@ -93,8 +115,8 @@ proc fillGradientLinear*(image: Image, paint: Paint) = for x in 0 ..< image.width: let xy = vec2(x.float32, y.float32) - a = toLineSpace(at, to, xy) - image.gradientPut(x, y, a, paint.gradientStops) + t = toLineSpace(at, to, xy) + image.gradientPut(paint, x, y, t, paint.gradientStops) proc fillGradientRadial*(image: Image, paint: Paint) = ## Fills a radial gradient. @@ -108,6 +130,9 @@ proc fillGradientRadial*(image: Image, paint: Paint) = if paint.gradientStops.len == 0: raise newException(PixieError, "Gradient must have at least 1 color stop") + if paint.opacity == 0: + return + let center = paint.gradientHandlePositions[0] edge = paint.gradientHandlePositions[1] @@ -124,8 +149,8 @@ proc fillGradientRadial*(image: Image, paint: Paint) = for x in 0 ..< image.width: let xy = vec2(x.float32, y.float32) - b = (mat * xy).length() - image.gradientPut(x, y, b, paint.gradientStops) + t = (mat * xy).length() + image.gradientPut(paint, x, y, t, paint.gradientStops) proc fillGradientAngular*(image: Image, paint: Paint) = ## Fills an angular gradient. @@ -139,6 +164,9 @@ proc fillGradientAngular*(image: Image, paint: Paint) = if paint.gradientStops.len == 0: raise newException(PixieError, "Gradient must have at least 1 color stop") + if paint.opacity == 0: + return + let center = paint.gradientHandlePositions[0] edge = paint.gradientHandlePositions[1] @@ -149,5 +177,5 @@ proc fillGradientAngular*(image: Image, paint: Paint) = let xy = vec2(x.float32, y.float32) angle = normalize(xy - center).angle() - a = (angle + gradientAngle + PI/2).fixAngle() / 2 / PI + 0.5 - image.gradientPut(x, y, a, paint.gradientStops) + t = (angle + gradientAngle + PI / 2).fixAngle() / 2 / PI + 0.5 + image.gradientPut(paint, x, y, t, paint.gradientStops) diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 688ce1e..790b0a7 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -1,5 +1,4 @@ -import blends, bumpy, chroma, common, images, masks, paints, pixie/internal, - strutils, vmath +import blends, bumpy, chroma, common, images, masks, paints, internal, strutils, vmath when defined(amd64) and not defined(pixieNoSimd): import nimsimd/sse2 @@ -1780,11 +1779,17 @@ proc fillPath*( windingRule = wrNonZero ) = ## Fills a path. + if paint.opacity == 0: + return + if paint.kind == pkSolid: if paint.color.a > 0 or paint.blendMode == bmOverwrite: var shapes = parseSomePath(path, true, transform.pixelScale()) shapes.transform(transform) - image.fillShapes(shapes, paint.color, windingRule, paint.blendMode) + var color = paint.color + if paint.opacity != 1: + color = color.applyOpacity(paint.opacity) + image.fillShapes(shapes, color, windingRule, paint.blendMode) return let @@ -1793,19 +1798,25 @@ proc fillPath*( mask.fillPath(path, transform, windingRule) + var paintOpaque = paint + paintOpaque.opacity = 1 + case paint.kind: of pkSolid: discard # Handled above of pkImage: - fill.draw(paint.image, paint.imageMat) + fill.draw(paintOpaque.image, paintOpaque.imageMat) of pkImageTiled: - fill.drawTiled(paint.image, paint.imageMat) + fill.drawTiled(paintOpaque.image, paintOpaque.imageMat) of pkGradientLinear: - fill.fillGradientLinear(paint) + fill.fillGradientLinear(paintOpaque) of pkGradientRadial: - fill.fillGradientRadial(paint) + fill.fillGradientRadial(paintOpaque) of pkGradientAngular: - fill.fillGradientAngular(paint) + fill.fillGradientAngular(paintOpaque) + + if paint.opacity != 1: + mask.applyOpacity(paint.opacity) fill.draw(mask) image.draw(fill, blendMode = paint.blendMode) @@ -1845,6 +1856,9 @@ proc strokePath*( dashes: seq[float32] = @[] ) = ## Strokes a path. + if paint.opacity == 0: + return + if paint.kind == pkSolid: if paint.color.a > 0 or paint.blendMode == bmOverwrite: var strokeShapes = strokeShapes( @@ -1856,7 +1870,10 @@ proc strokePath*( dashes ) strokeShapes.transform(transform) - image.fillShapes(strokeShapes, paint.color, wrNonZero, paint.blendMode) + var color = paint.color + if paint.opacity != 1: + color = color.applyOpacity(paint.opacity) + image.fillShapes(strokeShapes, color, wrNonZero, paint.blendMode) return let @@ -1873,19 +1890,25 @@ proc strokePath*( dashes ) + var paintOpaque = paint + paintOpaque.opacity = 1 + case paint.kind: of pkSolid: discard # Handled above of pkImage: - fill.draw(paint.image, paint.imageMat) + fill.draw(paintOpaque.image, paintOpaque.imageMat) of pkImageTiled: - fill.drawTiled(paint.image, paint.imageMat) + fill.drawTiled(paintOpaque.image, paintOpaque.imageMat) of pkGradientLinear: - fill.fillGradientLinear(paint) + fill.fillGradientLinear(paintOpaque) of pkGradientRadial: - fill.fillGradientRadial(paint) + fill.fillGradientRadial(paintOpaque) of pkGradientAngular: - fill.fillGradientAngular(paint) + fill.fillGradientAngular(paintOpaque) + + if paint.opacity != 1: + mask.applyOpacity(paint.opacity) fill.draw(mask) image.draw(fill, blendMode = paint.blendMode) diff --git a/tests/fonts/diffs/image_paint_fill.png b/tests/fonts/diffs/image_paint_fill.png index 5561a1c..aa1cbaa 100644 Binary files a/tests/fonts/diffs/image_paint_fill.png and b/tests/fonts/diffs/image_paint_fill.png differ diff --git a/tests/fonts/rendered/image_paint_fill.png b/tests/fonts/rendered/image_paint_fill.png index 63d8515..e3fe51c 100644 Binary files a/tests/fonts/rendered/image_paint_fill.png and b/tests/fonts/rendered/image_paint_fill.png differ diff --git a/tests/images/paths/gradientAngular.png b/tests/images/paths/gradientAngular.png index 260e7d5..6b20de1 100644 Binary files a/tests/images/paths/gradientAngular.png and b/tests/images/paths/gradientAngular.png differ diff --git a/tests/images/paths/gradientAngularOpacity.png b/tests/images/paths/gradientAngularOpacity.png new file mode 100644 index 0000000..2f298aa Binary files /dev/null and b/tests/images/paths/gradientAngularOpacity.png differ diff --git a/tests/images/paths/gradientLinear.png b/tests/images/paths/gradientLinear.png index e1a15fc..944cc45 100644 Binary files a/tests/images/paths/gradientLinear.png and b/tests/images/paths/gradientLinear.png differ diff --git a/tests/images/paths/gradientRadial.png b/tests/images/paths/gradientRadial.png index cb2c7ee..80ff1f1 100644 Binary files a/tests/images/paths/gradientRadial.png and b/tests/images/paths/gradientRadial.png differ diff --git a/tests/images/paths/opacityFill.png b/tests/images/paths/opacityFill.png new file mode 100644 index 0000000..a51781e Binary files /dev/null and b/tests/images/paths/opacityFill.png differ diff --git a/tests/images/paths/opacityStroke.png b/tests/images/paths/opacityStroke.png new file mode 100644 index 0000000..aaefbfd Binary files /dev/null and b/tests/images/paths/opacityStroke.png differ diff --git a/tests/images/paths/paintImageOpacity.png b/tests/images/paths/paintImageOpacity.png new file mode 100644 index 0000000..31f19d8 Binary files /dev/null and b/tests/images/paths/paintImageOpacity.png differ diff --git a/tests/images/paths/paintImageTiledOpacity.png b/tests/images/paths/paintImageTiledOpacity.png new file mode 100644 index 0000000..16096e5 Binary files /dev/null and b/tests/images/paths/paintImageTiledOpacity.png differ diff --git a/tests/test_paints.nim b/tests/test_paints.nim index 304f9f2..fac4748 100644 --- a/tests/test_paints.nim +++ b/tests/test_paints.nim @@ -31,6 +31,21 @@ block: ) image.writeFile("tests/images/paths/paintImage.png") +block: + var paint = Paint( + kind: pkImage, + image: decodePng(readFile("tests/images/png/baboon.png")), + imageMat: scale(vec2(0.2, 0.2)) + ) + paint.opacity = 0.5 + + let image = newImage(100, 100) + image.fillPath( + heartShape, + paint + ) + image.writeFile("tests/images/paths/paintImageOpacity.png") + block: let image = newImage(100, 100) image.fillPath( @@ -43,6 +58,21 @@ block: ) image.writeFile("tests/images/paths/paintImageTiled.png") +block: + var paint = Paint( + kind: pkImageTiled, + image: decodePng(readFile("tests/images/png/baboon.png")), + imageMat: scale(vec2(0.02, 0.02)) + ) + paint.opacity = 0.5 + + let image = newImage(100, 100) + image.fillPath( + heartShape, + paint + ) + image.writeFile("tests/images/paths/paintImageTiledOpacity.png") + block: let image = newImage(100, 100) image.fillPath( @@ -100,3 +130,26 @@ block: ) image.writeFile("tests/images/paths/gradientAngular.png") + +block: + var paint = Paint( + kind: pkGradientAngular, + gradientHandlePositions: @[ + vec2(50, 50), + vec2(100, 50), + vec2(50, 100) + ], + gradientStops: @[ + ColorStop(color: rgba(255, 0, 0, 255), position: 0), + ColorStop(color: rgba(255, 0, 0, 40), position: 1.0), + ] + ) + paint.opacity = 0.5 + + let image = newImage(100, 100) + image.fillPath( + heartShape, + paint + ) + + image.writeFile("tests/images/paths/gradientAngularOpacity.png") diff --git a/tests/test_paths.nim b/tests/test_paths.nim index 915639e..fe98c3b 100644 --- a/tests/test_paths.nim +++ b/tests/test_paths.nim @@ -568,3 +568,27 @@ block: doAssert path.strokeOverlaps(vec2(40, 20)) doAssert path.strokeOverlaps(vec2(19.8, 30.2)) doAssert not path.strokeOverlaps(vec2(19.4, 30.6)) + +block: + var path: Path + path.circle(50, 50, 30) + + var paint = Paint(kind: pkSolid, color: rgba(255, 0, 255, 255)) + paint.opacity = 0.5 + + let image = newImage(100, 100) + image.fillPath(path, paint) + + image.writeFile("tests/images/paths/opacityFill.png") + +block: + var path: Path + path.circle(50, 50, 30) + + var paint = Paint(kind: pkSolid, color: rgba(255, 0, 255, 255)) + paint.opacity = 0.5 + + let image = newImage(100, 100) + image.strokePath(path, paint, strokeWidth = 10) + + image.writeFile("tests/images/paths/opacityStroke.png")