paint has opacity

This commit is contained in:
Ryan Oldenburg 2021-07-18 16:49:55 -05:00
parent 928b06c693
commit c3e168caa4
15 changed files with 174 additions and 34 deletions

View file

@ -23,6 +23,18 @@ proc gaussianKernel*(radius: int): seq[uint32] =
for i, f in floats: for i, f in floats:
result[i] = round(f * 255 * 1024).uint32 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]) = proc toStraightAlpha*(data: var seq[ColorRGBA | ColorRGBX]) =
## Converts an image from premultiplied alpha to straight alpha. ## Converts an image from premultiplied alpha to straight alpha.
## This is expensive for large images. ## This is expensive for large images.

View file

@ -1,4 +1,4 @@
import blends, chroma, common, images, vmath import blends, chroma, common, images, internal, options, vmath
type type
PaintKind* = enum PaintKind* = enum
@ -21,6 +21,7 @@ type
gradientHandlePositions*: seq[Vec2] ## Gradient positions (image space). gradientHandlePositions*: seq[Vec2] ## Gradient positions (image space).
gradientStops*: seq[ColorStop] ## Color stops (gradient space). gradientStops*: seq[ColorStop] ## Color stops (gradient space).
blendMode*: BlendMode ## Blend mode. blendMode*: BlendMode ## Blend mode.
opacityOption: Option[float32]
ColorStop* = object ColorStop* = object
## Color stop on a gradient curve. ## Color stop on a gradient curve.
@ -41,37 +42,55 @@ converter parseSomePaint*(paint: SomePaint): Paint {.inline.} =
elif type(paint) is Paint: elif type(paint) is Paint:
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.} = proc toLineSpace(at, to, point: Vec2): float32 {.inline.} =
## Convert position on to where it would fall on a line between at and to. ## Convert position on to where it would fall on a line between at and to.
let let
d = to - at d = to - at
det = d.x * d.x + d.y * d.y 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]) = proc gradientPut(
## Put an gradient color based on the "a" - were are we related to a line. 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 var index = -1
for i, stop in stops: for i, stop in stops:
if stop.position < a: if stop.position < t:
index = i index = i
if stop.position > a: if stop.position > t:
break break
var color: Color var color: ColorRGBX
if index == -1: if index == -1:
# first stop solid # first stop solid
color = stops[0].color.color color = stops[0].color
elif index + 1 >= stops.len: elif index + 1 >= stops.len:
# last stop solid # last stop solid
color = stops[index].color.color color = stops[index].color
else: else:
let let
gs1 = stops[index] gs1 = stops[index]
gs2 = stops[index+1] gs2 = stops[index + 1]
color = mix( color = lerp(
gs1.color.color, gs1.color,
gs2.color.color, gs2.color,
(a - gs1.position) / (gs2.position - gs1.position) (t - gs1.position) / (gs2.position - gs1.position)
) )
if paint.opacity != 0:
color = color.applyOpacity(paint.opacity)
image.setRgbaUnsafe(x, y, color.rgba.rgbx()) image.setRgbaUnsafe(x, y, color.rgba.rgbx())
proc fillGradientLinear*(image: Image, paint: Paint) = proc fillGradientLinear*(image: Image, paint: Paint) =
@ -86,6 +105,9 @@ proc fillGradientLinear*(image: Image, paint: Paint) =
if paint.gradientStops.len == 0: if paint.gradientStops.len == 0:
raise newException(PixieError, "Gradient must have at least 1 color stop") raise newException(PixieError, "Gradient must have at least 1 color stop")
if paint.opacity == 0:
return
let let
at = paint.gradientHandlePositions[0] at = paint.gradientHandlePositions[0]
to = paint.gradientHandlePositions[1] to = paint.gradientHandlePositions[1]
@ -93,8 +115,8 @@ proc fillGradientLinear*(image: Image, paint: Paint) =
for x in 0 ..< image.width: for x in 0 ..< image.width:
let let
xy = vec2(x.float32, y.float32) xy = vec2(x.float32, y.float32)
a = toLineSpace(at, to, xy) t = toLineSpace(at, to, xy)
image.gradientPut(x, y, a, paint.gradientStops) image.gradientPut(paint, x, y, t, paint.gradientStops)
proc fillGradientRadial*(image: Image, paint: Paint) = proc fillGradientRadial*(image: Image, paint: Paint) =
## Fills a radial gradient. ## Fills a radial gradient.
@ -108,6 +130,9 @@ proc fillGradientRadial*(image: Image, paint: Paint) =
if paint.gradientStops.len == 0: if paint.gradientStops.len == 0:
raise newException(PixieError, "Gradient must have at least 1 color stop") raise newException(PixieError, "Gradient must have at least 1 color stop")
if paint.opacity == 0:
return
let let
center = paint.gradientHandlePositions[0] center = paint.gradientHandlePositions[0]
edge = paint.gradientHandlePositions[1] edge = paint.gradientHandlePositions[1]
@ -124,8 +149,8 @@ proc fillGradientRadial*(image: Image, paint: Paint) =
for x in 0 ..< image.width: for x in 0 ..< image.width:
let let
xy = vec2(x.float32, y.float32) xy = vec2(x.float32, y.float32)
b = (mat * xy).length() t = (mat * xy).length()
image.gradientPut(x, y, b, paint.gradientStops) image.gradientPut(paint, x, y, t, paint.gradientStops)
proc fillGradientAngular*(image: Image, paint: Paint) = proc fillGradientAngular*(image: Image, paint: Paint) =
## Fills an angular gradient. ## Fills an angular gradient.
@ -139,6 +164,9 @@ proc fillGradientAngular*(image: Image, paint: Paint) =
if paint.gradientStops.len == 0: if paint.gradientStops.len == 0:
raise newException(PixieError, "Gradient must have at least 1 color stop") raise newException(PixieError, "Gradient must have at least 1 color stop")
if paint.opacity == 0:
return
let let
center = paint.gradientHandlePositions[0] center = paint.gradientHandlePositions[0]
edge = paint.gradientHandlePositions[1] edge = paint.gradientHandlePositions[1]
@ -149,5 +177,5 @@ proc fillGradientAngular*(image: Image, paint: Paint) =
let let
xy = vec2(x.float32, y.float32) xy = vec2(x.float32, y.float32)
angle = normalize(xy - center).angle() angle = normalize(xy - center).angle()
a = (angle + gradientAngle + PI/2).fixAngle() / 2 / PI + 0.5 t = (angle + gradientAngle + PI / 2).fixAngle() / 2 / PI + 0.5
image.gradientPut(x, y, a, paint.gradientStops) image.gradientPut(paint, x, y, t, paint.gradientStops)

View file

@ -1,5 +1,4 @@
import blends, bumpy, chroma, common, images, masks, paints, pixie/internal, import blends, bumpy, chroma, common, images, masks, paints, internal, strutils, vmath
strutils, vmath
when defined(amd64) and not defined(pixieNoSimd): when defined(amd64) and not defined(pixieNoSimd):
import nimsimd/sse2 import nimsimd/sse2
@ -1780,11 +1779,17 @@ proc fillPath*(
windingRule = wrNonZero windingRule = wrNonZero
) = ) =
## Fills a path. ## Fills a path.
if paint.opacity == 0:
return
if paint.kind == pkSolid: if paint.kind == pkSolid:
if paint.color.a > 0 or paint.blendMode == bmOverwrite: if paint.color.a > 0 or paint.blendMode == bmOverwrite:
var shapes = parseSomePath(path, true, transform.pixelScale()) var shapes = parseSomePath(path, true, transform.pixelScale())
shapes.transform(transform) 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 return
let let
@ -1793,19 +1798,25 @@ proc fillPath*(
mask.fillPath(path, transform, windingRule) mask.fillPath(path, transform, windingRule)
var paintOpaque = paint
paintOpaque.opacity = 1
case paint.kind: case paint.kind:
of pkSolid: of pkSolid:
discard # Handled above discard # Handled above
of pkImage: of pkImage:
fill.draw(paint.image, paint.imageMat) fill.draw(paintOpaque.image, paintOpaque.imageMat)
of pkImageTiled: of pkImageTiled:
fill.drawTiled(paint.image, paint.imageMat) fill.drawTiled(paintOpaque.image, paintOpaque.imageMat)
of pkGradientLinear: of pkGradientLinear:
fill.fillGradientLinear(paint) fill.fillGradientLinear(paintOpaque)
of pkGradientRadial: of pkGradientRadial:
fill.fillGradientRadial(paint) fill.fillGradientRadial(paintOpaque)
of pkGradientAngular: of pkGradientAngular:
fill.fillGradientAngular(paint) fill.fillGradientAngular(paintOpaque)
if paint.opacity != 1:
mask.applyOpacity(paint.opacity)
fill.draw(mask) fill.draw(mask)
image.draw(fill, blendMode = paint.blendMode) image.draw(fill, blendMode = paint.blendMode)
@ -1845,6 +1856,9 @@ proc strokePath*(
dashes: seq[float32] = @[] dashes: seq[float32] = @[]
) = ) =
## Strokes a path. ## Strokes a path.
if paint.opacity == 0:
return
if paint.kind == pkSolid: if paint.kind == pkSolid:
if paint.color.a > 0 or paint.blendMode == bmOverwrite: if paint.color.a > 0 or paint.blendMode == bmOverwrite:
var strokeShapes = strokeShapes( var strokeShapes = strokeShapes(
@ -1856,7 +1870,10 @@ proc strokePath*(
dashes dashes
) )
strokeShapes.transform(transform) 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 return
let let
@ -1873,19 +1890,25 @@ proc strokePath*(
dashes dashes
) )
var paintOpaque = paint
paintOpaque.opacity = 1
case paint.kind: case paint.kind:
of pkSolid: of pkSolid:
discard # Handled above discard # Handled above
of pkImage: of pkImage:
fill.draw(paint.image, paint.imageMat) fill.draw(paintOpaque.image, paintOpaque.imageMat)
of pkImageTiled: of pkImageTiled:
fill.drawTiled(paint.image, paint.imageMat) fill.drawTiled(paintOpaque.image, paintOpaque.imageMat)
of pkGradientLinear: of pkGradientLinear:
fill.fillGradientLinear(paint) fill.fillGradientLinear(paintOpaque)
of pkGradientRadial: of pkGradientRadial:
fill.fillGradientRadial(paint) fill.fillGradientRadial(paintOpaque)
of pkGradientAngular: of pkGradientAngular:
fill.fillGradientAngular(paint) fill.fillGradientAngular(paintOpaque)
if paint.opacity != 1:
mask.applyOpacity(paint.opacity)
fill.draw(mask) fill.draw(mask)
image.draw(fill, blendMode = paint.blendMode) image.draw(fill, blendMode = paint.blendMode)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -31,6 +31,21 @@ block:
) )
image.writeFile("tests/images/paths/paintImage.png") 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: block:
let image = newImage(100, 100) let image = newImage(100, 100)
image.fillPath( image.fillPath(
@ -43,6 +58,21 @@ block:
) )
image.writeFile("tests/images/paths/paintImageTiled.png") 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: block:
let image = newImage(100, 100) let image = newImage(100, 100)
image.fillPath( image.fillPath(
@ -100,3 +130,26 @@ block:
) )
image.writeFile("tests/images/paths/gradientAngular.png") 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")

View file

@ -568,3 +568,27 @@ block:
doAssert path.strokeOverlaps(vec2(40, 20)) doAssert path.strokeOverlaps(vec2(40, 20))
doAssert path.strokeOverlaps(vec2(19.8, 30.2)) doAssert path.strokeOverlaps(vec2(19.8, 30.2))
doAssert not path.strokeOverlaps(vec2(19.4, 30.6)) 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")