blends, blurs, masks checkpoint

This commit is contained in:
Ryan Oldenburg 2021-02-09 15:22:22 -06:00
parent 913c7e9498
commit 29047d6a46
7 changed files with 193 additions and 152 deletions

View file

@ -32,19 +32,15 @@ type
bmExcludeMask bmExcludeMask
Blender* = proc(backdrop, source: ColorRGBA): ColorRGBA Blender* = proc(backdrop, source: ColorRGBA): ColorRGBA
Masker* = proc(backdrop, source: uint8): uint8
when defined(amd64) and not defined(pixieNoSimd):
import nimsimd/sse2
type BlenderSimd* = proc(blackdrop, source: M128i): M128i
when defined(release): when defined(release):
{.push checks: off.} {.push checks: off.}
proc blendAlpha(backdrop, source: uint8): uint8 {.inline.} = proc blendAlpha*(backdrop, source: uint8): uint8 {.inline.} =
source + ((backdrop.uint32 * (255 - source)) div 255).uint8 source + ((backdrop.uint32 * (255 - source)) div 255).uint8
proc blendNormalPremultiplied*(backdrop, source: ColorRGBA): ColorRGBA = proc blendNormal(backdrop, source: ColorRGBA): ColorRGBA =
if backdrop.a == 0: if backdrop.a == 0:
return source return source
if source.a == 255: if source.a == 255:
@ -65,18 +61,36 @@ proc blendMask(backdrop, source: ColorRGBA): ColorRGBA =
result.b = ((backdrop.b * k) div 255).uint8 result.b = ((backdrop.b * k) div 255).uint8
result.a = ((backdrop.a * k) div 255).uint8 result.a = ((backdrop.a * k) div 255).uint8
proc blendOverwrite*(backdrop, source: ColorRGBA): ColorRGBA = proc blendOverwrite(backdrop, source: ColorRGBA): ColorRGBA =
source source
proc blenderPremultiplied*(blendMode: BlendMode): Blender = proc blender*(blendMode: BlendMode): Blender =
case blendMode: case blendMode:
of bmNormal: blendNormalPremultiplied of bmNormal: blendNormal
of bmOverwrite: blendOverwrite
of bmMask: blendMask of bmMask: blendMask
of bmOverwrite: blendOverwrite
else: else:
raise newException(PixieError, "No premultiplied blender for " & $blendMode) blendNormal
# raise newException(PixieError, "No blender for " & $blendMode)
proc maskMask(backdrop, source: uint8): uint8 =
((backdrop.uint32 * source) div 255).uint8
proc maskOverwrite(backdrop, source: uint8): uint8 =
source
proc masker*(blendMode: BlendMode): Masker =
case blendMode:
of bmMask: maskMask
of bmOverwrite: maskOverwrite
else:
raise newException(PixieError, "No masker for " & $blendMode)
when defined(amd64) and not defined(pixieNoSimd): when defined(amd64) and not defined(pixieNoSimd):
import nimsimd/sse2
type BlenderSimd* = proc(blackdrop, source: M128i): M128i
proc blendNormalPremultipliedSimd*(backdrop, source: M128i): M128i = proc blendNormalPremultipliedSimd*(backdrop, source: M128i): M128i =
let let
alphaMask = mm_set1_epi32(cast[int32](0xff000000)) alphaMask = mm_set1_epi32(cast[int32](0xff000000))
@ -116,7 +130,6 @@ when defined(amd64) and not defined(pixieNoSimd):
else: else:
raise newException(PixieError, "No SIMD blender for " & $blendMode) raise newException(PixieError, "No SIMD blender for " & $blendMode)
when defined(release): when defined(release):
{.pop.} {.pop.}
@ -415,8 +428,8 @@ proc hardLight(backdrop, source: uint32): uint8 {.inline.} =
else: else:
screen(backdrop, 2 * source - 255) screen(backdrop, 2 * source - 255)
proc blendNormal(backdrop, source: ColorRGBA): ColorRGBA = proc blendNormalOld(backdrop, source: ColorRGBA): ColorRGBA =
blendNormalPremultiplied( blendNormal(
backdrop.toPremultipliedAlpha(), backdrop.toPremultipliedAlpha(),
source.toPremultipliedAlpha() source.toPremultipliedAlpha()
).toStraightAlpha() ).toStraightAlpha()
@ -558,28 +571,28 @@ proc blendExcludeMask(backdrop, source: ColorRGBA): ColorRGBA =
result = backdrop result = backdrop
result.a = max(backdrop.a, source.a) - min(backdrop.a, source.a) result.a = max(backdrop.a, source.a) - min(backdrop.a, source.a)
proc blender*(blendMode: BlendMode): Blender = # proc blender*(blendMode: BlendMode): Blender =
case blendMode: # case blendMode:
of bmNormal: blendNormal # of bmNormal: blendNormal
of bmDarken: blendDarken # of bmDarken: blendDarken
of bmMultiply: blendMultiply # of bmMultiply: blendMultiply
of bmLinearBurn: blendLinearBurn # of bmLinearBurn: blendLinearBurn
of bmColorBurn: blendColorBurn # of bmColorBurn: blendColorBurn
of bmLighten: blendLighten # of bmLighten: blendLighten
of bmScreen: blendScreen # of bmScreen: blendScreen
of bmLinearDodge: blendLinearDodge # of bmLinearDodge: blendLinearDodge
of bmColorDodge: blendColorDodge # of bmColorDodge: blendColorDodge
of bmOverlay: blendOverlay # of bmOverlay: blendOverlay
of bmSoftLight: blendSoftLight # of bmSoftLight: blendSoftLight
of bmHardLight: blendHardLight # of bmHardLight: blendHardLight
of bmDifference: blendDifference # of bmDifference: blendDifference
of bmExclusion: blendExclusion # of bmExclusion: blendExclusion
of bmHue: blendHue # of bmHue: blendHue
of bmSaturation: blendSaturation # of bmSaturation: blendSaturation
of bmColor: blendColor # of bmColor: blendColor
of bmLuminosity: blendLuminosity # of bmLuminosity: blendLuminosity
of bmMask: blendMask # of bmMask: blendMask
of bmOverwrite: blendOverwrite # of bmOverwrite: blendOverwrite
of bmSubtractMask: blendSubtractMask # of bmSubtractMask: blendSubtractMask
of bmIntersectMask: blendIntersectMask # of bmIntersectMask: blendIntersectMask
of bmExcludeMask: blendExcludeMask # of bmExcludeMask: blendExcludeMask

View file

@ -369,6 +369,13 @@ proc invert*(target: Image | Mask) =
for j in i ..< target.data.len: for j in i ..< target.data.len:
target.data[j] = (255 - target.data[j]).uint8 target.data[j] = (255 - target.data[j]).uint8
proc newMask*(image: Image): Mask =
## Returns a new mask using the alpha values of the parameter image.
result = newMask(image.width, image.height)
for i, rgba in image.data:
result.data[i] = rgba.a
proc getRgbaSmooth*(image: Image, x, y: float32): ColorRGBA = proc getRgbaSmooth*(image: Image, x, y: float32): ColorRGBA =
let let
minX = floor(x) minX = floor(x)
@ -393,21 +400,10 @@ proc drawCorrect(
) = ) =
## Draws one image onto another using matrix with color blending. ## Draws one image onto another using matrix with color blending.
proc validateMaskBlendMode() =
if blendMode notin {bmMask}:
raise newException(
PixieError,
"Blend mode " & $blendMode & " not supported for masks"
)
when type(a) is Image: when type(a) is Image:
when type(b) is Image: let blender = blendMode.blender()
let blender = blendMode.blenderPremultiplied()
else: # b is a Mask
validateMaskBlendMode()
else: # a is a Mask else: # a is a Mask
when type(b) is Mask: let masker = blendMode.masker()
validateMaskBlendMode()
var var
matInv = mat.inverse() matInv = mat.inverse()
@ -435,27 +431,23 @@ proc drawCorrect(
yFloat = samplePos.y - h yFloat = samplePos.y - h
when type(a) is Image: when type(a) is Image:
let rgba = a.getRgbaUnsafe(x, y) let backdrop = a.getRgbaUnsafe(x, y)
var blended: ColorRGBA
when type(b) is Image: when type(b) is Image:
let sample = b.getRgbaSmooth(xFloat, yFloat) let
blended = blender(rgba, sample) sample = b.getRgbaSmooth(xFloat, yFloat)
blended = blender(backdrop, sample)
else: # b is a Mask else: # b is a Mask
let sample = b.getValueSmooth(xFloat, yFloat).uint32 let
blended = rgba( sample = b.getValueSmooth(xFloat, yFloat)
((rgba.r * sample) div 255).uint8, blended = blender(backdrop, rgba(0, 0, 0, sample))
((rgba.g * sample) div 255).uint8,
((rgba.b * sample) div 255).uint8,
((rgba.a * sample) div 255).uint8
)
a.setRgbaUnsafe(x, y, blended) a.setRgbaUnsafe(x, y, blended)
else: # a is a Mask, b must be a mask else: # a is a Mask
let value = a.getValueUnsafe(x, y) let backdrop = a.getValueUnsafe(x, y)
when type(b) is Image: when type(b) is Image:
let sample = b.getRgbaSmooth(xFloat, yFloat).a.uint32 let sample = b.getRgbaSmooth(xFloat, yFloat).a
else: # a is a Mask else: # b is a Mask
let sample = b.getValueSmooth(xFloat, yFloat).uint32 let sample = b.getValueSmooth(xFloat, yFloat)
a.setValueUnsafe(x, y, ((value * sample) div 255).uint8) a.setValueUnsafe(x, y, masker(backdrop, sample))
proc draw*(image: Image, mask: Mask, mat: Mat3, blendMode = bmMask) = proc draw*(image: Image, mask: Mask, mat: Mat3, blendMode = bmMask) =
image.drawCorrect(mask, mat, blendMode) image.drawCorrect(mask, mat, blendMode)
@ -496,7 +488,7 @@ proc gaussianLookup(radius: int): seq[float32] =
when defined(release): when defined(release):
{.pop.} {.pop.}
proc blur*(image: Image, radius: float32) = proc blur*(target: Image | Mask, radius: float32) =
## Applies Gaussian blur to the image given a radius. ## Applies Gaussian blur to the image given a radius.
let radius = round(radius).int let radius = round(radius).int
if radius == 0: if radius == 0:
@ -504,73 +496,68 @@ proc blur*(image: Image, radius: float32) =
let lookup = gaussianLookup(radius) let lookup = gaussianLookup(radius)
# Blur in the X direction. when type(target) is Image:
var blurX = newImage(image.width, image.height) # Blur in the X direction.
for y in 0 ..< image.height: var blurX = newImage(target.width, target.height)
for x in 0 ..< image.width: for y in 0 ..< target.height:
var c: Color for x in 0 ..< target.width:
var totalA = 0.0 var c: Color
for xb in -radius .. radius: var totalA = 0.0
let c2 = image[x + xb, y].color for xb in -radius .. radius:
let a = lookup[xb + radius] let c2 = target[x + xb, y].color
let aa = c2.a * a let a = lookup[xb + radius]
totalA += aa let aa = c2.a * a
c.r += c2.r * aa totalA += aa
c.g += c2.g * aa c.r += c2.r * aa
c.b += c2.b * aa c.g += c2.g * aa
c.a += c2.a * a c.b += c2.b * aa
c.r = c.r / totalA c.a += c2.a * a
c.g = c.g / totalA c.r = c.r / totalA
c.b = c.b / totalA c.g = c.g / totalA
blurX.setRgbaUnsafe(x, y, c.rgba) c.b = c.b / totalA
blurX.setRgbaUnsafe(x, y, c.rgba)
# Blur in the Y direction. # Blur in the Y direction.
for y in 0 ..< image.height: for y in 0 ..< target.height:
for x in 0 ..< image.width: for x in 0 ..< target.width:
var c: Color var c: Color
var totalA = 0.0 var totalA = 0.0
for yb in -radius .. radius: for yb in -radius .. radius:
let c2 = blurX[x, y + yb].color let c2 = blurX[x, y + yb].color
let a = lookup[yb + radius] let a = lookup[yb + radius]
let aa = c2.a * a let aa = c2.a * a
totalA += aa totalA += aa
c.r += c2.r * aa c.r += c2.r * aa
c.g += c2.g * aa c.g += c2.g * aa
c.b += c2.b * aa c.b += c2.b * aa
c.a += c2.a * a c.a += c2.a * a
c.r = c.r / totalA c.r = c.r / totalA
c.g = c.g / totalA c.g = c.g / totalA
c.b = c.b / totalA c.b = c.b / totalA
image.setRgbaUnsafe(x, y, c.rgba) target.setRgbaUnsafe(x, y, c.rgba)
proc blurAlpha*(image: Image, radius: float32) = else: # target is a Mask
## Applies Gaussian blur to the image given a radius.
let radius = round(radius).int
if radius == 0:
return
let lookup = gaussianLookup(radius) # Blur in the X direction.
var blurX = newMask(target.width, target.height)
for y in 0 ..< target.height:
for x in 0 ..< target.width:
var alpha: float32
for xb in -radius .. radius:
let c2 = target[x + xb, y]
let a = lookup[xb + radius]
alpha += c2.float32 * a
blurX.setValueUnsafe(x, y, alpha.uint8)
# Blur in the X direction. # Blur in the Y direction and modify image.
var blurX = newImage(image.width, image.height) for y in 0 ..< target.height:
for y in 0 ..< image.height: for x in 0 ..< target.width:
for x in 0 ..< image.width: var alpha: float32
var alpha: float32 for yb in -radius .. radius:
for xb in -radius .. radius: let c2 = blurX[x, y + yb]
let c2 = image[x + xb, y] let a = lookup[yb + radius]
let a = lookup[xb + radius] alpha += c2.float32 * a
alpha += c2.a.float32 * a target.setValueUnsafe(x, y, alpha.uint8)
blurX.setRgbaUnsafe(x, y, rgba(0, 0, 0, alpha.uint8))
# Blur in the Y direction and modify image.
for y in 0 ..< image.height:
for x in 0 ..< image.width:
var alpha: float32
for yb in -radius .. radius:
let c2 = blurX[x, y + yb]
let a = lookup[yb + radius]
alpha += c2.a.float32 * a
image.setRgbaUnsafe(x, y, rgba(0, 0, 0, alpha.uint8))
proc sharpOpacity*(image: Image) = proc sharpOpacity*(image: Image) =
## Sharpens the opacity to extreme. ## Sharpens the opacity to extreme.
@ -688,15 +675,19 @@ proc resize*(srcImage: Image, width, height: int): Image =
bmOverwrite bmOverwrite
) )
proc shift*(image: Image, offset: Vec2) = proc shift*(target: Image | Mask, offset: Vec2) =
## Shifts the image by offset. ## Shifts the target by offset.
if offset != vec2(0, 0): if offset != vec2(0, 0):
let copy = image.copy() # Copy to read from. let copy = target.copy() # Copy to read from
image.fill(rgba(0, 0, 0, 0)) # Reset this for being drawn to. # Reset target for being drawn to
image.draw(copy, offset, bmOverwrite) # Draw copy into image. when type(target) is Image:
target.fill(rgba(0, 0, 0, 0))
else:
target.fill(0)
target.draw(copy, offset, bmOverwrite) # Draw copy at offset
proc spread*(image: Image, spread: float32) = proc spread*(image: Image, spread: float32) =
## Grows the image as a mask by spread. ## Grows the target as a mask by spread.
if spread == 0: if spread == 0:
return return
if spread < 0: if spread < 0:
@ -719,18 +710,16 @@ proc spread*(image: Image, spread: float32) =
image.setRgbaUnsafe(x, y, rgba(0, 0, 0, maxAlpha)) image.setRgbaUnsafe(x, y, rgba(0, 0, 0, maxAlpha))
proc shadow*( proc shadow*(
mask: Image, offset: Vec2, spread, blur: float32, color: ColorRGBA image: Image, offset: Vec2, spread, blur: float32, color: ColorRGBA
): Image = ): Image =
## Create a shadow of the image with the offset, spread and blur. ## Create a shadow of the image with the offset, spread and blur.
# TODO: copying is bad here due to this being slow already, let mask = image.newMask()
# we're doing it tho to avoid mutating param and returning new Image.
let copy = mask.copy()
if offset != vec2(0, 0): if offset != vec2(0, 0):
copy.shift(offset) mask.shift(offset)
if spread > 0: if spread > 0:
copy.spread(spread) mask.spread(spread)
if blur > 0: if blur > 0:
copy.blurAlpha(blur) mask.blur(blur)
result = newImage(mask.width, mask.height) result = newImage(mask.width, mask.height)
result.fill(color) result.fill(color)
result.draw(copy, blendMode = bmMask) result.draw(mask, blendMode = bmMask)

View file

@ -109,5 +109,28 @@ proc getValueSmooth*(mask: Mask, x, y: float32): uint8 =
lerp(bottomMix, topMix, diffY) lerp(bottomMix, topMix, diffY)
proc spread*(mask: Mask, spread: float32) =
## Grows the mask by spread.
if spread == 0:
return
if spread < 0:
raise newException(PixieError, "Cannot apply negative spread")
let
copy = mask.copy()
spread = round(spread).int
for y in 0 ..< mask.height:
for x in 0 ..< mask.width:
var maxValue: uint8
block blurBox:
for bx in -spread .. spread:
for by in -spread .. spread:
let value = copy[x + bx, y + by]
if value > maxValue:
maxValue = value
if maxValue == 255:
break blurBox
mask.setValueUnsafe(x, y, maxValue)
when defined(release): when defined(release):
{.pop.} {.pop.}

View file

@ -931,7 +931,7 @@ proc fillShapes(
startX = max(0, bounds.x.int) startX = max(0, bounds.x.int)
startY = max(0, bounds.y.int) startY = max(0, bounds.y.int)
stopY = min(image.height, (bounds.y + bounds.h).int) stopY = min(image.height, (bounds.y + bounds.h).int)
blender = blendMode.blenderPremultiplied() blender = blendMode.blender()
when defined(amd64) and not defined(pixieNoSimd): when defined(amd64) and not defined(pixieNoSimd):
let blenderSimd = blendMode.blenderSimd() let blenderSimd = blendMode.blenderSimd()
@ -1136,8 +1136,7 @@ proc fillShapes(
if coverage != 0: if coverage != 0:
let let
backdrop = mask.getValueUnsafe(x, y) backdrop = mask.getValueUnsafe(x, y)
blended = blended = blendAlpha(backdrop, coverage)
coverage + ((backdrop.uint32 * (255 - coverage)) div 255).uint8
mask.setValueUnsafe(x, y, blended) mask.setValueUnsafe(x, y, blended)
inc x inc x

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

View file

@ -1,4 +1,4 @@
import chroma, pixie, pixie/fileformats/png import chroma, pixie, pixie/fileformats/png, vmath
block: block:
let mask = newMask(100, 100) let mask = newMask(100, 100)
@ -74,3 +74,20 @@ block:
a.draw(b) a.draw(b)
writeFile("tests/images/masks/imageMaskedMask.png", a.encodePng()) writeFile("tests/images/masks/imageMaskedMask.png", a.encodePng())
block:
let a = newMask(100, 100)
a.fill(255)
a.shift(vec2(10, 10))
writeFile("tests/images/masks/shifted.png", a.encodePng())
block:
var path: Path
path.rect(40, 40, 20, 20)
let a = newMask(100, 100)
a.fillPath(path)
a.spread(10)
writeFile("tests/images/masks/spread.png", a.encodePng())