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.
|
||||
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.khronos.org/registry/OpenGL/extensions/KHR/KHR_blend_equation_advanced.txt
|
||||
|
||||
|
@ -34,12 +31,20 @@ type
|
|||
bmIntersectMask
|
||||
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):
|
||||
{.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:
|
||||
return source
|
||||
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.g = source.g + ((backdrop.g.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):
|
||||
proc blendNormalPremultiplied*(backdrop, source: M128i): M128i {.inline.} =
|
||||
proc blendNormalPremultipliedSimd*(backdrop, source: M128i): M128i =
|
||||
let
|
||||
alphaMask = mm_set1_epi32(cast[int32](0xff000000))
|
||||
oddMask = mm_set1_epi16(cast[int16](0xff00))
|
||||
div255 = mm_set1_epi16(cast[int16](0x8081))
|
||||
|
||||
# Shortcuts didn't help (backdrop.a == 0, source.a == 0, source.a == 255)
|
||||
|
||||
var
|
||||
sourceAlpha = mm_and_si128(source, alphaMask)
|
||||
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))
|
||||
)
|
||||
|
||||
proc blenderSimd*(blendMode: BlendMode): BlenderSimd =
|
||||
case blendMode:
|
||||
of bmNormal: blendNormalPremultipliedSimd
|
||||
else:
|
||||
raise newException(PixieError, "No SIMD blender for " & $blendMode)
|
||||
|
||||
|
||||
when defined(release):
|
||||
{.pop.}
|
||||
|
||||
|
@ -534,7 +550,7 @@ proc blendOverwrite(backdrop, source: ColorRGBA): ColorRGBA =
|
|||
source
|
||||
|
||||
proc blender*(blendMode: BlendMode): Blender =
|
||||
case blendMode
|
||||
case blendMode:
|
||||
of bmNormal: blendNormal
|
||||
of bmDarken: blendDarken
|
||||
of bmMultiply: blendMultiply
|
||||
|
|
|
@ -176,16 +176,28 @@ proc minifyBy2*(image: Image, power = 1): Image =
|
|||
if power == 0:
|
||||
return image.copy()
|
||||
|
||||
var src = image
|
||||
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 x in 0 ..< result.width:
|
||||
var color =
|
||||
image.getRgbaUnsafe(x * 2 + 0, y * 2 + 0).color / 4.0 +
|
||||
image.getRgbaUnsafe(x * 2 + 1, y * 2 + 0).color / 4.0 +
|
||||
image.getRgbaUnsafe(x * 2 + 1, y * 2 + 1).color / 4.0 +
|
||||
image.getRgbaUnsafe(x * 2 + 0, y * 2 + 1).color / 4.0
|
||||
result.setRgbaUnsafe(x, y, color.rgba)
|
||||
let
|
||||
a = src.getRgbaUnsafe(x * 2 + 0, y * 2 + 0)
|
||||
b = src.getRgbaUnsafe(x * 2 + 1, y * 2 + 0)
|
||||
c = src.getRgbaUnsafe(x * 2 + 1, y * 2 + 1)
|
||||
d = src.getRgbaUnsafe(x * 2 + 0, y * 2 + 1)
|
||||
|
||||
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 =
|
||||
## 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):
|
||||
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):
|
||||
{.pop.}
|
||||
|
|
|
@ -360,6 +360,9 @@ proc rect*(path: var Path, x, y, w, h: float32) =
|
|||
path.lineTo(x, y + h)
|
||||
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) =
|
||||
## Draws a n sided regular polygon at (x, y) with size.
|
||||
path.moveTo(x + size * cos(0.0), y + size * sin(0.0))
|
||||
|
@ -886,7 +889,8 @@ proc fillShapes(
|
|||
image: Image,
|
||||
shapes: seq[seq[Vec2]],
|
||||
color: ColorRGBA,
|
||||
windingRule: WindingRule
|
||||
windingRule: WindingRule,
|
||||
blendMode: BlendMode
|
||||
) =
|
||||
let (topHalf, bottomHalf, fullHeight) =
|
||||
partitionSegments(shapes, image.height div 2)
|
||||
|
@ -898,6 +902,10 @@ proc fillShapes(
|
|||
startX = max(0, bounds.x.int)
|
||||
startY = max(0, bounds.y.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
|
||||
coverages = newSeq[uint8](image.width)
|
||||
|
@ -979,10 +987,7 @@ proc fillShapes(
|
|||
let
|
||||
index = image.dataIndex(x, y)
|
||||
backdrop = mm_loadu_si128(image.data[index].addr)
|
||||
mm_storeu_si128(
|
||||
image.data[index].addr,
|
||||
blendNormalPremultiplied(backdrop, source)
|
||||
)
|
||||
mm_storeu_si128(image.data[index].addr, blenderSimd(backdrop, source))
|
||||
|
||||
x += 4
|
||||
|
||||
|
@ -1003,7 +1008,7 @@ proc fillShapes(
|
|||
source.a = ((color.a.uint16 * coverage) div 255).uint8
|
||||
|
||||
let backdrop = image.getRgbaUnsafe(x, y)
|
||||
image.setRgbaUnsafe(x, y, blendNormalPremultiplied(backdrop, source))
|
||||
image.setRgbaUnsafe(x, y, blender(backdrop, source))
|
||||
inc x
|
||||
|
||||
proc fillShapes(
|
||||
|
@ -1160,35 +1165,38 @@ proc fillPath*(
|
|||
image: Image,
|
||||
path: SomePath,
|
||||
color: ColorRGBA,
|
||||
windingRule = wrNonZero
|
||||
windingRule = wrNonZero,
|
||||
blendMode = bmNormal
|
||||
) {.inline.} =
|
||||
image.fillShapes(parseSomePath(path), color, windingRule)
|
||||
image.fillShapes(parseSomePath(path), color, windingRule, blendMode)
|
||||
|
||||
proc fillPath*(
|
||||
image: Image,
|
||||
path: SomePath,
|
||||
color: ColorRGBA,
|
||||
pos: Vec2,
|
||||
windingRule = wrNonZero
|
||||
windingRule = wrNonZero,
|
||||
blendMode = bmNormal
|
||||
) =
|
||||
var shapes = parseSomePath(path)
|
||||
for shape in shapes.mitems:
|
||||
for segment in shape.mitems:
|
||||
segment += pos
|
||||
image.fillShapes(shapes, color, windingRule)
|
||||
image.fillShapes(shapes, color, windingRule, blendMode)
|
||||
|
||||
proc fillPath*(
|
||||
image: Image,
|
||||
path: SomePath,
|
||||
color: ColorRGBA,
|
||||
mat: Mat3,
|
||||
windingRule = wrNonZero
|
||||
windingRule = wrNonZero,
|
||||
blendMode = bmNormal
|
||||
) =
|
||||
var shapes = parseSomePath(path)
|
||||
for shape in shapes.mitems:
|
||||
for segment in shape.mitems:
|
||||
segment = mat * segment
|
||||
image.fillShapes(shapes, color, windingRule)
|
||||
image.fillShapes(shapes, color, windingRule, blendMode)
|
||||
|
||||
proc fillPath*(
|
||||
mask: Mask,
|
||||
|
@ -1226,14 +1234,15 @@ proc strokePath*(
|
|||
path: SomePath,
|
||||
color: ColorRGBA,
|
||||
strokeWidth = 1.0,
|
||||
windingRule = wrNonZero
|
||||
windingRule = wrNonZero,
|
||||
blendMode = bmNormal
|
||||
) =
|
||||
let strokeShapes = strokeShapes(
|
||||
parseSomePath(path),
|
||||
strokeWidth,
|
||||
windingRule
|
||||
)
|
||||
image.fillShapes(strokeShapes, color, windingRule)
|
||||
image.fillShapes(strokeShapes, color, windingRule, blendMode)
|
||||
|
||||
proc strokePath*(
|
||||
image: Image,
|
||||
|
@ -1241,7 +1250,8 @@ proc strokePath*(
|
|||
color: ColorRGBA,
|
||||
strokeWidth = 1.0,
|
||||
pos: Vec2,
|
||||
windingRule = wrNonZero
|
||||
windingRule = wrNonZero,
|
||||
blendMode = bmNormal
|
||||
) =
|
||||
var strokeShapes = strokeShapes(
|
||||
parseSomePath(path),
|
||||
|
@ -1251,7 +1261,7 @@ proc strokePath*(
|
|||
for shape in strokeShapes.mitems:
|
||||
for segment in shape.mitems:
|
||||
segment += pos
|
||||
image.fillShapes(strokeShapes, color, windingRule)
|
||||
image.fillShapes(strokeShapes, color, windingRule, blendMode)
|
||||
|
||||
proc strokePath*(
|
||||
image: Image,
|
||||
|
@ -1259,7 +1269,8 @@ proc strokePath*(
|
|||
color: ColorRGBA,
|
||||
strokeWidth = 1.0,
|
||||
mat: Mat3,
|
||||
windingRule = wrNonZero
|
||||
windingRule = wrNonZero,
|
||||
blendMode = bmNormal
|
||||
) =
|
||||
var strokeShapes = strokeShapes(
|
||||
parseSomePath(path),
|
||||
|
@ -1269,7 +1280,7 @@ proc strokePath*(
|
|||
for shape in strokeShapes.mitems:
|
||||
for segment in shape.mitems:
|
||||
segment = mat * segment
|
||||
image.fillShapes(strokeShapes, color, windingRule)
|
||||
image.fillShapes(strokeShapes, color, windingRule, blendMode)
|
||||
|
||||
proc strokePath*(
|
||||
mask: Mask,
|
||||
|
|
|
@ -13,6 +13,13 @@ timeIt "fill_rgba":
|
|||
timeIt "subImage":
|
||||
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":
|
||||
a.invert()
|
||||
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")
|
||||
b = a.superImage(45, 45, 20, 20)
|
||||
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