Merge pull request #89 from guzba/master

image minifyBy2 faster, mask minifyBy2, path fills take blendMode
This commit is contained in:
treeform 2021-02-08 10:53:00 -08:00 committed by GitHub
commit c233894283
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 130 additions and 35 deletions

View file

@ -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

View file

@ -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.

View file

@ -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.}

View file

@ -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,

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

View file

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