paths.nim masks take blendMode for masking, tests, fixes and etc

This commit is contained in:
Ryan Oldenburg 2021-06-18 21:43:42 -05:00
parent 5e4f291f7d
commit a838de821c
8 changed files with 142 additions and 82 deletions

View file

@ -1,4 +1,4 @@
version = "2.0.5" version = "2.1.0"
author = "Andre von Houck and Ryan Oldenburg" author = "Andre von Houck and Ryan Oldenburg"
description = "Full-featured 2d graphics library for Nim." description = "Full-featured 2d graphics library for Nim."
license = "MIT" license = "MIT"

View file

@ -435,9 +435,6 @@ proc blendSubtractMask(backdrop, source: ColorRGBX): ColorRGBX =
result.b = ((backdrop.b * a) div 255).uint8 result.b = ((backdrop.b * a) div 255).uint8
result.a = a.uint8 result.a = a.uint8
proc blendIntersectMask(backdrop, source: ColorRGBX): ColorRGBX =
blendMask(backdrop, source)
proc blendExcludeMask(backdrop, source: ColorRGBX): ColorRGBX = proc blendExcludeMask(backdrop, source: ColorRGBX): ColorRGBX =
let a = max(backdrop.a, source.a).uint32 - min(backdrop.a, source.a) let a = max(backdrop.a, source.a).uint32 - min(backdrop.a, source.a)
result.r = ((source.r * a) div 255).uint8 result.r = ((source.r * a) div 255).uint8
@ -489,9 +486,6 @@ proc maskMask(backdrop, source: uint8): uint8 =
proc maskSubtract(backdrop, source: uint8): uint8 = proc maskSubtract(backdrop, source: uint8): uint8 =
((backdrop.uint32 * (255 - source)) div 255).uint8 ((backdrop.uint32 * (255 - source)) div 255).uint8
proc maskIntersect(backdrop, source: uint8): uint8 =
maskMask(backdrop, source)
proc maskExclude(backdrop, source: uint8): uint8 = proc maskExclude(backdrop, source: uint8): uint8 =
max(backdrop, source) - min(backdrop, source) max(backdrop, source) - min(backdrop, source)
@ -592,7 +586,7 @@ when defined(amd64) and not defined(pixieNoSimd):
div255 = mm_set1_epi16(cast[int16](0x8081)) div255 = mm_set1_epi16(cast[int16](0x8081))
var var
sourceEven = mm_slli_epi16(mm_andnot_si128(oddMask, source), 8) sourceEven = mm_slli_epi16(source, 8)
sourceOdd = mm_and_si128(source, oddMask) sourceOdd = mm_and_si128(source, oddMask)
let let
@ -600,7 +594,7 @@ when defined(amd64) and not defined(pixieNoSimd):
oddK = mm_sub_epi16(v255high, sourceOdd) oddK = mm_sub_epi16(v255high, sourceOdd)
var var
backdropEven = mm_slli_epi16(mm_andnot_si128(oddMask, backdrop), 8) backdropEven = mm_slli_epi16(backdrop, 8)
backdropOdd = mm_and_si128(backdrop, oddMask) backdropOdd = mm_and_si128(backdrop, oddMask)
# backdrop * k # backdrop * k
@ -625,11 +619,11 @@ when defined(amd64) and not defined(pixieNoSimd):
let let
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))
sourceEven = mm_slli_epi16(mm_andnot_si128(oddMask, source), 8) sourceEven = mm_slli_epi16(source, 8)
sourceOdd = mm_and_si128(source, oddMask) sourceOdd = mm_and_si128(source, oddMask)
var var
backdropEven = mm_slli_epi16(mm_andnot_si128(oddMask, backdrop), 8) backdropEven = mm_slli_epi16(backdrop, 8)
backdropOdd = mm_and_si128(backdrop, oddMask) backdropOdd = mm_and_si128(backdrop, oddMask)
# backdrop * source # backdrop * source

View file

@ -1137,10 +1137,12 @@ iterator walk(
# between zero and nonzero (or the last hit) # between zero and nonzero (or the last hit)
count += winding count += winding
continue continue
if at > 0: if at <= 0:
if shouldFill(windingRule, count): count += winding
yield (prevAt, at, count) continue
prevAt = at if shouldFill(windingRule, count):
yield (prevAt, at, count)
prevAt = at
count += winding count += winding
when defined(pixieLeakCheck): when defined(pixieLeakCheck):
@ -1226,13 +1228,15 @@ proc computeCoverages(
for j in i ..< fillStart + fillLen: for j in i ..< fillStart + fillLen:
coverages[j] += sampleCoverage coverages[j] += sampleCoverage
proc clearUnsafe(image: Image, startX, startY, toX, toY: int) = proc clearUnsafe(target: Image | Mask, startX, startY, toX, toY: int) =
## From startXY to toXY, exclusive, toXY is not cleared. ## Clears data from [start, to).
image.data.fillUnsafe( let
rgbx(0, 0, 0, 0), start = target.dataIndex(startX, startY)
image.dataIndex(startX, startY), len = target.dataIndex(toX, toY) - start
image.dataIndex(toX, toY) - image.dataIndex(startX, startY) when type(target) is Image:
) target.data.fillUnsafe(rgbx(0, 0, 0, 0), start, len)
else: # target is Mask
target.data.fillUnsafe(0, start, len)
proc fillCoverage( proc fillCoverage(
image: Image, image: Image,
@ -1258,7 +1262,7 @@ proc fillCoverage(
let let
index = image.dataIndex(x, y) index = image.dataIndex(x, y)
eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128()) eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128())
if mm_movemask_epi8(eqZero) != 0xffff: if mm_movemask_epi8(eqZero) != 0xffff: # or blendMode == bmExcludeMask:
# If the coverages are not all zero # If the coverages are not all zero
if mm_movemask_epi8(mm_cmpeq_epi32(coverage, first32)) == 0xffff: if mm_movemask_epi8(mm_cmpeq_epi32(coverage, first32)) == 0xffff:
# Coverages are all 255 # Coverages are all 255
@ -1294,6 +1298,8 @@ proc fillCoverage(
image.data[index].addr, image.data[index].addr,
blenderSimd(backdrop, source) blenderSimd(backdrop, source)
) )
elif blendMode == bmMask:
mm_storeu_si128(image.data[index].addr, mm_setzero_si128())
x += 4 x += 4
let blender = blendMode.blender() let blender = blendMode.blender()
@ -1319,31 +1325,45 @@ proc fillCoverage(
if blendMode == bmMask: if blendMode == bmMask:
image.clearUnsafe(0, y, startX, y) image.clearUnsafe(0, y, startX, y)
proc fillCoverage(mask: Mask, startX, y: int, coverages: seq[uint8]) = proc fillCoverage(
mask: Mask,
startX, y: int,
coverages: seq[uint8],
blendMode: BlendMode
) =
var x = startX var x = startX
when defined(amd64) and not defined(pixieNoSimd): when defined(amd64) and not defined(pixieNoSimd):
# When supported, SIMD blend as much as possible if blendMode.hasSimdMasker():
let maskerSimd = bmNormal.maskerSimd() let maskerSimd = blendMode.maskerSimd()
for _ in countup(x, coverages.len - 16, 16): for _ in countup(x, coverages.len - 16, 16):
let let
coverage = mm_loadu_si128(coverages[x].unsafeAddr) index = mask.dataIndex(x, y)
eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128()) coverage = mm_loadu_si128(coverages[x].unsafeAddr)
if mm_movemask_epi8(eqZero) != 0xffff: eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128())
# If the coverages are not all zero if mm_movemask_epi8(eqZero) != 0xffff: # or blendMode == bmExcludeMask:
let backdrop = mm_loadu_si128(mask.data[mask.dataIndex(x, y)].addr) # If the coverages are not all zero
mm_storeu_si128( let backdrop = mm_loadu_si128(mask.data[index].addr)
mask.data[mask.dataIndex(x, y)].addr, mm_storeu_si128(
maskerSimd(backdrop, coverage) mask.data[index].addr,
) maskerSimd(backdrop, coverage)
x += 16 )
elif blendMode == bmMask:
mm_storeu_si128(mask.data[index].addr, mm_setzero_si128())
x += 16
let masker = blendMode.masker()
while x < mask.width: while x < mask.width:
let coverage = coverages[x] let coverage = coverages[x]
if coverage != 0: if coverage != 0 or blendMode == bmExcludeMask:
let backdrop = mask.getValueUnsafe(x, y) let backdrop = mask.getValueUnsafe(x, y)
mask.setValueUnsafe(x, y, blendAlpha(backdrop, coverage)) mask.setValueUnsafe(x, y, masker(backdrop, coverage))
elif blendMode == bmMask:
mask.setValueUnsafe(x, y, 0)
inc x inc x
if blendMode == bmMask:
mask.clearUnsafe(0, y, startX, y)
proc fillHits( proc fillHits(
image: Image, image: Image,
rgbx: ColorRGBX, rgbx: ColorRGBX,
@ -1354,53 +1374,89 @@ proc fillHits(
blendMode: BlendMode blendMode: BlendMode
) = ) =
let blender = blendMode.blender() let blender = blendMode.blender()
var x = 0 var filledTo: int
for (prevAt, at, count) in hits.walk(numHits, windingRule, y, image.wh): for (prevAt, at, count) in hits.walk(numHits, windingRule, y, image.wh):
let let
fillStart = prevAt.int fillStart = prevAt.int
fillLen = at.int - fillStart fillLen = at.int - fillStart
if fillLen > 0: if fillLen <= 0:
if blendMode == bmNormal and rgbx.a == 255: continue
fillUnsafe(image.data, rgbx, image.dataIndex(fillStart, y), fillLen)
else: filledTo = fillStart + fillLen
x = fillStart
when defined(amd64) and not defined(pixieNoSimd): if blendMode == bmNormal and rgbx.a == 255:
if blendMode.hasSimdBlender(): fillUnsafe(image.data, rgbx, image.dataIndex(fillStart, y), fillLen)
# When supported, SIMD blend as much as possible continue
let
blenderSimd = blendMode.blenderSimd() var x = fillStart
vColor = mm_set1_epi32(cast[int32](rgbx)) when defined(amd64) and not defined(pixieNoSimd):
for _ in countup(fillStart, fillLen - 16, 4): if blendMode.hasSimdBlender():
let # When supported, SIMD blend as much as possible
index = image.dataIndex(x, y) let
backdrop = mm_loadu_si128(image.data[index].addr) blenderSimd = blendMode.blenderSimd()
mm_storeu_si128( vColor = mm_set1_epi32(cast[int32](rgbx))
image.data[index].addr, for _ in countup(fillStart, fillLen - 16, 4):
blenderSimd(backdrop, vColor) let
) index = image.dataIndex(x, y)
x += 4 backdrop = mm_loadu_si128(image.data[index].addr)
while x < fillStart + fillLen: mm_storeu_si128(
let backdrop = image.getRgbaUnsafe(x, y) image.data[index].addr,
image.setRgbaUnsafe(x, y, blender(backdrop, rgbx)) blenderSimd(backdrop, vColor)
inc x )
x += 4
for x in x ..< fillStart + fillLen:
let backdrop = image.getRgbaUnsafe(x, y)
image.setRgbaUnsafe(x, y, blender(backdrop, rgbx))
if blendMode == bmMask: if blendMode == bmMask:
image.clearUnsafe(0, y, startX, y) image.clearUnsafe(0, y, startX, y)
image.clearUnsafe(x, y, image.width, y) image.clearUnsafe(filledTo, y, image.width, y)
proc fillHits( proc fillHits(
mask: Mask, mask: Mask,
startX, y: int, startX, y: int,
hits: seq[(float32, int16)], hits: seq[(float32, int16)],
numHits: int, numHits: int,
windingRule: WindingRule windingRule: WindingRule,
blendMode: BlendMode
) = ) =
let masker = blendMode.masker()
var filledTo: int
for (prevAt, at, count) in hits.walk(numHits, windingRule, y, mask.wh): for (prevAt, at, count) in hits.walk(numHits, windingRule, y, mask.wh):
let let
fillStart = prevAt.int fillStart = prevAt.int
fillLen = at.int - fillStart fillLen = at.int - fillStart
if fillLen > 0: if fillLen <= 0:
continue
filledTo = fillStart + fillLen
if blendMode == bmNormal:
fillUnsafe(mask.data, 255, mask.dataIndex(fillStart, y), fillLen) fillUnsafe(mask.data, 255, mask.dataIndex(fillStart, y), fillLen)
continue
var x = fillStart
when defined(amd64) and not defined(pixieNoSimd):
if blendMode.hasSimdMasker():
let
maskerSimd = blendMode.maskerSimd()
vValue = mm_set1_epi8(cast[int8](255))
for _ in countup(fillStart, fillLen - 16, 16):
let backdrop = mm_loadu_si128(mask.data[mask.dataIndex(x, y)].addr)
mm_storeu_si128(
mask.data[mask.dataIndex(x, y)].addr,
maskerSimd(backdrop, vValue)
)
x += 16
for x in x ..< fillStart + fillLen:
let backdrop = mask.getValueUnsafe(x, y)
mask.setValueUnsafe(x, y, masker(backdrop, 255))
if blendMode == bmMask:
mask.clearUnsafe(0, y, startX, y)
mask.clearUnsafe(filledTo, y, mask.width, y)
proc fillShapes( proc fillShapes(
image: Image, image: Image,
@ -1460,7 +1516,12 @@ proc fillShapes(
image.clearUnsafe(0, 0, 0, startY) image.clearUnsafe(0, 0, 0, startY)
image.clearUnsafe(0, pathHeight, 0, image.height) image.clearUnsafe(0, pathHeight, 0, image.height)
proc fillShapes(mask: Mask, shapes: seq[seq[Vec2]], windingRule: WindingRule) = proc fillShapes(
mask: Mask,
shapes: seq[seq[Vec2]],
windingRule: WindingRule,
blendMode: BlendMode
) =
# Figure out the total bounds of all the shapes, # Figure out the total bounds of all the shapes,
# rasterize only within the total bounds # rasterize only within the total bounds
let let
@ -1469,8 +1530,7 @@ proc fillShapes(mask: Mask, shapes: seq[seq[Vec2]], windingRule: WindingRule) =
bounds = computePixelBounds(segments) bounds = computePixelBounds(segments)
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(mask.height, (bounds.y + bounds.h).int) pathHeight = min(mask.height, (bounds.y + bounds.h).int)
pathHeight = stopY - startY
partitioning = partitionSegments(segments, startY, pathHeight) partitioning = partitionSegments(segments, startY, pathHeight)
var var
@ -1478,7 +1538,7 @@ proc fillShapes(mask: Mask, shapes: seq[seq[Vec2]], windingRule: WindingRule) =
hits = newSeq[(float32, int16)](4) hits = newSeq[(float32, int16)](4)
numHits: int numHits: int
for y in startY ..< stopY: for y in startY ..< pathHeight:
computeCoverages( computeCoverages(
coverages, coverages,
hits, hits,
@ -1490,9 +1550,13 @@ proc fillShapes(mask: Mask, shapes: seq[seq[Vec2]], windingRule: WindingRule) =
windingRule windingRule
) )
if aa: if aa:
mask.fillCoverage(startX, y, coverages) mask.fillCoverage(startX, y, coverages, blendMode)
else: else:
mask.fillHits(startX, y, hits, numHits, windingRule) mask.fillHits(startX, y, hits, numHits, windingRule, blendMode)
if blendMode == bmMask:
mask.clearUnsafe(0, 0, 0, startY)
mask.clearUnsafe(0, pathHeight, 0, mask.height)
proc miterLimitToAngle*(limit: float32): float32 = proc miterLimitToAngle*(limit: float32): float32 =
## Converts miter-limit-ratio to miter-limit-angle. ## Converts miter-limit-ratio to miter-limit-angle.
@ -1668,12 +1732,13 @@ proc fillPath*(
mask: Mask, mask: Mask,
path: SomePath, path: SomePath,
transform: Vec2 | Mat3 = vec2(), transform: Vec2 | Mat3 = vec2(),
windingRule = wrNonZero windingRule = wrNonZero,
blendMode = bmNormal
) = ) =
## Fills a path. ## Fills a path.
var shapes = parseSomePath(path, true, transform.pixelScale()) var shapes = parseSomePath(path, true, transform.pixelScale())
shapes.transform(transform) shapes.transform(transform)
mask.fillShapes(shapes, windingRule) mask.fillShapes(shapes, windingRule, blendMode)
proc fillPath*( proc fillPath*(
image: Image, image: Image,
@ -1721,7 +1786,8 @@ proc strokePath*(
lineCap = lcButt, lineCap = lcButt,
lineJoin = ljMiter, lineJoin = ljMiter,
miterLimit = defaultMiterLimit, miterLimit = defaultMiterLimit,
dashes: seq[float32] = @[] dashes: seq[float32] = @[],
blendMode = bmNormal
) = ) =
## Strokes a path. ## Strokes a path.
var strokeShapes = strokeShapes( var strokeShapes = strokeShapes(
@ -1733,7 +1799,7 @@ proc strokePath*(
dashes dashes
) )
strokeShapes.transform(transform) strokeShapes.transform(transform)
mask.fillShapes(strokeShapes, wrNonZero) mask.fillShapes(strokeShapes, wrNonZero, blendMode)
proc strokePath*( proc strokePath*(
image: Image, image: Image,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 B

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 B

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 207 B

View file

@ -415,23 +415,23 @@ block:
block: block:
let mask = newMask(100, 100) let mask = newMask(100, 100)
mask.fillPath("M 10 10 H 60 V 60 H 10 z") mask.fillPath("M 10 10 H 60 V 60 H 10 z")
mask.fillPath("M 30 30 H 80 V 80 H 30 z")#, blendMode = bmExcludeMask) mask.fillPath("M 30 30 H 80 V 80 H 30 z", blendMode = bmExcludeMask)
writeFile("tests/images/paths/maskRectExcludeMask.png", mask.encodePng()) writeFile("tests/images/paths/maskRectExcludeMask.png", mask.encodePng())
block: block:
let mask = newMask(100, 100) let mask = newMask(100, 100)
mask.fillPath("M 10.1 10.1 H 60.1 V 60.1 H 10.1 z") mask.fillPath("M 10.1 10.1 H 60.1 V 60.1 H 10.1 z")
mask.fillPath("M 30.1 30.1 H 80.1 V 80.1 H 30.1 z")#, blendMode = bmExcludeMask) mask.fillPath("M 30.1 30.1 H 80.1 V 80.1 H 30.1 z", blendMode = bmExcludeMask)
writeFile("tests/images/paths/maskRectExcludeMaskAA.png", mask.encodePng()) writeFile("tests/images/paths/maskRectExcludeMaskAA.png", mask.encodePng())
block: block:
let mask = newMask(100, 100) let mask = newMask(100, 100)
mask.fillPath("M 10 10 H 60 V 60 H 10 z") mask.fillPath("M 10 10 H 60 V 60 H 10 z")
mask.fillPath("M 30 30 H 80 V 80 H 30 z")#, blendMode = bmMask) mask.fillPath("M 30 30 H 80 V 80 H 30 z", blendMode = bmMask)
writeFile("tests/images/paths/maskRectMask.png", mask.encodePng()) writeFile("tests/images/paths/maskRectMask.png", mask.encodePng())
block: block:
let mask = newMask(100, 100) let mask = newMask(100, 100)
mask.fillPath("M 10.1 10.1 H 60.1 V 60.1 H 10.1 z") mask.fillPath("M 10.1 10.1 H 60.1 V 60.1 H 10.1 z")
mask.fillPath("M 30.1 30.1 H 80.1 V 80.1 H 30.1 z")#, blendMode = bmMask) mask.fillPath("M 30.1 30.1 H 80.1 V 80.1 H 30.1 z", blendMode = bmMask)
writeFile("tests/images/paths/maskRectMaskAA.png", mask.encodePng()) writeFile("tests/images/paths/maskRectMaskAA.png", mask.encodePng())