Merge pull request #99 from guzba/master

switch to premultiplied alpha, faster blurs
This commit is contained in:
treeform 2021-02-09 21:53:34 -08:00 committed by GitHub
commit 955a38ae6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 474 additions and 371 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:
@ -58,14 +54,107 @@ proc blendNormalPremultiplied*(backdrop, source: ColorRGBA): ColorRGBA =
result.b = source.b + ((backdrop.b.uint32 * k) div 255).uint8 result.b = source.b + ((backdrop.b.uint32 * k) div 255).uint8
result.a = blendAlpha(backdrop.a, source.a) result.a = blendAlpha(backdrop.a, source.a)
proc blenderPremultiplied*(blendMode: BlendMode): Blender = proc blendExclusion(backdrop, source: ColorRGBA): ColorRGBA =
proc blend(backdrop, source: uint32): uint8 {.inline.} =
let v = (backdrop + source).int32 - ((2 * backdrop * source) div 255).int32
(cast[uint32](v) and uint8.high.uint32).uint8
result.r = blend(backdrop.r.uint32, source.r.uint32)
result.g = blend(backdrop.g.uint32, source.g.uint32)
result.b = blend(backdrop.b.uint32, source.b.uint32)
result.a = blendAlpha(backdrop.a, source.a)
proc blendMask(backdrop, source: ColorRGBA): ColorRGBA =
let k = source.a.uint32
result.r = ((backdrop.r * k) div 255).uint8
result.g = ((backdrop.g * k) div 255).uint8
result.b = ((backdrop.b * k) div 255).uint8
result.a = ((backdrop.a * k) div 255).uint8
proc blendSubtractMask(backdrop, source: ColorRGBA): ColorRGBA =
let a = (backdrop.a.uint32 * (255 - source.a)) div 255
result.r = ((backdrop.r * a) div 255).uint8
result.g = ((backdrop.g * a) div 255).uint8
result.b = ((backdrop.b * a) div 255).uint8
result.a = a.uint8
proc blendIntersectMask(backdrop, source: ColorRGBA): ColorRGBA =
blendMask(backdrop, source)
proc blendExcludeMask(backdrop, source: ColorRGBA): ColorRGBA =
let a = max(backdrop.a, source.a).uint32 - min(backdrop.a, source.a)
result.r = ((backdrop.r * a) div 255).uint8
result.g = ((backdrop.g * a) div 255).uint8
result.b = ((backdrop.b * a) div 255).uint8
result.a = a.uint8
proc blendOverwrite(backdrop, source: ColorRGBA): ColorRGBA =
source
proc blendWhite(backdrop, source: ColorRGBA): ColorRGBA =
## For testing
rgba(255, 255, 255, 255)
proc blender*(blendMode: BlendMode): Blender =
case blendMode: case blendMode:
of bmNormal: blendNormalPremultiplied of bmNormal: blendNormal
# of bmDarken: blendDarken
# of bmMultiply: blendMultiply
# of bmLinearBurn: blendLinearBurn
# of bmColorBurn: blendColorBurn
# of bmLighten: blendLighten
# of bmScreen: blendScreen
# of bmLinearDodge: blendLinearDodge
# of bmColorDodge: blendColorDodge
# of bmOverlay: blendOverlay
# of bmSoftLight: blendSoftLight
# of bmHardLight: blendHardLight
# of bmDifference: blendDifference
of bmExclusion: blendExclusion
# of bmHue: blendHue
# of bmSaturation: blendSaturation
# of bmColor: blendColor
# of bmLuminosity: blendLuminosity
of bmMask: blendMask
of bmOverwrite: blendOverwrite
of bmSubtractMask: blendSubtractMask
of bmIntersectMask: blendIntersectMask
of bmExcludeMask: blendExcludeMask
else: else:
raise newException(PixieError, "No premultiplied blender for " & $blendMode) # blendWhite
blendNormal
# raise newException(PixieError, "No blender for " & $blendMode)
proc maskMask(backdrop, source: uint8): uint8 =
((backdrop.uint32 * source) div 255).uint8
proc maskSubtract(backdrop, source: uint8): uint8 =
((backdrop.uint32 * (255 - source)) div 255).uint8
proc maskIntersect(backdrop, source: uint8): uint8 =
maskMask(backdrop, source)
proc maskExclude(backdrop, source: uint8): uint8 =
max(backdrop, source) - min(backdrop, source)
proc maskOverwrite(backdrop, source: uint8): uint8 =
source
proc masker*(blendMode: BlendMode): Masker =
case blendMode:
of bmMask: maskMask
of bmOverwrite: maskOverwrite
of bmSubtractMask: maskSubtract
of bmIntersectMask: maskIntersect
of bmExcludeMask: maskExclude
else:
raise newException(PixieError, "No masker for " & $blendMode)
when defined(amd64) and not defined(pixieNoSimd): when defined(amd64) and not defined(pixieNoSimd):
proc blendNormalPremultipliedSimd*(backdrop, source: M128i): M128i = import nimsimd/sse2
type BlenderSimd* = proc(blackdrop, source: M128i): M128i
proc blendNormalSimd*(backdrop, source: M128i): M128i =
let let
alphaMask = mm_set1_epi32(cast[int32](0xff000000)) alphaMask = mm_set1_epi32(cast[int32](0xff000000))
oddMask = mm_set1_epi16(cast[int16](0xff00)) oddMask = mm_set1_epi16(cast[int16](0xff00))
@ -94,13 +183,16 @@ when defined(amd64) and not defined(pixieNoSimd):
mm_or_si128(backdropEven, mm_slli_epi16(backdropOdd, 8)) mm_or_si128(backdropEven, mm_slli_epi16(backdropOdd, 8))
) )
proc blendOverwriteSimd*(backdrop, source: M128i): M128i =
source
proc blenderSimd*(blendMode: BlendMode): BlenderSimd = proc blenderSimd*(blendMode: BlendMode): BlenderSimd =
case blendMode: case blendMode:
of bmNormal: blendNormalPremultipliedSimd of bmNormal: blendNormalSimd
of bmOverwrite: blendOverwriteSimd
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.}
@ -399,12 +491,6 @@ 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 =
blendNormalPremultiplied(
backdrop.toPremultipliedAlpha(),
source.toPremultipliedAlpha()
).toStraightAlpha()
proc blendDarken(backdrop, source: ColorRGBA): ColorRGBA = proc blendDarken(backdrop, source: ColorRGBA): ColorRGBA =
result.r = min(backdrop.r, source.r) result.r = min(backdrop.r, source.r)
result.g = min(backdrop.g, source.g) result.g = min(backdrop.g, source.g)
@ -510,14 +596,6 @@ proc blendDifference(backdrop, source: ColorRGBA): ColorRGBA =
result.b = max(backdrop.b, source.b) - min(backdrop.b, source.b) result.b = max(backdrop.b, source.b) - min(backdrop.b, source.b)
result = alphaFix(backdrop, source, result) result = alphaFix(backdrop, source, result)
proc blendExclusion(backdrop, source: ColorRGBA): ColorRGBA =
proc blend(backdrop, source: int32): uint8 {.inline.} =
max(0, backdrop + source - (2 * backdrop * source) div 255).uint8
result.r = blend(backdrop.r.int32, source.r.int32)
result.g = blend(backdrop.g.int32, source.g.int32)
result.b = blend(backdrop.b.int32, source.b.int32)
result = alphaFix(backdrop, source, result)
proc blendColor(backdrop, source: ColorRGBA): ColorRGBA = proc blendColor(backdrop, source: ColorRGBA): ColorRGBA =
blendColorFloats(backdrop.color, source.color).rgba blendColorFloats(backdrop.color, source.color).rgba
@ -529,48 +607,3 @@ proc blendHue(backdrop, source: ColorRGBA): ColorRGBA =
proc blendSaturation(backdrop, source: ColorRGBA): ColorRGBA = proc blendSaturation(backdrop, source: ColorRGBA): ColorRGBA =
blendSaturationFloats(backdrop.color, source.color).rgba blendSaturationFloats(backdrop.color, source.color).rgba
proc blendMask(backdrop, source: ColorRGBA): ColorRGBA =
result = backdrop
result.a = min(backdrop.a, source.a)
proc blendSubtractMask(backdrop, source: ColorRGBA): ColorRGBA =
result = backdrop
result.a = max(0, (backdrop.a.int32 * (255 - source.a.int32)) div 255).uint8
proc blendIntersectMask(backdrop, source: ColorRGBA): ColorRGBA =
result = backdrop
result.a = ((backdrop.a.uint32 * (source.a.uint32)) div 255).uint8
proc blendExcludeMask(backdrop, source: ColorRGBA): ColorRGBA =
result = backdrop
result.a = max(backdrop.a, source.a) - min(backdrop.a, source.a)
proc blendOverwrite(backdrop, source: ColorRGBA): ColorRGBA =
source
proc blender*(blendMode: BlendMode): Blender =
case blendMode:
of bmNormal: blendNormal
of bmDarken: blendDarken
of bmMultiply: blendMultiply
of bmLinearBurn: blendLinearBurn
of bmColorBurn: blendColorBurn
of bmLighten: blendLighten
of bmScreen: blendScreen
of bmLinearDodge: blendLinearDodge
of bmColorDodge: blendColorDodge
of bmOverlay: blendOverlay
of bmSoftLight: blendSoftLight
of bmHardLight: blendHardLight
of bmDifference: blendDifference
of bmExclusion: blendExclusion
of bmHue: blendHue
of bmSaturation: blendSaturation
of bmColor: blendColor
of bmLuminosity: blendLuminosity
of bmMask: blendMask
of bmOverwrite: blendOverwrite
of bmSubtractMask: blendSubtractMask
of bmIntersectMask: blendIntersectMask
of bmExcludeMask: blendExcludeMask

View file

@ -369,6 +369,50 @@ 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)
var i: int
when defined(amd64) and not defined(pixieNoSimd):
let mask32 = cast[M128i]([uint32.high, 0, 0, 0])
for _ in countup(0, image.data.len - 16, 16):
var
a = mm_loadu_si128(image.data[i + 0].addr)
b = mm_loadu_si128(image.data[i + 4].addr)
c = mm_loadu_si128(image.data[i + 8].addr)
d = mm_loadu_si128(image.data[i + 12].addr)
template pack(v: var M128i) =
# Shuffle the alpha values for these 4 colors to the first 4 bytes
v = mm_srli_epi32(v, 24)
let
i = mm_srli_si128(v, 3)
j = mm_srli_si128(v, 6)
k = mm_srli_si128(v, 9)
v = mm_or_si128(mm_or_si128(v, i), mm_or_si128(j, k))
v = mm_and_si128(v, mask32)
pack(a)
pack(b)
pack(c)
pack(d)
b = mm_slli_si128(b, 4)
c = mm_slli_si128(c, 8)
d = mm_slli_si128(d, 12)
mm_storeu_si128(
result.data[i].addr,
mm_or_si128(mm_or_si128(a, b), mm_or_si128(c, d))
)
i += 16
for j in i ..< image.data.len:
result.data[j] = image.data[j].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 +437,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 +468,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)
@ -479,99 +508,110 @@ proc draw*(
) {.inline.} = ) {.inline.} =
mask.draw(image, translate(pos), blendMode) mask.draw(image, translate(pos), blendMode)
proc gaussianLookup(radius: int): seq[float32] = proc blur*(target: Image | Mask, radius: float32) =
## Compute lookup table for 1d Gaussian kernel. ## Applies Gaussian blur to the image given a radius.
result.setLen(radius * 2 + 1) let radius = round(radius).int
var total = 0.0 if radius == 0:
for xb in -radius .. radius: return
let
s = radius.float32 / 2.2 # 2.2 matches Figma. proc gaussianLookup(radius: int): seq[uint32] =
x = xb.float32 ## Compute lookup table for 1d Gaussian kernel.
a = 1 / sqrt(2 * PI * s^2) * exp(-1 * x^2 / (2 * s^2)) ## Values are [0, 255] * 1024.
result[xb + radius] = a result.setLen(radius * 2 + 1)
total += a
for xb in -radius .. radius: var
result[xb + radius] = result[xb + radius] / total floats = newSeq[float32](result.len)
total = 0.0
for xb in -radius .. radius:
let
s = radius.float32 / 2.2 # 2.2 matches Figma.
x = xb.float32
a = 1 / sqrt(2 * PI * s^2) * exp(-1 * x^2 / (2 * s^2))
floats[xb + radius] = a
total += a
for xb in -radius .. radius:
floats[xb + radius] = floats[xb + radius] / total
for i, f in floats:
result[i] = round(f * 255 * 1024).uint32
let lookup = gaussianLookup(radius)
when type(target) is Image:
template `*`(sample: ColorRGBA, a: uint32): array[4, uint32] =
[
sample.r * a,
sample.g * a,
sample.b * a,
sample.a * a
]
template `+=`(values: var array[4, uint32], sample: array[4, uint32]) =
values[0] += sample[0]
values[1] += sample[1]
values[2] += sample[2]
values[3] += sample[3]
template rgba(values: array[4, uint32]): ColorRGBA =
rgba(
(values[0] div 1024 div 255).uint8,
(values[1] div 1024 div 255).uint8,
(values[2] div 1024 div 255).uint8,
(values[3] div 1024 div 255).uint8
)
# Blur in the X direction.
var blurX = newImage(target.width, target.height)
for y in 0 ..< target.height:
for x in 0 ..< target.width:
var values: array[4, uint32]
for xb in -radius .. radius:
let
sample = target[x + xb, y]
a = lookup[xb + radius].uint32
values += sample * a
blurX.setRgbaUnsafe(x, y, values.rgba())
# Blur in the Y direction.
for y in 0 ..< target.height:
for x in 0 ..< target.width:
var values: array[4, uint32]
for yb in -radius .. radius:
let
sample = blurX[x, y + yb]
a = lookup[yb + radius].uint32
values += sample * a
target.setRgbaUnsafe(x, y, values.rgba())
else: # target is a Mask
# 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 value: uint32
for xb in -radius .. radius:
let
sample = target[x + xb, y]
a = lookup[xb + radius].uint32
value += sample * a
blurX.setValueUnsafe(x, y, (value div 1024 div 255).uint8)
# Blur in the Y direction and modify image.
for y in 0 ..< target.height:
for x in 0 ..< target.width:
var value: uint32
for yb in -radius .. radius:
let
sample = blurX[x, y + yb]
a = lookup[yb + radius].uint32
value += sample * a
target.setValueUnsafe(x, y, (value div 1024 div 255).uint8)
when defined(release): when defined(release):
{.pop.} {.pop.}
proc blur*(image: Image, radius: float32) =
## 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 = newImage(image.width, image.height)
for y in 0 ..< image.height:
for x in 0 ..< image.width:
var c: Color
var totalA = 0.0
for xb in -radius .. radius:
let c2 = image[x + xb, y].color
let a = lookup[xb + radius]
let aa = c2.a * a
totalA += aa
c.r += c2.r * aa
c.g += c2.g * aa
c.b += c2.b * aa
c.a += c2.a * a
c.r = c.r / totalA
c.g = c.g / totalA
c.b = c.b / totalA
blurX.setRgbaUnsafe(x, y, c.rgba)
# Blur in the Y direction.
for y in 0 ..< image.height:
for x in 0 ..< image.width:
var c: Color
var totalA = 0.0
for yb in -radius .. radius:
let c2 = blurX[x, y + yb].color
let a = lookup[yb + radius]
let aa = c2.a * a
totalA += aa
c.r += c2.r * aa
c.g += c2.g * aa
c.b += c2.b * aa
c.a += c2.a * a
c.r = c.r / totalA
c.g = c.g / totalA
c.b = c.b / totalA
image.setRgbaUnsafe(x, y, c.rgba)
proc blurAlpha*(image: Image, radius: float32) =
## 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 = newImage(image.width, image.height)
for y in 0 ..< image.height:
for x in 0 ..< image.width:
var alpha: float32
for xb in -radius .. radius:
let c2 = image[x + xb, y]
let a = lookup[xb + radius]
alpha += c2.a.float32 * a
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.
## A = 0 stays 0. Anything else turns into 255. ## A = 0 stays 0. Anything else turns into 255.
@ -688,49 +728,28 @@ 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))
proc spread*(image: Image, spread: float32) = else:
## Grows the image as a mask by spread. target.fill(0)
if spread == 0: target.draw(copy, offset, bmOverwrite) # Draw copy at offset
return
if spread < 0:
raise newException(PixieError, "Cannot apply negative spread")
let
copy = image.copy()
spread = round(spread).int
for y in 0 ..< image.height:
for x in 0 ..< image.width:
var maxAlpha = 0.uint8
block blurBox:
for bx in -spread .. spread:
for by in -spread .. spread:
let alpha = copy[x + bx, y + by].a
if alpha > maxAlpha:
maxAlpha = alpha
if maxAlpha == 255:
break blurBox
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

View file

@ -18,20 +18,144 @@ timeIt "blendNormal":
reset() reset()
timeIt "blendNormalFloats":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendNormalFloats(
backdrop.data[i].color, source.data[i].color
).rgba
reset()
timeIt "blendDarken": timeIt "blendDarken":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendDarken(backdrop.data[i], source.data[i]) backdrop.data[i] = blendDarken(backdrop.data[i], source.data[i])
reset() reset()
timeIt "blendMultiply":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendMultiply(backdrop.data[i], source.data[i])
reset()
timeIt "blendLinearBurn":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLinearBurn(backdrop.data[i], source.data[i])
reset()
timeIt "blendColorBurn":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendColorBurn(backdrop.data[i], source.data[i])
reset()
timeIt "blendLighten":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLighten(backdrop.data[i], source.data[i])
reset()
timeIt "blendScreen":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendScreen(backdrop.data[i], source.data[i])
reset()
timeIt "blendLinearDodge":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLinearDodge(backdrop.data[i], source.data[i])
reset()
timeIt "blendColorDodge":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendColorDodge(backdrop.data[i], source.data[i])
reset()
timeIt "blendOverlay":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendOverlay(backdrop.data[i], source.data[i])
reset()
timeIt "blendSoftLight":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendSoftLight(backdrop.data[i], source.data[i])
reset()
timeIt "blendHardLight":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendHardLight(backdrop.data[i], source.data[i])
reset()
timeIt "blendDifference":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendDifference(backdrop.data[i], source.data[i])
reset()
timeIt "blendExclusion":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendExclusion(backdrop.data[i], source.data[i])
reset()
timeIt "blendHue":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendHue(backdrop.data[i], source.data[i])
reset()
timeIt "blendSaturation":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendSaturation(backdrop.data[i], source.data[i])
reset()
timeIt "blendColor":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendColor(backdrop.data[i], source.data[i])
reset()
timeIt "blendLuminosity":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLuminosity(backdrop.data[i], source.data[i])
reset()
timeIt "blendMask":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendMask(backdrop.data[i], source.data[i])
reset()
timeIt "blendSubtractMask":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendSubtractMask(backdrop.data[i], source.data[i])
reset()
timeIt "blendIntersectMask":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendIntersectMask(backdrop.data[i], source.data[i])
reset()
timeIt "blendExcludeMask":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendExcludeMask(backdrop.data[i], source.data[i])
when defined(amd64) and not defined(pixieNoSimd):
import nimsimd/sse2
reset()
timeIt "blendNormal [simd]":
for i in countup(0, backdrop.data.len - 4, 4):
let
b = mm_loadu_si128(backdrop.data[i].addr)
s = mm_loadu_si128(source.data[i].addr)
mm_storeu_si128(backdrop.data[i].addr, blendNormalSimd(b, s))
reset()
timeIt "blendDarkenFloats": timeIt "blendDarkenFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendDarkenFloats( backdrop.data[i] = blendDarkenFloats(
@ -40,9 +164,11 @@ timeIt "blendDarkenFloats":
reset() reset()
timeIt "blendMultiply": timeIt "blendNormalFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendMultiply(backdrop.data[i], source.data[i]) backdrop.data[i] = blendNormalFloats(
backdrop.data[i].color, source.data[i].color
).rgba
reset() reset()
@ -54,12 +180,6 @@ timeIt "blendMultiplyFloats":
reset() reset()
timeIt "blendLinearBurn":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLinearBurn(backdrop.data[i], source.data[i])
reset()
timeIt "blendLinearBurnFloats": timeIt "blendLinearBurnFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLinearBurnFloats( backdrop.data[i] = blendLinearBurnFloats(
@ -68,12 +188,6 @@ timeIt "blendLinearBurnFloats":
reset() reset()
timeIt "blendColorBurn":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendColorBurn(backdrop.data[i], source.data[i])
reset()
timeIt "blendColorBurnFloats": timeIt "blendColorBurnFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendColorBurnFloats( backdrop.data[i] = blendColorBurnFloats(
@ -82,12 +196,6 @@ timeIt "blendColorBurnFloats":
reset() reset()
timeIt "blendLighten":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLighten(backdrop.data[i], source.data[i])
reset()
timeIt "blendLightenFloats": timeIt "blendLightenFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLightenFloats( backdrop.data[i] = blendLightenFloats(
@ -96,12 +204,6 @@ timeIt "blendLightenFloats":
reset() reset()
timeIt "blendScreen":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendScreen(backdrop.data[i], source.data[i])
reset()
timeIt "blendScreenFloats": timeIt "blendScreenFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendScreenFloats( backdrop.data[i] = blendScreenFloats(
@ -110,12 +212,6 @@ timeIt "blendScreenFloats":
reset() reset()
timeIt "blendLinearDodge":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLinearDodge(backdrop.data[i], source.data[i])
reset()
timeIt "blendLinearDodgeFloats": timeIt "blendLinearDodgeFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLinearDodgeFloats( backdrop.data[i] = blendLinearDodgeFloats(
@ -124,12 +220,6 @@ timeIt "blendLinearDodgeFloats":
reset() reset()
timeIt "blendColorDodge":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendColorDodge(backdrop.data[i], source.data[i])
reset()
timeIt "blendColorDodgeFloats": timeIt "blendColorDodgeFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendColorDodgeFloats( backdrop.data[i] = blendColorDodgeFloats(
@ -138,12 +228,6 @@ timeIt "blendColorDodgeFloats":
reset() reset()
timeIt "blendOverlay":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendOverlay(backdrop.data[i], source.data[i])
reset()
timeIt "blendOverlayFloats": timeIt "blendOverlayFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendOverlayFloats( backdrop.data[i] = blendOverlayFloats(
@ -152,12 +236,6 @@ timeIt "blendOverlayFloats":
reset() reset()
timeIt "blendSoftLight":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendSoftLight(backdrop.data[i], source.data[i])
reset()
timeIt "blendSoftLightFloats": timeIt "blendSoftLightFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendSoftLightFloats( backdrop.data[i] = blendSoftLightFloats(
@ -166,12 +244,6 @@ timeIt "blendSoftLightFloats":
reset() reset()
timeIt "blendHardLight":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendHardLight(backdrop.data[i], source.data[i])
reset()
timeIt "blendHardLightFloats": timeIt "blendHardLightFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendHardLightFloats( backdrop.data[i] = blendHardLightFloats(
@ -180,12 +252,6 @@ timeIt "blendHardLightFloats":
reset() reset()
timeIt "blendDifference":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendDifference(backdrop.data[i], source.data[i])
reset()
timeIt "blendDifferenceFloats": timeIt "blendDifferenceFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendDifferenceFloats( backdrop.data[i] = blendDifferenceFloats(
@ -194,12 +260,6 @@ timeIt "blendDifferenceFloats":
reset() reset()
timeIt "blendExclusion":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendExclusion(backdrop.data[i], source.data[i])
reset()
timeIt "blendExclusionFloats": timeIt "blendExclusionFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendExclusionFloats( backdrop.data[i] = blendExclusionFloats(
@ -208,12 +268,6 @@ timeIt "blendExclusionFloats":
reset() reset()
timeIt "blendHue":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendHue(backdrop.data[i], source.data[i])
reset()
timeIt "blendHueFloats": timeIt "blendHueFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendHueFloats( backdrop.data[i] = blendHueFloats(
@ -222,12 +276,6 @@ timeIt "blendHueFloats":
reset() reset()
timeIt "blendSaturation":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendSaturation(backdrop.data[i], source.data[i])
reset()
timeIt "blendSaturationFloats": timeIt "blendSaturationFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendSaturationFloats( backdrop.data[i] = blendSaturationFloats(
@ -236,12 +284,6 @@ timeIt "blendSaturationFloats":
reset() reset()
timeIt "blendColor":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendColor(backdrop.data[i], source.data[i])
reset()
timeIt "blendColorFloats": timeIt "blendColorFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendColorFloats( backdrop.data[i] = blendColorFloats(
@ -250,12 +292,6 @@ timeIt "blendColorFloats":
reset() reset()
timeIt "blendLuminosity":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLuminosity(backdrop.data[i], source.data[i])
reset()
timeIt "blendLuminosityFloats": timeIt "blendLuminosityFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendLuminosityFloats( backdrop.data[i] = blendLuminosityFloats(
@ -264,12 +300,6 @@ timeIt "blendLuminosityFloats":
reset() reset()
timeIt "blendMask":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendMask(backdrop.data[i], source.data[i])
reset()
timeIt "blendMaskFloats": timeIt "blendMaskFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendMaskFloats( backdrop.data[i] = blendMaskFloats(
@ -278,12 +308,6 @@ timeIt "blendMaskFloats":
reset() reset()
timeIt "blendSubtractMask":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendSubtractMask(backdrop.data[i], source.data[i])
reset()
timeIt "blendSubtractMaskFloats": timeIt "blendSubtractMaskFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendSubtractMaskFloats( backdrop.data[i] = blendSubtractMaskFloats(
@ -292,12 +316,6 @@ timeIt "blendSubtractMaskFloats":
reset() reset()
timeIt "blendIntersectMask":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendIntersectMask(backdrop.data[i], source.data[i])
reset()
timeIt "blendIntersectMaskFloats": timeIt "blendIntersectMaskFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendIntersectMaskFloats( backdrop.data[i] = blendIntersectMaskFloats(
@ -306,30 +324,8 @@ timeIt "blendIntersectMaskFloats":
reset() reset()
timeIt "blendExcludeMask":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendExcludeMask(backdrop.data[i], source.data[i])
reset()
timeIt "blendExcludeMaskFloats": timeIt "blendExcludeMaskFloats":
for i in 0 ..< backdrop.data.len: for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendExcludeMaskFloats( backdrop.data[i] = blendExcludeMaskFloats(
backdrop.data[i].color, source.data[i].color backdrop.data[i].color, source.data[i].color
).rgba ).rgba
reset()
timeIt "blendNormalPremultiplied":
for i in 0 ..< backdrop.data.len:
backdrop.data[i] = blendNormalPremultiplied(backdrop.data[i], source.data[i])
when defined(amd64) and not defined(pixieNoSimd):
import nimsimd/sse2
timeIt "blendNormalPremultiplied [simd]":
for i in countup(0, backdrop.data.len - 4, 4):
let
b = mm_loadu_si128(backdrop.data[i].addr)
s = mm_loadu_si128(source.data[i].addr)
mm_storeu_si128(backdrop.data[i].addr, blendNormalPremultiplied(b, s))

View file

@ -72,6 +72,17 @@ block:
reset() reset()
timeIt "newMask":
let mask = image.newMask()
doAssert mask[0, 0] == image[0, 0].a
reset()
timeIt "blur":
image.blur(40)
reset()
timeIt "lerp integers": timeIt "lerp integers":
for i in 0 ..< 100000: for i in 0 ..< 100000:
let c = image[0, 0] let c = image[0, 0]

View file

@ -20,3 +20,8 @@ reset()
timeIt "applyOpacity": timeIt "applyOpacity":
mask.applyOpacity(0.5) mask.applyOpacity(0.5)
reset()
timeIt "blur":
mask.blur(40)

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())