diff --git a/src/pixie/images.nim b/src/pixie/images.nim index 7ca2b4f..8662ef6 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -1,4 +1,4 @@ -import strformat, chroma, vmath +import strformat, chroma, chroma/blends, vmath type Image* = ref object @@ -70,3 +70,207 @@ proc fill*(image: Image, rgba: ColorRgba) = ## Fills the image with a solid color. for i in 0 ..< image.data.len: image.data[i] = rgba + +proc subImage*(image: Image, x, y, w, h: int): Image = + ## Gets a sub image of the main image. + doAssert x >= 0 and y >= 0 + doAssert x + w <= image.width and y + h <= image.height + result = newImage(w, h) + for y2 in 0 ..< h: + for x2 in 0 ..< w: + result.setRgbaUnsafe(x2, y2, image.getRgbaUnsafe(x2 + x, y2 + y)) + +proc minifyBy2*(image: Image): Image = + ## Scales the image down by an integer scale. + result = newImage(image.width div 2, image.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) + +proc blitUnsafe*(destImage: Image, srcImage: Image, src, dest: Rect) = + ## Blits rectangle from one image to the other image. + ## * No bounds checking * + ## Make sure that src and dest rect are in bounds. + ## Make sure that channels for images are the same. + ## Failure in the assumptions will case unsafe memory writes. + ## Note: Does not do alpha or color mixing. + for y in 0 ..< int(dest.h): + let + srcIdx = int(src.x) + (int(src.y) + y) * srcImage.width + destIdx = int(dest.x) + (int(dest.y) + y) * destImage.width + copyMem( + destImage.data[destIdx].addr, + srcImage.data[srcIdx].addr, + int(dest.w) * 4 + ) + +proc blit*(destImage: Image, srcImage: Image, src, dest: Rect) = + ## Blits rectangle from one image to the other image. + ## Note: Does not do alpha or color mixing. + doAssert src.w == dest.w and src.h == dest.h + doAssert src.x >= 0 and src.x + src.w <= srcImage.width.float32 + doAssert src.y >= 0 and src.y + src.h <= srcImage.height.float32 + + # See if the image hits the bounds and needs to be adjusted. + var + src = src + dest = dest + if dest.x < 0: + dest.w += dest.x + src.x -= dest.x + src.w += dest.x + dest.x = 0 + if dest.x + dest.w > destImage.width.float32: + let diff = destImage.width.float32 - (dest.x + dest.w) + dest.w += diff + src.w += diff + if dest.y < 0: + dest.h += dest.y + src.y -= dest.y + src.h += dest.y + dest.y = 0 + if dest.y + dest.h > destImage.height.float32: + let diff = destImage.height.float32 - (dest.y + dest.h) + dest.h += diff + src.h += diff + + # See if image is entirely outside the bounds: + if dest.x + dest.w < 0 or dest.x > destImage.width.float32: + return + if dest.y + dest.h < 0 or dest.y > destImage.height.float32: + return + + blitUnsafe(destImage, srcImage, src, dest) + +proc blit*(destImage: Image, srcImage: Image, pos: Vec2) = + ## Blits rectangle from one image to the other image. + ## Note: Does not do alpha or color mixing. + destImage.blit( + srcImage, + rect(0.0, 0.0, srcImage.width.float32, srcImage.height.float32), + rect(pos.x, pos.y, srcImage.width.float32, srcImage.height.float32) + ) + +func moduloMod(n, M: int): int {.inline.} = + ## Computes "mathematical" modulo vs c modulo. + ((n mod M) + M) mod M + +func lerp(a, b: Color, v: float): Color {.inline.} = + result.r = lerp(a.r, b.r, v) + result.g = lerp(a.g, b.g, v) + result.b = lerp(a.b, b.b, v) + result.a = lerp(a.a, b.a, v) + +proc getRgbaSmooth*(image: Image, x, y: float64): ColorRGBA + {.inline, raises: [].} = + ## Gets a pixel as (x, y) floats. + let + minX = floor(x).int + difX = (x - minX.float32) + + minY = floor(y).int + difY = (y - minY.float32) + + vX0Y0 = image.getRgbaUnsafe( + moduloMod(minX, image.width), + moduloMod(minY, image.height), + ).color() + + vX1Y0 = image.getRgbaUnsafe( + moduloMod(minX + 1, image.width), + moduloMod(minY, image.height), + ).color() + + vX0Y1 = image.getRgbaUnsafe( + moduloMod(minX, image.width), + moduloMod(minY + 1, image.height), + ).color() + + vX1Y1 = image.getRgbaUnsafe( + moduloMod(minX + 1, image.width), + moduloMod(minY + 1, image.height), + ).color() + + bottomMix = lerp(vX0Y0, vX1Y0, difX) + topMix = lerp(vX0Y1, vX1Y1, difX) + finalMix = lerp(bottomMix, topMix, difY) + + return finalMix.rgba() + +proc draw*(destImage: Image, srcImage: Image, mat: Mat4, blendMode = Normal) = + ## Draws one image onto another using matrix with color blending. + var srcImage = srcImage + let + matInv = mat.inverse() + bounds = [ + mat * vec3(-1, -1, 0), + mat * vec3(-1, float32 srcImage.height + 1, 0), + mat * vec3(float32 srcImage.width + 1, -1, 0), + mat * vec3(float32 srcImage.width + 1, float32 srcImage.height + 1, 0) + ] + var + boundsX: array[4, float32] + boundsY: array[4, float32] + for i, v in bounds: + boundsX[i] = v.x + boundsY[i] = v.y + let + xStart = max(int min(boundsX), 0) + yStart = max(int min(boundsY), 0) + xEnd = min(int max(boundsX), destImage.width) + yEnd = min(int max(boundsY), destImage.height) + + var + # compute movement vectors + start = matInv * vec3(0.5, 0.5, 0) + stepX = matInv * vec3(1.5, 0.5, 0) - start + stepY = matInv * vec3(0.5, 1.5, 0) - start + + minFilterBy2 = max(stepX.length, stepY.length) + + while minFilterBy2 > 2.0: + srcImage = srcImage.minifyBy2() + start /= 2 + stepX /= 2 + stepY /= 2 + minFilterBy2 /= 2 + + # fill the bounding rectangle + for y in yStart ..< yEnd: + for x in xStart ..< xEnd: + let srcV = start + stepX * float32(x) + stepY * float32(y) + if srcImage.inside(int srcV.x, int srcV.y): + let + srcRgba = srcImage.getRgbaSmooth(srcV.x - 0.5, srcV.y - 0.5) + let + destRgba = destImage.getRgbaUnsafe(x, y) + color = blendMode.mix(destRgba.color, srcRgba.color) + destImage.setRgbaUnsafe(x, y, color.rgba) + +proc draw*( + destImage, srcImage: Image, pos = vec2(0, 0), blendMode = Normal, +) = + ## Fast draw of dest + fill using offset with color blending. + for y in 0 ..< srcImage.height: + for x in 0 ..< srcImage.width: + let + srcRgba = srcImage.getRgbaUnsafe(x, y) + if blendMode == Mask or srcRgba.a > 0 : + let + destRgba = destImage.getRgbaUnsafe(x + pos.x.int, y + pos.y.int) + rgba = blendMode.mix(destRgba, srcRgba) + # TODO: Make unsafe + destImage[x + pos.x.int, y + pos.y.int] = rgba + +proc invert*(image: Image) = + ## Inverts all of the colors and alpha. + for rgba in image.data.mitems: + rgba.r = 255 - rgba.r + rgba.g = 255 - rgba.g + rgba.b = 255 - rgba.b + rgba.a = 255 - rgba.a diff --git a/src/pixie/masks.nim b/src/pixie/masks.nim index d9119b5..009d8d3 100644 --- a/src/pixie/masks.nim +++ b/src/pixie/masks.nim @@ -53,3 +53,8 @@ proc fill*(mask: Mask, alpha: uint8) = ## Fills the mask with a solid color. for i in 0 ..< mask.data.len: mask.data[i] = alpha + +proc invert*(mask: Mask) = + ## Inverts all of the colors and alpha. + for alpha in mask.data.mitems: + alpha = 255 - alpha diff --git a/tests/testimages.nim b/tests/testimages.nim new file mode 100644 index 0000000..87c9e33 --- /dev/null +++ b/tests/testimages.nim @@ -0,0 +1,11 @@ +import pixie, chroma + +block: + var image = newImage(10, 10) + image[0, 0] = rgba(255, 255, 255, 255) + doAssert image[0, 0] == rgba(255, 255, 255, 255) + +block: + var image = newImage(10, 10) + image.fill(rgba(255, 0, 0, 255)) + doAssert image[0, 0] == rgba(255, 0, 0, 255)