diff --git a/src/pixie/common.nim b/src/pixie/common.nim index b9e120c..ab0f957 100644 --- a/src/pixie/common.nim +++ b/src/pixie/common.nim @@ -9,6 +9,10 @@ proc fractional*(v: float32): float32 {.inline.} = result = abs(v) result = result - floor(result) +proc lerp*(a, b: uint8, t: float32): uint8 {.inline.} = + let t = round(t * 255).uint32 + ((a * (255 - t) + b * t) div 255).uint8 + proc lerp*(a, b: ColorRGBA, t: float32): ColorRGBA {.inline.} = let x = round(t * 255).uint32 result.r = ((a.r.uint32 * (255 - x) + b.r.uint32 * x) div 255).uint8 diff --git a/src/pixie/fileformats/svg.nim b/src/pixie/fileformats/svg.nim index 381f4c5..45bdf98 100644 --- a/src/pixie/fileformats/svg.nim +++ b/src/pixie/fileformats/svg.nim @@ -196,17 +196,8 @@ proc draw( rx = parseFloat(node.attr("rx")) ry = parseFloat(node.attr("ry")) - let - magicX = (4.0 * (-1.0 + sqrt(2.0)) / 3) * rx - magicY = (4.0 * (-1.0 + sqrt(2.0)) / 3) * ry - var path: Path - path.moveTo(cx + rx, cy) - path.bezierCurveTo(cx + rx, cy + magicY, cx + magicX, cy + ry, cx, cy + ry) - path.bezierCurveTo(cx - magicX, cy + ry, cx - rx, cy + magicY, cx - rx, cy) - path.bezierCurveTo(cx - rx, cy - magicY, cx - magicX, cy - ry, cx, cy - ry) - path.bezierCurveTo(cx + magicX, cy - ry, cx + rx, cy - magicY, cx + rx, cy) - path.closePath() + path.ellipse(cx, cy, rx, ry) if ctx.fill != ColorRGBA(): img.fillPath(path, ctx.fill, ctx.transform) diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 95bf711..191d12d 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -1,4 +1,4 @@ -import chroma, blends, bumpy, vmath, common, system/memory +import chroma, blends, bumpy, vmath, common, system/memory, masks when defined(amd64) and not defined(pixieNoSimd): import nimsimd/sse2 @@ -263,6 +263,48 @@ proc toStraightAlpha*(image: Image) = c.g = ((c.g.uint32 * multiplier) div 255).uint8 c.b = ((c.b.uint32 * multiplier) div 255).uint8 +proc drawCorrect*(image: Image, mask: Mask, mat = mat3(), blendMode = bmMask) = + if blendMode notin {bmMask}: + raise newException( + PixieError, + "Blend mode " & $blendMode & " not supported for masks" + ) + + var + matInv = mat.inverse() + mask = mask + + block: # Shrink mask by 2 as needed + var + dx = matInv * vec2(1, 0) + dy = matInv * vec2(0, 1) + while max(dx.length, dy.length) > 2: + mask = mask.minifyBy2() + dx /= 2 + dy /= 2 + matInv = matInv * scale(vec2(0.5, 0.5)) + + for y in 0 ..< image.height: + for x in 0 ..< image.width: + let + maskPos = matInv * vec2(x.float32 + h, y.float32 + h) + xFloat = maskPos.x - h + yFloat = maskPos.y - h + value = mask.getValueSmooth(xFloat, yFloat).uint32 + rgba = image.getRgbaUnsafe(x, y) + blended = rgba( + ((rgba.r * value) div 255).uint8, + ((rgba.g * value) div 255).uint8, + ((rgba.b * value) div 255).uint8, + ((rgba.a * value) div 255).uint8 + ) + image.setRgbaUnsafe(x, y, blended) + +proc draw*( + image: Image, mask: Mask, pos = vec2(0, 0), blendMode = bmMask +) {.inline.} = + image.drawCorrect(mask, translate(pos), blendMode) + when defined(release): {.pop.} @@ -286,15 +328,17 @@ proc invert*(image: Image) = proc getRgbaSmooth*(image: Image, x, y: float32): ColorRGBA {.inline.} = let - minX = x.floor.int - diffX = x - x.floor - minY = y.floor.int - diffY = y - y.floor + minX = floor(x) + minY = floor(y) + diffX = x - minX + diffY = y - minY + x = minX.int + y = minY.int - x0y0 = image[minX, minY].toPremultipliedAlpha() - x1y0 = image[minX + 1, minY].toPremultipliedAlpha() - x0y1 = image[minX, minY + 1].toPremultipliedAlpha() - x1y1 = image[minX + 1, minY + 1].toPremultipliedAlpha() + x0y0 = image[x + 0, y + 0].toPremultipliedAlpha() + x1y0 = image[x + 1, y + 0].toPremultipliedAlpha() + x0y1 = image[x + 0, y + 1].toPremultipliedAlpha() + x1y1 = image[x + 1, y + 1].toPremultipliedAlpha() bottomMix = lerp(x0y0, x1y0, diffX) topMix = lerp(x0y1, x1y1, diffX) @@ -407,24 +451,21 @@ proc sharpOpacity*(image: Image) = else: rgba = rgba(255, 255, 255, 255) -proc drawCorrect*(a, b: Image, mat: Mat3, blendMode: BlendMode) = +proc drawCorrect*(a, b: Image, mat = mat3(), blendMode = bmNormal) = ## Draws one image onto another using matrix with color blending. var matInv = mat.inverse() - # Compute movement vectors - p = matInv * vec2(0 + h, 0 + h) - dx = matInv * vec2(1 + h, 0 + h) - p - dy = matInv * vec2(0 + h, 1 + h) - p - minFilterBy2 = max(dx.length, dy.length) b = b - while minFilterBy2 > 2.0: - b = b.minifyBy2() - p /= 2 - dx /= 2 - dy /= 2 - minFilterBy2 /= 2 - matInv = matInv * scale(vec2(0.5, 0.5)) + block: # Shrink image by 2 as needed + var + dx = matInv * vec2(1, 0) + dy = matInv * vec2(0, 1) + while max(dx.length, dy.length) > 2: + b = b.minifyBy2() + dx /= 2 + dy /= 2 + matInv = matInv * scale(vec2(0.5, 0.5)) let blender = blendMode.blender() for y in 0 ..< a.height: diff --git a/src/pixie/masks.nim b/src/pixie/masks.nim index 13a95f3..c600409 100644 --- a/src/pixie/masks.nim +++ b/src/pixie/masks.nim @@ -1,4 +1,4 @@ -import common, vmath +import common, vmath, system/memory type Mask* = ref object @@ -81,5 +81,39 @@ proc minifyBy2*(mask: Mask, power = 1): Mask = mask.getValueUnsafe(x * 2 + 0, y * 2 + 1) result.setValueUnsafe(x, y, (value div 4).uint8) +proc fillUnsafe(data: var seq[uint8], value: uint8, start, len: int) = + ## Fills the mask data with the parameter value starting at index start and + ## continuing for len indices. + nimSetMem(data[start].addr, value.cint, len) + +proc fill*(mask: Mask, value: uint8) {.inline.} = + ## Fills the mask with the parameter value. + fillUnsafe(mask.data, value, 0, mask.data.len) + +proc getValueSmooth*(mask: Mask, x, y: float32): uint8 = + let + minX = floor(x) + minY = floor(y) + diffX = x - minX + diffY = y - minY + x = minX.int + y = minY.int + + x0y0 = mask[x + 0, y + 0] + x1y0 = mask[x + 1, y + 0] + x0y1 = mask[x + 0, y + 1] + x1y1 = mask[x + 1, y + 1] + + bottomMix = lerp(x0y0, x1y0, diffX) + topMix = lerp(x0y1, x1y1, diffX) + + lerp(bottomMix, topMix, diffY) + +proc applyOpacity*(mask: Mask, opacity: float32) = + ## Multiplies the values of the mask by opacity. + let opacity = round(255 * opacity).uint32 + for value in mask.data.mitems: + value = ((value * opacity) div 255).uint8 + when defined(release): {.pop.} diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 4cef0f3..d0895b6 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -363,6 +363,18 @@ proc rect*(path: var Path, x, y, w, h: float32) = proc rect*(path: var Path, pos: Vec2, wh: Vec2) {.inline.} = path.rect(pos.x, pos.y, wh.x, wh.y) +proc ellipse*(path: var Path, cx, cy, rx, ry: float32) = + let + magicX = (4.0 * (-1.0 + sqrt(2.0)) / 3) * rx + magicY = (4.0 * (-1.0 + sqrt(2.0)) / 3) * ry + + path.moveTo(cx + rx, cy) + path.bezierCurveTo(cx + rx, cy + magicY, cx + magicX, cy + ry, cx, cy + ry) + path.bezierCurveTo(cx - magicX, cy + ry, cx - rx, cy + magicY, cx - rx, cy) + path.bezierCurveTo(cx - rx, cy - magicY, cx - magicX, cy - ry, cx, cy - ry) + path.bezierCurveTo(cx + magicX, cy - ry, cx + rx, cy - magicY, cx + rx, cy) + path.closePath() + 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)) diff --git a/tests/benchmark_images.nim b/tests/benchmark_images.nim index 7864ca7..0d6fc2b 100644 --- a/tests/benchmark_images.nim +++ b/tests/benchmark_images.nim @@ -1,48 +1,80 @@ import chroma, pixie, benchy -let a = newImage(2560, 1440) +let image = newImage(2560, 1440) + +proc reset() = + image.fill(rgba(63, 127, 191, 191)) + +reset() timeIt "fill": - a.fill(rgba(255, 255, 255, 255)) - doAssert a[0, 0] == rgba(255, 255, 255, 255) + image.fill(rgba(255, 255, 255, 255)) + doAssert image[0, 0] == rgba(255, 255, 255, 255) + +reset() timeIt "fill_rgba": - a.fill(rgba(63, 127, 191, 191)) - doAssert a[0, 0] == rgba(63, 127, 191, 191) + image.fill(rgba(63, 127, 191, 191)) + doAssert image[0, 0] == rgba(63, 127, 191, 191) + +reset() timeIt "subImage": - keep a.subImage(0, 0, 256, 256) + keep image.subImage(0, 0, 256, 256) + +reset() # timeIt "superImage": # discard +reset() + timeIt "minifyBy2": - let minified = a.minifyBy2() + let minified = image.minifyBy2() doAssert minified[0, 0] == rgba(63, 127, 191, 191) +reset() + timeIt "invert": - a.invert() - keep(a) + image.invert() + +reset() timeIt "applyOpacity": - a.applyOpacity(0.5) - keep(a) + image.applyOpacity(0.5) + +reset() timeIt "sharpOpacity": - a.sharpOpacity() - keep(a) + image.sharpOpacity() -a.fill(rgba(63, 127, 191, 191)) +reset() timeIt "toPremultipliedAlpha": - a.toPremultipliedAlpha() + image.toPremultipliedAlpha() + +reset() timeIt "toStraightAlpha": - a.toStraightAlpha() + image.toStraightAlpha() + +reset() + +block: + var path: Path + path.ellipse(image.width / 2, image.height / 2, 300, 300) + + let mask = newMask(image.width, image.height) + mask.fillPath(path) + + timeIt "mask": + image.mask(mask) + +reset() timeIt "lerp integers": for i in 0 ..< 100000: - let c = a[0, 0] + let c = image[0, 0] var z: int for t in 0 .. 100: z += lerp(c, c, t.float32 / 100).a.int @@ -50,7 +82,7 @@ timeIt "lerp integers": timeIt "lerp floats": for i in 0 ..< 100000: - let c = a[0, 0] + let c = image[0, 0] var z: int for t in 0 .. 100: z += lerp(c.color, c.color, t.float32 / 100).rgba().a.int diff --git a/tests/benchmark_masks.nim b/tests/benchmark_masks.nim new file mode 100644 index 0000000..a359c4a --- /dev/null +++ b/tests/benchmark_masks.nim @@ -0,0 +1,14 @@ +import chroma, pixie, benchy + +let mask = newMask(2560, 1440) + +proc reset() = + mask.fill(63) + +reset() + +timeIt "minifyBy2": + let minified = mask.minifyBy2() + doAssert minified[0, 0] == 63 + +reset() diff --git a/tests/images/circleMask.png b/tests/images/circleMask.png new file mode 100644 index 0000000..89de309 Binary files /dev/null and b/tests/images/circleMask.png differ diff --git a/tests/test_images.nim b/tests/test_images.nim index 6bf3ff2..6f02f09 100644 --- a/tests/test_images.nim +++ b/tests/test_images.nim @@ -91,3 +91,17 @@ block: a = readImage("tests/images/flipped1.png") b = a.minifyBy2(2) b.writeFile("tests/images/minifiedBy4.png") + +block: + let image = newImage(100, 100) + image.fill(rgba(255, 100, 100, 255)) + + var path: Path + path.ellipse(image.width / 2, image.height / 2, 25, 25) + + let mask = newMask(image.width, image.height) + mask.fillPath(path) + + image.draw(mask) + image.toStraightAlpha() + image.writeFile("tests/images/circleMask.png") diff --git a/tests/test_masks.nim b/tests/test_masks.nim index c5449b3..6c261df 100644 --- a/tests/test_masks.nim +++ b/tests/test_masks.nim @@ -16,4 +16,8 @@ block: path.arcTo(x, y, x + w, y, r) mask.fillPath(path) - writeFile("tests/images/masks/maskMinified.png", mask.minifyBy2().encodePng()) + let minified = mask.minifyBy2() + + doAssert minified.width == 50 and minified.height == 50 + + writeFile("tests/images/masks/maskMinified.png", minified.encodePng())