paint has opacity
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
tests/images/paths/gradientAngularOpacity.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.9 KiB |
BIN
tests/images/paths/opacityFill.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/images/paths/opacityStroke.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
tests/images/paths/paintImageOpacity.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
tests/images/paths/paintImageTiledOpacity.png
Normal file
After Width: | Height: | Size: 15 KiB |
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|