Merge pull request #89 from guzba/master
image minifyBy2 faster, mask minifyBy2, path fills take blendMode
This commit is contained in:
commit
c233894283
10 changed files with 130 additions and 35 deletions
|
@ -1,9 +1,6 @@
|
||||||
## Blending modes.
|
## Blending modes.
|
||||||
import chroma, math, common
|
import chroma, math, common
|
||||||
|
|
||||||
when defined(amd64) and not defined(pixieNoSimd):
|
|
||||||
import nimsimd/sse2
|
|
||||||
|
|
||||||
# See https://www.w3.org/TR/compositing-1/
|
# See https://www.w3.org/TR/compositing-1/
|
||||||
# See https://www.khronos.org/registry/OpenGL/extensions/KHR/KHR_blend_equation_advanced.txt
|
# See https://www.khronos.org/registry/OpenGL/extensions/KHR/KHR_blend_equation_advanced.txt
|
||||||
|
|
||||||
|
@ -34,12 +31,20 @@ type
|
||||||
bmIntersectMask
|
bmIntersectMask
|
||||||
bmExcludeMask
|
bmExcludeMask
|
||||||
|
|
||||||
Blender* = proc(a, b: ColorRGBA): ColorRGBA
|
Blender* = proc(backdrop, source: ColorRGBA): ColorRGBA
|
||||||
|
|
||||||
|
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 blendNormalPremultiplied*(backdrop, source: ColorRGBA): ColorRGBA {.inline.} =
|
proc blendAlpha(backdrop, source: uint8): uint8 {.inline.} =
|
||||||
|
source + ((backdrop.uint32 * (255 - source)) div 255).uint8
|
||||||
|
|
||||||
|
proc blendNormalPremultiplied*(backdrop, source: ColorRGBA): ColorRGBA =
|
||||||
if backdrop.a == 0:
|
if backdrop.a == 0:
|
||||||
return source
|
return source
|
||||||
if source.a == 255:
|
if source.a == 255:
|
||||||
|
@ -51,17 +56,21 @@ proc blendNormalPremultiplied*(backdrop, source: ColorRGBA): ColorRGBA {.inline.
|
||||||
result.r = source.r + ((backdrop.r.uint32 * k) div 255).uint8
|
result.r = source.r + ((backdrop.r.uint32 * k) div 255).uint8
|
||||||
result.g = source.g + ((backdrop.g.uint32 * k) div 255).uint8
|
result.g = source.g + ((backdrop.g.uint32 * k) div 255).uint8
|
||||||
result.b = source.b + ((backdrop.b.uint32 * k) div 255).uint8
|
result.b = source.b + ((backdrop.b.uint32 * k) div 255).uint8
|
||||||
result.a = source.a + ((backdrop.a.uint32 * k) div 255).uint8
|
result.a = blendAlpha(backdrop.a, source.a)
|
||||||
|
|
||||||
|
proc blenderPremultiplied*(blendMode: BlendMode): Blender =
|
||||||
|
case blendMode:
|
||||||
|
of bmNormal: blendNormalPremultiplied
|
||||||
|
else:
|
||||||
|
raise newException(PixieError, "No premultiplied blender for " & $blendMode)
|
||||||
|
|
||||||
when defined(amd64) and not defined(pixieNoSimd):
|
when defined(amd64) and not defined(pixieNoSimd):
|
||||||
proc blendNormalPremultiplied*(backdrop, source: M128i): M128i {.inline.} =
|
proc blendNormalPremultipliedSimd*(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))
|
||||||
div255 = mm_set1_epi16(cast[int16](0x8081))
|
div255 = mm_set1_epi16(cast[int16](0x8081))
|
||||||
|
|
||||||
# Shortcuts didn't help (backdrop.a == 0, source.a == 0, source.a == 255)
|
|
||||||
|
|
||||||
var
|
var
|
||||||
sourceAlpha = mm_and_si128(source, alphaMask)
|
sourceAlpha = mm_and_si128(source, alphaMask)
|
||||||
backdropEven = mm_slli_epi16(backdrop, 8)
|
backdropEven = mm_slli_epi16(backdrop, 8)
|
||||||
|
@ -85,6 +94,13 @@ 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 blenderSimd*(blendMode: BlendMode): BlenderSimd =
|
||||||
|
case blendMode:
|
||||||
|
of bmNormal: blendNormalPremultipliedSimd
|
||||||
|
else:
|
||||||
|
raise newException(PixieError, "No SIMD blender for " & $blendMode)
|
||||||
|
|
||||||
|
|
||||||
when defined(release):
|
when defined(release):
|
||||||
{.pop.}
|
{.pop.}
|
||||||
|
|
||||||
|
@ -534,7 +550,7 @@ proc blendOverwrite(backdrop, source: ColorRGBA): ColorRGBA =
|
||||||
source
|
source
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -176,16 +176,28 @@ proc minifyBy2*(image: Image, power = 1): Image =
|
||||||
if power == 0:
|
if power == 0:
|
||||||
return image.copy()
|
return image.copy()
|
||||||
|
|
||||||
|
var src = image
|
||||||
for i in 1 .. power:
|
for i in 1 .. power:
|
||||||
result = newImage(image.width div 2, image.height div 2)
|
result = newImage(src.width div 2, src.height div 2)
|
||||||
for y in 0 ..< result.height:
|
for y in 0 ..< result.height:
|
||||||
for x in 0 ..< result.width:
|
for x in 0 ..< result.width:
|
||||||
var color =
|
let
|
||||||
image.getRgbaUnsafe(x * 2 + 0, y * 2 + 0).color / 4.0 +
|
a = src.getRgbaUnsafe(x * 2 + 0, y * 2 + 0)
|
||||||
image.getRgbaUnsafe(x * 2 + 1, y * 2 + 0).color / 4.0 +
|
b = src.getRgbaUnsafe(x * 2 + 1, y * 2 + 0)
|
||||||
image.getRgbaUnsafe(x * 2 + 1, y * 2 + 1).color / 4.0 +
|
c = src.getRgbaUnsafe(x * 2 + 1, y * 2 + 1)
|
||||||
image.getRgbaUnsafe(x * 2 + 0, y * 2 + 1).color / 4.0
|
d = src.getRgbaUnsafe(x * 2 + 0, y * 2 + 1)
|
||||||
result.setRgbaUnsafe(x, y, color.rgba)
|
|
||||||
|
let color = rgba(
|
||||||
|
((a.r.uint32 + b.r + c.r + d.r) div 4).uint8,
|
||||||
|
((a.g.uint32 + b.g + c.g + d.g) div 4).uint8,
|
||||||
|
((a.b.uint32 + b.b + c.b + d.b) div 4).uint8,
|
||||||
|
((a.a.uint32 + b.a + c.a + d.a) div 4).uint8
|
||||||
|
)
|
||||||
|
|
||||||
|
result.setRgbaUnsafe(x, y, color)
|
||||||
|
|
||||||
|
# Set src as this result for if we do another power
|
||||||
|
src = result
|
||||||
|
|
||||||
proc magnifyBy2*(image: Image, power = 1): Image =
|
proc magnifyBy2*(image: Image, power = 1): Image =
|
||||||
## Scales image image up by 2 ^ power.
|
## Scales image image up by 2 ^ power.
|
||||||
|
|
|
@ -63,5 +63,23 @@ proc `[]=`*(mask: Mask, x, y: int, value: uint8) {.inline.} =
|
||||||
if mask.inside(x, y):
|
if mask.inside(x, y):
|
||||||
mask.setValueUnsafe(x, y, value)
|
mask.setValueUnsafe(x, y, value)
|
||||||
|
|
||||||
|
proc minifyBy2*(mask: Mask, power = 1): Mask =
|
||||||
|
## Scales the mask down by an integer scale.
|
||||||
|
if power < 0:
|
||||||
|
raise newException(PixieError, "Cannot minifyBy2 with negative power")
|
||||||
|
if power == 0:
|
||||||
|
return mask.copy()
|
||||||
|
|
||||||
|
for i in 1 .. power:
|
||||||
|
result = newMask(mask.width div 2, mask.height div 2)
|
||||||
|
for y in 0 ..< result.height:
|
||||||
|
for x in 0 ..< result.width:
|
||||||
|
let value =
|
||||||
|
mask.getValueUnsafe(x * 2 + 0, y * 2 + 0).uint32 +
|
||||||
|
mask.getValueUnsafe(x * 2 + 1, y * 2 + 0) +
|
||||||
|
mask.getValueUnsafe(x * 2 + 1, y * 2 + 1) +
|
||||||
|
mask.getValueUnsafe(x * 2 + 0, y * 2 + 1)
|
||||||
|
result.setValueUnsafe(x, y, (value div 4).uint8)
|
||||||
|
|
||||||
when defined(release):
|
when defined(release):
|
||||||
{.pop.}
|
{.pop.}
|
||||||
|
|
|
@ -360,6 +360,9 @@ proc rect*(path: var Path, x, y, w, h: float32) =
|
||||||
path.lineTo(x, y + h)
|
path.lineTo(x, y + h)
|
||||||
path.closePath()
|
path.closePath()
|
||||||
|
|
||||||
|
proc rect*(path: var Path, pos: Vec2, wh: Vec2) {.inline.} =
|
||||||
|
path.rect(pos.x, pos.y, wh.x, wh.y)
|
||||||
|
|
||||||
proc polygon*(path: var Path, x, y, size: float32, sides: int) =
|
proc polygon*(path: var Path, x, y, size: float32, sides: int) =
|
||||||
## Draws a n sided regular polygon at (x, y) with size.
|
## Draws a n sided regular polygon at (x, y) with size.
|
||||||
path.moveTo(x + size * cos(0.0), y + size * sin(0.0))
|
path.moveTo(x + size * cos(0.0), y + size * sin(0.0))
|
||||||
|
@ -886,7 +889,8 @@ proc fillShapes(
|
||||||
image: Image,
|
image: Image,
|
||||||
shapes: seq[seq[Vec2]],
|
shapes: seq[seq[Vec2]],
|
||||||
color: ColorRGBA,
|
color: ColorRGBA,
|
||||||
windingRule: WindingRule
|
windingRule: WindingRule,
|
||||||
|
blendMode: BlendMode
|
||||||
) =
|
) =
|
||||||
let (topHalf, bottomHalf, fullHeight) =
|
let (topHalf, bottomHalf, fullHeight) =
|
||||||
partitionSegments(shapes, image.height div 2)
|
partitionSegments(shapes, image.height div 2)
|
||||||
|
@ -898,6 +902,10 @@ 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()
|
||||||
|
|
||||||
|
when defined(amd64) and not defined(pixieNoSimd):
|
||||||
|
let blenderSimd = blendMode.blenderSimd()
|
||||||
|
|
||||||
var
|
var
|
||||||
coverages = newSeq[uint8](image.width)
|
coverages = newSeq[uint8](image.width)
|
||||||
|
@ -979,10 +987,7 @@ proc fillShapes(
|
||||||
let
|
let
|
||||||
index = image.dataIndex(x, y)
|
index = image.dataIndex(x, y)
|
||||||
backdrop = mm_loadu_si128(image.data[index].addr)
|
backdrop = mm_loadu_si128(image.data[index].addr)
|
||||||
mm_storeu_si128(
|
mm_storeu_si128(image.data[index].addr, blenderSimd(backdrop, source))
|
||||||
image.data[index].addr,
|
|
||||||
blendNormalPremultiplied(backdrop, source)
|
|
||||||
)
|
|
||||||
|
|
||||||
x += 4
|
x += 4
|
||||||
|
|
||||||
|
@ -1003,7 +1008,7 @@ proc fillShapes(
|
||||||
source.a = ((color.a.uint16 * coverage) div 255).uint8
|
source.a = ((color.a.uint16 * coverage) div 255).uint8
|
||||||
|
|
||||||
let backdrop = image.getRgbaUnsafe(x, y)
|
let backdrop = image.getRgbaUnsafe(x, y)
|
||||||
image.setRgbaUnsafe(x, y, blendNormalPremultiplied(backdrop, source))
|
image.setRgbaUnsafe(x, y, blender(backdrop, source))
|
||||||
inc x
|
inc x
|
||||||
|
|
||||||
proc fillShapes(
|
proc fillShapes(
|
||||||
|
@ -1160,35 +1165,38 @@ proc fillPath*(
|
||||||
image: Image,
|
image: Image,
|
||||||
path: SomePath,
|
path: SomePath,
|
||||||
color: ColorRGBA,
|
color: ColorRGBA,
|
||||||
windingRule = wrNonZero
|
windingRule = wrNonZero,
|
||||||
|
blendMode = bmNormal
|
||||||
) {.inline.} =
|
) {.inline.} =
|
||||||
image.fillShapes(parseSomePath(path), color, windingRule)
|
image.fillShapes(parseSomePath(path), color, windingRule, blendMode)
|
||||||
|
|
||||||
proc fillPath*(
|
proc fillPath*(
|
||||||
image: Image,
|
image: Image,
|
||||||
path: SomePath,
|
path: SomePath,
|
||||||
color: ColorRGBA,
|
color: ColorRGBA,
|
||||||
pos: Vec2,
|
pos: Vec2,
|
||||||
windingRule = wrNonZero
|
windingRule = wrNonZero,
|
||||||
|
blendMode = bmNormal
|
||||||
) =
|
) =
|
||||||
var shapes = parseSomePath(path)
|
var shapes = parseSomePath(path)
|
||||||
for shape in shapes.mitems:
|
for shape in shapes.mitems:
|
||||||
for segment in shape.mitems:
|
for segment in shape.mitems:
|
||||||
segment += pos
|
segment += pos
|
||||||
image.fillShapes(shapes, color, windingRule)
|
image.fillShapes(shapes, color, windingRule, blendMode)
|
||||||
|
|
||||||
proc fillPath*(
|
proc fillPath*(
|
||||||
image: Image,
|
image: Image,
|
||||||
path: SomePath,
|
path: SomePath,
|
||||||
color: ColorRGBA,
|
color: ColorRGBA,
|
||||||
mat: Mat3,
|
mat: Mat3,
|
||||||
windingRule = wrNonZero
|
windingRule = wrNonZero,
|
||||||
|
blendMode = bmNormal
|
||||||
) =
|
) =
|
||||||
var shapes = parseSomePath(path)
|
var shapes = parseSomePath(path)
|
||||||
for shape in shapes.mitems:
|
for shape in shapes.mitems:
|
||||||
for segment in shape.mitems:
|
for segment in shape.mitems:
|
||||||
segment = mat * segment
|
segment = mat * segment
|
||||||
image.fillShapes(shapes, color, windingRule)
|
image.fillShapes(shapes, color, windingRule, blendMode)
|
||||||
|
|
||||||
proc fillPath*(
|
proc fillPath*(
|
||||||
mask: Mask,
|
mask: Mask,
|
||||||
|
@ -1226,14 +1234,15 @@ proc strokePath*(
|
||||||
path: SomePath,
|
path: SomePath,
|
||||||
color: ColorRGBA,
|
color: ColorRGBA,
|
||||||
strokeWidth = 1.0,
|
strokeWidth = 1.0,
|
||||||
windingRule = wrNonZero
|
windingRule = wrNonZero,
|
||||||
|
blendMode = bmNormal
|
||||||
) =
|
) =
|
||||||
let strokeShapes = strokeShapes(
|
let strokeShapes = strokeShapes(
|
||||||
parseSomePath(path),
|
parseSomePath(path),
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
windingRule
|
windingRule
|
||||||
)
|
)
|
||||||
image.fillShapes(strokeShapes, color, windingRule)
|
image.fillShapes(strokeShapes, color, windingRule, blendMode)
|
||||||
|
|
||||||
proc strokePath*(
|
proc strokePath*(
|
||||||
image: Image,
|
image: Image,
|
||||||
|
@ -1241,7 +1250,8 @@ proc strokePath*(
|
||||||
color: ColorRGBA,
|
color: ColorRGBA,
|
||||||
strokeWidth = 1.0,
|
strokeWidth = 1.0,
|
||||||
pos: Vec2,
|
pos: Vec2,
|
||||||
windingRule = wrNonZero
|
windingRule = wrNonZero,
|
||||||
|
blendMode = bmNormal
|
||||||
) =
|
) =
|
||||||
var strokeShapes = strokeShapes(
|
var strokeShapes = strokeShapes(
|
||||||
parseSomePath(path),
|
parseSomePath(path),
|
||||||
|
@ -1251,7 +1261,7 @@ proc strokePath*(
|
||||||
for shape in strokeShapes.mitems:
|
for shape in strokeShapes.mitems:
|
||||||
for segment in shape.mitems:
|
for segment in shape.mitems:
|
||||||
segment += pos
|
segment += pos
|
||||||
image.fillShapes(strokeShapes, color, windingRule)
|
image.fillShapes(strokeShapes, color, windingRule, blendMode)
|
||||||
|
|
||||||
proc strokePath*(
|
proc strokePath*(
|
||||||
image: Image,
|
image: Image,
|
||||||
|
@ -1259,7 +1269,8 @@ proc strokePath*(
|
||||||
color: ColorRGBA,
|
color: ColorRGBA,
|
||||||
strokeWidth = 1.0,
|
strokeWidth = 1.0,
|
||||||
mat: Mat3,
|
mat: Mat3,
|
||||||
windingRule = wrNonZero
|
windingRule = wrNonZero,
|
||||||
|
blendMode = bmNormal
|
||||||
) =
|
) =
|
||||||
var strokeShapes = strokeShapes(
|
var strokeShapes = strokeShapes(
|
||||||
parseSomePath(path),
|
parseSomePath(path),
|
||||||
|
@ -1269,7 +1280,7 @@ proc strokePath*(
|
||||||
for shape in strokeShapes.mitems:
|
for shape in strokeShapes.mitems:
|
||||||
for segment in shape.mitems:
|
for segment in shape.mitems:
|
||||||
segment = mat * segment
|
segment = mat * segment
|
||||||
image.fillShapes(strokeShapes, color, windingRule)
|
image.fillShapes(strokeShapes, color, windingRule, blendMode)
|
||||||
|
|
||||||
proc strokePath*(
|
proc strokePath*(
|
||||||
mask: Mask,
|
mask: Mask,
|
||||||
|
|
|
@ -13,6 +13,13 @@ timeIt "fill_rgba":
|
||||||
timeIt "subImage":
|
timeIt "subImage":
|
||||||
keep a.subImage(0, 0, 256, 256)
|
keep a.subImage(0, 0, 256, 256)
|
||||||
|
|
||||||
|
# timeIt "superImage":
|
||||||
|
# discard
|
||||||
|
|
||||||
|
timeIt "minifyBy2":
|
||||||
|
let minified = a.minifyBy2()
|
||||||
|
doAssert minified[0, 0] == rgba(63, 127, 191, 191)
|
||||||
|
|
||||||
timeIt "invert":
|
timeIt "invert":
|
||||||
a.invert()
|
a.invert()
|
||||||
keep(a)
|
keep(a)
|
||||||
|
|
BIN
tests/images/masks/maskMinified.png
Normal file
BIN
tests/images/masks/maskMinified.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 270 B |
BIN
tests/images/minifiedBy2.png
Normal file
BIN
tests/images/minifiedBy2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 194 B |
BIN
tests/images/minifiedBy4.png
Normal file
BIN
tests/images/minifiedBy4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 161 B |
|
@ -79,3 +79,15 @@ block:
|
||||||
a = readImage("tests/images/flipped1.png")
|
a = readImage("tests/images/flipped1.png")
|
||||||
b = a.superImage(45, 45, 20, 20)
|
b = a.superImage(45, 45, 20, 20)
|
||||||
b.writeFile("tests/images/superimage6.png")
|
b.writeFile("tests/images/superimage6.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
a = readImage("tests/images/flipped1.png")
|
||||||
|
b = a.minifyBy2()
|
||||||
|
b.writeFile("tests/images/minifiedBy2.png")
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
a = readImage("tests/images/flipped1.png")
|
||||||
|
b = a.minifyBy2(2)
|
||||||
|
b.writeFile("tests/images/minifiedBy4.png")
|
||||||
|
|
19
tests/test_masks.nim
Normal file
19
tests/test_masks.nim
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import pixie, pixie/fileformats/png
|
||||||
|
|
||||||
|
block:
|
||||||
|
let
|
||||||
|
mask = newMask(100, 100)
|
||||||
|
r = 10.0
|
||||||
|
x = 10.0
|
||||||
|
y = 10.0
|
||||||
|
h = 80.0
|
||||||
|
w = 80.0
|
||||||
|
var path: Path
|
||||||
|
path.moveTo(x + r, y)
|
||||||
|
path.arcTo(x + w, y, x + w, y + h, r)
|
||||||
|
path.arcTo(x + w, y + h, x, y + h, r)
|
||||||
|
path.arcTo(x, y + h, x, y, r)
|
||||||
|
path.arcTo(x, y, x + w, y, r)
|
||||||
|
mask.fillPath(path)
|
||||||
|
|
||||||
|
writeFile("tests/images/masks/maskMinified.png", mask.minifyBy2().encodePng())
|
Loading…
Reference in a new issue