diff --git a/pixie.nimble b/pixie.nimble index 973ada1..01de21a 100644 --- a/pixie.nimble +++ b/pixie.nimble @@ -1,4 +1,4 @@ -version = "2.0.5" +version = "2.1.0" author = "Andre von Houck and Ryan Oldenburg" description = "Full-featured 2d graphics library for Nim." license = "MIT" diff --git a/src/pixie/blends.nim b/src/pixie/blends.nim index 3fdd211..5e5b2c7 100644 --- a/src/pixie/blends.nim +++ b/src/pixie/blends.nim @@ -435,9 +435,6 @@ proc blendSubtractMask(backdrop, source: ColorRGBX): ColorRGBX = result.b = ((backdrop.b * a) div 255).uint8 result.a = a.uint8 -proc blendIntersectMask(backdrop, source: ColorRGBX): ColorRGBX = - blendMask(backdrop, source) - proc blendExcludeMask(backdrop, source: ColorRGBX): ColorRGBX = let a = max(backdrop.a, source.a).uint32 - min(backdrop.a, source.a) result.r = ((source.r * a) div 255).uint8 @@ -489,9 +486,6 @@ proc maskMask(backdrop, source: uint8): 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) @@ -592,7 +586,7 @@ when defined(amd64) and not defined(pixieNoSimd): div255 = mm_set1_epi16(cast[int16](0x8081)) var - sourceEven = mm_slli_epi16(mm_andnot_si128(oddMask, source), 8) + sourceEven = mm_slli_epi16(source, 8) sourceOdd = mm_and_si128(source, oddMask) let @@ -600,7 +594,7 @@ when defined(amd64) and not defined(pixieNoSimd): oddK = mm_sub_epi16(v255high, sourceOdd) var - backdropEven = mm_slli_epi16(mm_andnot_si128(oddMask, backdrop), 8) + backdropEven = mm_slli_epi16(backdrop, 8) backdropOdd = mm_and_si128(backdrop, oddMask) # backdrop * k @@ -625,11 +619,11 @@ when defined(amd64) and not defined(pixieNoSimd): let oddMask = mm_set1_epi16(cast[int16](0xff00)) 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) var - backdropEven = mm_slli_epi16(mm_andnot_si128(oddMask, backdrop), 8) + backdropEven = mm_slli_epi16(backdrop, 8) backdropOdd = mm_and_si128(backdrop, oddMask) # backdrop * source diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index fbeccea..124dd5b 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -1137,10 +1137,12 @@ iterator walk( # between zero and nonzero (or the last hit) count += winding continue - if at > 0: - if shouldFill(windingRule, count): - yield (prevAt, at, count) - prevAt = at + if at <= 0: + count += winding + continue + if shouldFill(windingRule, count): + yield (prevAt, at, count) + prevAt = at count += winding when defined(pixieLeakCheck): @@ -1226,13 +1228,15 @@ proc computeCoverages( for j in i ..< fillStart + fillLen: coverages[j] += sampleCoverage -proc clearUnsafe(image: Image, startX, startY, toX, toY: int) = - ## From startXY to toXY, exclusive, toXY is not cleared. - image.data.fillUnsafe( - rgbx(0, 0, 0, 0), - image.dataIndex(startX, startY), - image.dataIndex(toX, toY) - image.dataIndex(startX, startY) - ) +proc clearUnsafe(target: Image | Mask, startX, startY, toX, toY: int) = + ## Clears data from [start, to). + let + start = target.dataIndex(startX, startY) + len = target.dataIndex(toX, toY) - start + 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( image: Image, @@ -1258,7 +1262,7 @@ proc fillCoverage( let index = image.dataIndex(x, y) 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 mm_movemask_epi8(mm_cmpeq_epi32(coverage, first32)) == 0xffff: # Coverages are all 255 @@ -1294,6 +1298,8 @@ proc fillCoverage( image.data[index].addr, blenderSimd(backdrop, source) ) + elif blendMode == bmMask: + mm_storeu_si128(image.data[index].addr, mm_setzero_si128()) x += 4 let blender = blendMode.blender() @@ -1319,31 +1325,45 @@ proc fillCoverage( if blendMode == bmMask: 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 when defined(amd64) and not defined(pixieNoSimd): - # When supported, SIMD blend as much as possible - let maskerSimd = bmNormal.maskerSimd() - for _ in countup(x, coverages.len - 16, 16): - let - coverage = mm_loadu_si128(coverages[x].unsafeAddr) - eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128()) - if mm_movemask_epi8(eqZero) != 0xffff: - # If the coverages are not all zero - let backdrop = mm_loadu_si128(mask.data[mask.dataIndex(x, y)].addr) - mm_storeu_si128( - mask.data[mask.dataIndex(x, y)].addr, - maskerSimd(backdrop, coverage) - ) - x += 16 + if blendMode.hasSimdMasker(): + let maskerSimd = blendMode.maskerSimd() + for _ in countup(x, coverages.len - 16, 16): + let + index = mask.dataIndex(x, y) + coverage = mm_loadu_si128(coverages[x].unsafeAddr) + eqZero = mm_cmpeq_epi16(coverage, mm_setzero_si128()) + if mm_movemask_epi8(eqZero) != 0xffff: # or blendMode == bmExcludeMask: + # If the coverages are not all zero + let backdrop = mm_loadu_si128(mask.data[index].addr) + mm_storeu_si128( + mask.data[index].addr, + maskerSimd(backdrop, coverage) + ) + elif blendMode == bmMask: + mm_storeu_si128(mask.data[index].addr, mm_setzero_si128()) + x += 16 + let masker = blendMode.masker() while x < mask.width: let coverage = coverages[x] - if coverage != 0: + if coverage != 0 or blendMode == bmExcludeMask: 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 + if blendMode == bmMask: + mask.clearUnsafe(0, y, startX, y) + proc fillHits( image: Image, rgbx: ColorRGBX, @@ -1354,53 +1374,89 @@ proc fillHits( blendMode: BlendMode ) = let blender = blendMode.blender() - var x = 0 + var filledTo: int for (prevAt, at, count) in hits.walk(numHits, windingRule, y, image.wh): let fillStart = prevAt.int fillLen = at.int - fillStart - if fillLen > 0: - if blendMode == bmNormal and rgbx.a == 255: - fillUnsafe(image.data, rgbx, image.dataIndex(fillStart, y), fillLen) - else: - x = fillStart - when defined(amd64) and not defined(pixieNoSimd): - if blendMode.hasSimdBlender(): - # When supported, SIMD blend as much as possible - let - blenderSimd = blendMode.blenderSimd() - vColor = mm_set1_epi32(cast[int32](rgbx)) - for _ in countup(fillStart, fillLen - 16, 4): - let - index = image.dataIndex(x, y) - backdrop = mm_loadu_si128(image.data[index].addr) - mm_storeu_si128( - image.data[index].addr, - blenderSimd(backdrop, vColor) - ) - x += 4 - while x < fillStart + fillLen: - let backdrop = image.getRgbaUnsafe(x, y) - image.setRgbaUnsafe(x, y, blender(backdrop, rgbx)) - inc x + if fillLen <= 0: + continue + + filledTo = fillStart + fillLen + + if blendMode == bmNormal and rgbx.a == 255: + fillUnsafe(image.data, rgbx, image.dataIndex(fillStart, y), fillLen) + continue + + var x = fillStart + when defined(amd64) and not defined(pixieNoSimd): + if blendMode.hasSimdBlender(): + # When supported, SIMD blend as much as possible + let + blenderSimd = blendMode.blenderSimd() + vColor = mm_set1_epi32(cast[int32](rgbx)) + for _ in countup(fillStart, fillLen - 16, 4): + let + index = image.dataIndex(x, y) + backdrop = mm_loadu_si128(image.data[index].addr) + mm_storeu_si128( + image.data[index].addr, + blenderSimd(backdrop, vColor) + ) + x += 4 + + for x in x ..< fillStart + fillLen: + let backdrop = image.getRgbaUnsafe(x, y) + image.setRgbaUnsafe(x, y, blender(backdrop, rgbx)) if blendMode == bmMask: image.clearUnsafe(0, y, startX, y) - image.clearUnsafe(x, y, image.width, y) + image.clearUnsafe(filledTo, y, image.width, y) proc fillHits( mask: Mask, startX, y: int, hits: seq[(float32, int16)], 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): let fillStart = prevAt.int 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) + 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( image: Image, @@ -1460,7 +1516,12 @@ proc fillShapes( image.clearUnsafe(0, 0, 0, startY) 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, # rasterize only within the total bounds let @@ -1469,8 +1530,7 @@ proc fillShapes(mask: Mask, shapes: seq[seq[Vec2]], windingRule: WindingRule) = bounds = computePixelBounds(segments) startX = max(0, bounds.x.int) startY = max(0, bounds.y.int) - stopY = min(mask.height, (bounds.y + bounds.h).int) - pathHeight = stopY - startY + pathHeight = min(mask.height, (bounds.y + bounds.h).int) partitioning = partitionSegments(segments, startY, pathHeight) var @@ -1478,7 +1538,7 @@ proc fillShapes(mask: Mask, shapes: seq[seq[Vec2]], windingRule: WindingRule) = hits = newSeq[(float32, int16)](4) numHits: int - for y in startY ..< stopY: + for y in startY ..< pathHeight: computeCoverages( coverages, hits, @@ -1490,9 +1550,13 @@ proc fillShapes(mask: Mask, shapes: seq[seq[Vec2]], windingRule: WindingRule) = windingRule ) if aa: - mask.fillCoverage(startX, y, coverages) + mask.fillCoverage(startX, y, coverages, blendMode) 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 = ## Converts miter-limit-ratio to miter-limit-angle. @@ -1668,12 +1732,13 @@ proc fillPath*( mask: Mask, path: SomePath, transform: Vec2 | Mat3 = vec2(), - windingRule = wrNonZero + windingRule = wrNonZero, + blendMode = bmNormal ) = ## Fills a path. var shapes = parseSomePath(path, true, transform.pixelScale()) shapes.transform(transform) - mask.fillShapes(shapes, windingRule) + mask.fillShapes(shapes, windingRule, blendMode) proc fillPath*( image: Image, @@ -1721,7 +1786,8 @@ proc strokePath*( lineCap = lcButt, lineJoin = ljMiter, miterLimit = defaultMiterLimit, - dashes: seq[float32] = @[] + dashes: seq[float32] = @[], + blendMode = bmNormal ) = ## Strokes a path. var strokeShapes = strokeShapes( @@ -1733,7 +1799,7 @@ proc strokePath*( dashes ) strokeShapes.transform(transform) - mask.fillShapes(strokeShapes, wrNonZero) + mask.fillShapes(strokeShapes, wrNonZero, blendMode) proc strokePath*( image: Image, diff --git a/tests/images/paths/maskRectExcludeMask.png b/tests/images/paths/maskRectExcludeMask.png new file mode 100644 index 0000000..145429c Binary files /dev/null and b/tests/images/paths/maskRectExcludeMask.png differ diff --git a/tests/images/paths/maskRectExcludeMaskAA.png b/tests/images/paths/maskRectExcludeMaskAA.png new file mode 100644 index 0000000..c28291f Binary files /dev/null and b/tests/images/paths/maskRectExcludeMaskAA.png differ diff --git a/tests/images/paths/maskRectMask.png b/tests/images/paths/maskRectMask.png new file mode 100644 index 0000000..2d7751c Binary files /dev/null and b/tests/images/paths/maskRectMask.png differ diff --git a/tests/images/paths/maskRectMaskAA.png b/tests/images/paths/maskRectMaskAA.png new file mode 100644 index 0000000..079cbc5 Binary files /dev/null and b/tests/images/paths/maskRectMaskAA.png differ diff --git a/tests/test_paths.nim b/tests/test_paths.nim index e0b17e7..e3ab5ec 100644 --- a/tests/test_paths.nim +++ b/tests/test_paths.nim @@ -411,3 +411,27 @@ block: Paint(kind: pkSolid, color: rgbx(0, 255, 0, 255), blendMode: bmMask) ) image.writeFile("tests/images/paths/rectMaskAA.png") + +block: + let mask = newMask(100, 100) + 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) + writeFile("tests/images/paths/maskRectExcludeMask.png", mask.encodePng()) + +block: + 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 30.1 30.1 H 80.1 V 80.1 H 30.1 z", blendMode = bmExcludeMask) + writeFile("tests/images/paths/maskRectExcludeMaskAA.png", mask.encodePng()) + +block: + let mask = newMask(100, 100) + 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) + writeFile("tests/images/paths/maskRectMask.png", mask.encodePng()) + +block: + 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 30.1 30.1 H 80.1 V 80.1 H 30.1 z", blendMode = bmMask) + writeFile("tests/images/paths/maskRectMaskAA.png", mask.encodePng())