diff --git a/src/pixie/blends.nim b/src/pixie/blends.nim index ed26fc1..31573e3 100644 --- a/src/pixie/blends.nim +++ b/src/pixie/blends.nim @@ -270,3 +270,404 @@ proc mix*(blendMode: BlendMode, dest, src: ColorRGBA): ColorRGBA {.inline.} = # result = target # else: # return blendMode.mix(target.color, blend.color).rgba + +proc multiply(Cb, Cs: float32): float32 {.inline.} = + Cb * Cs + +proc screen(Cb, Cs: float32): float32 {.inline.} = + 1 - (1 - Cb) * (1 - Cs) + +proc hardLight(Cb, Cs: float32): float32 {.inline.} = + if Cs <= 0.5: multiply(Cb, 2 * Cs) + else: screen(Cb, 2 * Cs - 1) + +proc softLight(a, b: float32): float32 {.inline.} = + ## Pegtop + (1 - 2 * b) * a ^ 2 + 2 * b * a + +proc Lum(C: Color): float32 {.inline.} = + 0.3 * C.r + 0.59 * C.g + 0.11 * C.b + +proc ClipColor(C: Color): Color {.inline.} = + let + L = Lum(C) + n = min([C.r, C.g, C.b]) + x = max([C.r, C.g, C.b]) + var + C = C + if n < 0: + C = L + (((C - L) * L) / (L - n)) + if x > 1: + C = L + (((C - L) * (1 - L)) / (x - L)) + return C + +proc SetLum(C: Color, l: float32): Color {.inline.} = + let + d = l - Lum(C) + result.r = C.r + d + result.g = C.g + d + result.b = C.b + d + return ClipColor(result) + +proc Sat(C: Color): float32 {.inline.} = + max([C.r, C.g, C.b]) - min([C.r, C.g, C.b]) + +proc SetSat(C: Color, s: float32): Color {.inline.} = + var arr = [(C.r, 0), (C.g, 1), (C.b, 2)] + # TODO: Don't rely on sort. + arr.sort() + var + Cmin = arr[0][0] + Cmid = arr[1][0] + Cmax = arr[2][0] + if Cmax > Cmin: + Cmid = (((Cmid - Cmin) * s) / (Cmax - Cmin)) + Cmax = s + else: + Cmid = 0 + Cmax = 0 + Cmin = 0 + + if arr[0][1] == 0: + result.r = Cmin + if arr[1][1] == 0: + result.r = Cmid + if arr[2][1] == 0: + result.r = Cmax + + if arr[0][1] == 1: + result.g = Cmin + if arr[1][1] == 1: + result.g = Cmid + if arr[2][1] == 1: + result.g = Cmax + + if arr[0][1] == 2: + result.b = Cmin + if arr[1][1] == 2: + result.b = Cmid + if arr[2][1] == 2: + result.b = Cmax + +proc alphaFix(Cb, Cs, mixed: Color): Color {.inline.} = + let ab = Cb.a + let As = Cs.a + result.r = As * (1 - ab) * Cs.r + As * ab * mixed.r + (1 - As) * ab * Cb.r + result.g = As * (1 - ab) * Cs.g + As * ab * mixed.g + (1 - As) * ab * Cb.g + result.b = As * (1 - ab) * Cs.b + As * ab * mixed.b + (1 - As) * ab * Cb.b + + result.a = (Cs.a + Cb.a * (1.0 - Cs.a)) + result.r /= result.a + result.g /= result.a + result.b /= result.a + +proc blendDarken(Cb, Cs: float32): float32 {.inline.} = + min(Cb, Cs) + +proc blendMultiply(Cb, Cs: float32): float32 {.inline.} = + multiply(Cb, Cs) + +proc blendLinearBurn(Cb, Cs: float32): float32 {.inline.} = + Cb + Cs - 1 + +proc blendColorBurn(Cb, Cs: float32): float32 {.inline.} = + if Cb == 1: 1.0 + elif Cs == 0: 0.0 + else: 1.0 - min(1, (1 - Cb) / Cs) + +proc blendLighten(Cb, Cs: float32): float32 {.inline.} = + max(Cb, Cs) + +proc blendScreen(Cb, Cs: float32): float32 {.inline.} = + screen(Cb, Cs) + +proc blendLinearDodge(Cb, Cs: float32): float32 {.inline.} = + Cb + Cs + +proc blendColorDodge(Cb, Cs: float32): float32 {.inline.} = + if Cb == 0: 0.0 + elif Cs == 1: 1.0 + else: min(1, Cb / (1 - Cs)) + +proc blendOverlay(Cb, Cs: float32): float32 {.inline.} = + hardLight(Cs, Cb) + +proc blendHardLight(Cb, Cs: float32): float32 {.inline.} = + hardLight(Cb, Cs) + +proc blendSoftLight(Cb, Cs: float32): float32 {.inline.} = + softLight(Cb, Cs) + +proc blendDifference(Cb, Cs: float32): float32 {.inline.} = + abs(Cb - Cs) + +proc blendExclusion(Cb, Cs: float32): float32 {.inline.} = + Cb + Cs - 2 * Cb * Cs + +proc blendNormal*(Cb, Cs: Color): Color {.inline.} = + result.r = Cs.r + result.g = Cs.g + result.b = Cs.b + result = alphaFix(Cb, Cs, result) + +proc blendDarken(Cb, Cs: Color): Color {.inline.} = + result.r = blendDarken(Cb.r, Cs.r) + result.g = blendDarken(Cb.g, Cs.g) + result.b = blendDarken(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendMultiply(Cb, Cs: Color): Color {.inline.} = + result.r = blendMultiply(Cb.r, Cs.r) + result.g = blendMultiply(Cb.g, Cs.g) + result.b = blendMultiply(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendLinearBurn(Cb, Cs: Color): Color {.inline.} = + result.r = blendLinearBurn(Cb.r, Cs.r) + result.g = blendLinearBurn(Cb.g, Cs.g) + result.b = blendLinearBurn(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendColorBurn(Cb, Cs: Color): Color {.inline.} = + result.r = blendColorBurn(Cb.r, Cs.r) + result.g = blendColorBurn(Cb.g, Cs.g) + result.b = blendColorBurn(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendLighten(Cb, Cs: Color): Color {.inline.} = + result.r = blendLighten(Cb.r, Cs.r) + result.g = blendLighten(Cb.g, Cs.g) + result.b = blendLighten(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendScreen(Cb, Cs: Color): Color {.inline.} = + result.r = blendScreen(Cb.r, Cs.r) + result.g = blendScreen(Cb.g, Cs.g) + result.b = blendScreen(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendLinearDodge(Cb, Cs: Color): Color {.inline.} = + result.r = blendLinearDodge(Cb.r, Cs.r) + result.g = blendLinearDodge(Cb.g, Cs.g) + result.b = blendLinearDodge(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendColorDodge(Cb, Cs: Color): Color {.inline.} = + result.r = blendColorDodge(Cb.r, Cs.r) + result.g = blendColorDodge(Cb.g, Cs.g) + result.b = blendColorDodge(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendOverlay(Cb, Cs: Color): Color {.inline.} = + result.r = blendOverlay(Cb.r, Cs.r) + result.g = blendOverlay(Cb.g, Cs.g) + result.b = blendOverlay(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendHardLight(Cb, Cs: Color): Color {.inline.} = + result.r = blendHardLight(Cb.r, Cs.r) + result.g = blendHardLight(Cb.g, Cs.g) + result.b = blendHardLight(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendSoftLight(Cb, Cs: Color): Color {.inline.} = + result.r = blendSoftLight(Cb.r, Cs.r) + result.g = blendSoftLight(Cb.g, Cs.g) + result.b = blendSoftLight(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendDifference(Cb, Cs: Color): Color {.inline.} = + result.r = blendDifference(Cb.r, Cs.r) + result.g = blendDifference(Cb.g, Cs.g) + result.b = blendDifference(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendExclusion(Cb, Cs: Color): Color {.inline.} = + result.r = blendExclusion(Cb.r, Cs.r) + result.g = blendExclusion(Cb.g, Cs.g) + result.b = blendExclusion(Cb.b, Cs.b) + result = alphaFix(Cb, Cs, result) + +proc blendColor(Cb, Cs: Color): Color {.inline.} = + let mixed = SetLum(Cs, Lum(Cb)) + alphaFix(Cb, Cs, mixed) + +proc blendLuminosity(Cb, Cs: Color): Color {.inline.} = + let mixed = SetLum(Cb, Lum(Cs)) + alphaFix(Cb, Cs, mixed) + +proc blendHue(Cb, Cs: Color): Color {.inline.} = + let mixed = SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb)) + alphaFix(Cb, Cs, mixed) + +proc blendSaturation(Cb, Cs: Color): Color {.inline.} = + let mixed = SetLum(SetSat(Cb, Sat(Cs)), Lum(Cb)) + alphaFix(Cb, Cs, mixed) + +proc blendMask(target, blend: Color): Color {.inline.} = + result.r = target.r + result.g = target.g + result.b = target.b + result.a = min(target.a, blend.a) + +proc blendSubtractMask(target, blend: Color): Color {.inline.} = + result.r = target.r + result.g = target.g + result.b = target.b + result.a = target.a * (1 - blend.a) + +proc blendIntersectMask(target, blend: Color): Color {.inline.} = + result.r = target.r + result.g = target.g + result.b = target.b + result.a = target.a * blend.a + +proc blendExcludeMask(target, blend: Color): Color {.inline.} = + result.r = target.r + result.g = target.g + result.b = target.b + result.a = abs(target.a - blend.a) + +proc blendOverwrite(target, blend: Color): Color {.inline.} = + result = blend + +proc mix2*(blendMode: BlendMode, dest, src: Color): Color {.inline.} = + case blendMode + of bmNormal: blendNormal(dest, src) + of bmDarken: blendDarken(dest, src) + of bmMultiply: blendMultiply(dest, src) + of bmLinearBurn: blendLinearBurn(dest, src) + of bmColorBurn: blendColorBurn(dest, src) + of bmLighten: blendLighten(dest, src) + of bmScreen: blendScreen(dest, src) + of bmLinearDodge: blendLinearDodge(dest, src) + of bmColorDodge: blendColorDodge(dest, src) + of bmOverlay: blendOverlay(dest, src) + of bmSoftLight: blendSoftLight(dest, src) + of bmHardLight: blendHardLight(dest, src) + of bmDifference: blendDifference(dest, src) + of bmExclusion: blendExclusion(dest, src) + of bmHue: blendHue(dest, src) + of bmSaturation: blendSaturation(dest, src) + of bmColor: blendColor(dest, src) + of bmLuminosity: blendLuminosity(dest, src) + of bmMask: blendMask(dest, src) + of bmOverwrite: blendOverwrite(dest, src) + of bmSubtractMask: blendSubtractMask(dest, src) + of bmIntersectMask: blendIntersectMask(dest, src) + of bmExcludeMask: blendExcludeMask(dest, src) + +proc alphaFix(Cb, Cs, mixed: ColorRGBA): ColorRGBA {.inline.} = + let ab = Cb.a.int32 + let As = Cs.a.int32 + let r = As * (255 - ab) * Cs.r.int32 + As * ab * mixed.r.int32 + (255 - As) * ab * Cb.r.int32 + let g = As * (255 - ab) * Cs.g.int32 + As * ab * mixed.g.int32 + (255 - As) * ab * Cb.g.int32 + let b = As * (255 - ab) * Cs.b.int32 + As * ab * mixed.b.int32 + (255 - As) * ab * Cb.b.int32 + + let a = Cs.a.int32 + Cb.a.int32 * (255 - Cs.a.int32) div 255 + if a == 0: + return + else: + result.r = (r div a div 255).uint8 + result.g = (g div a div 255).uint8 + result.b = (b div a div 255).uint8 + result.a = a.uint8 + +proc blendNormal*(a, b: ColorRGBA): ColorRGBA = + # blendNormal(a.color, b.color).rgba + result.r = b.r + result.g = b.g + result.b = b.b + result = alphaFix(a, b, result) + +proc blendDarken*(a, b: ColorRGBA): ColorRGBA = + blendDarken(a.color, b.color).rgba + +proc blendMultiply*(a, b: ColorRGBA): ColorRGBA = + blendMultiply(a.color, b.color).rgba + +proc blendLinearBurn*(a, b: ColorRGBA): ColorRGBA = + blendLinearBurn(a.color, b.color).rgba + +proc blendColorBurn*(a, b: ColorRGBA): ColorRGBA = + blendColorBurn(a.color, b.color).rgba + +proc blendLighten*(a, b: ColorRGBA): ColorRGBA = + blendLighten(a.color, b.color).rgba + +proc blendScreen*(a, b: ColorRGBA): ColorRGBA = + blendScreen(a.color, b.color).rgba + +proc blendLinearDodge*(a, b: ColorRGBA): ColorRGBA = + blendLinearDodge(a.color, b.color).rgba + +proc blendColorDodge*(a, b: ColorRGBA): ColorRGBA = + blendColorDodge(a.color, b.color).rgba + +proc blendOverlay*(a, b: ColorRGBA): ColorRGBA = + blendOverlay(a.color, b.color).rgba + +proc blendHardLight*(a, b: ColorRGBA): ColorRGBA = + blendHardLight(a.color, b.color).rgba + +proc blendSoftLight*(a, b: ColorRGBA): ColorRGBA = + blendSoftLight(a.color, b.color).rgba + +proc blendDifference*(a, b: ColorRGBA): ColorRGBA = + blendDifference(a.color, b.color).rgba + +proc blendExclusion*(a, b: ColorRGBA): ColorRGBA = + blendExclusion(a.color, b.color).rgba + +proc blendColor*(a, b: ColorRGBA): ColorRGBA = + blendColor(a.color, b.color).rgba + +proc blendLuminosity*(a, b: ColorRGBA): ColorRGBA = + blendLuminosity(a.color, b.color).rgba + +proc blendHue*(a, b: ColorRGBA): ColorRGBA = + blendHue(a.color, b.color).rgba + +proc blendSaturation*(a, b: ColorRGBA): ColorRGBA = + blendSaturation(a.color, b.color).rgba + +proc blendMask*(a, b: ColorRGBA): ColorRGBA = + blendMask(a.color, b.color).rgba + +proc blendSubtractMask*(a, b: ColorRGBA): ColorRGBA = + blendSubtractMask(a.color, b.color).rgba + +proc blendIntersectMask*(a, b: ColorRGBA): ColorRGBA = + blendIntersectMask(a.color, b.color).rgba + +proc blendExcludeMask*(a, b: ColorRGBA): ColorRGBA = + blendExcludeMask(a.color, b.color).rgba + +proc blendOverwrite*(a, b: ColorRGBA): ColorRGBA = + blendOverwrite(a.color, b.color).rgba + +proc mix2*(blendMode: BlendMode, dest, src: ColorRGBA): ColorRGBA {.inline.} = + case blendMode + of bmNormal: blendNormal(dest, src) + of bmDarken: blendDarken(dest, src) + of bmMultiply: blendMultiply(dest, src) + of bmLinearBurn: blendLinearBurn(dest, src) + of bmColorBurn: blendColorBurn(dest, src) + of bmLighten: blendLighten(dest, src) + of bmScreen: blendScreen(dest, src) + of bmLinearDodge: blendLinearDodge(dest, src) + of bmColorDodge: blendColorDodge(dest, src) + of bmOverlay: blendOverlay(dest, src) + of bmSoftLight: blendSoftLight(dest, src) + of bmHardLight: blendHardLight(dest, src) + of bmDifference: blendDifference(dest, src) + of bmExclusion: blendExclusion(dest, src) + of bmHue: blendHue(dest, src) + of bmSaturation: blendSaturation(dest, src) + of bmColor: blendColor(dest, src) + of bmLuminosity: blendLuminosity(dest, src) + of bmMask: blendMask(dest, src) + of bmOverwrite: blendOverwrite(dest, src) + of bmSubtractMask: blendSubtractMask(dest, src) + of bmIntersectMask: blendIntersectMask(dest, src) + of bmExcludeMask: blendExcludeMask(dest, src) diff --git a/src/pixie/images.nim b/src/pixie/images.nim index e664edb..542f526 100644 --- a/src/pixie/images.nim +++ b/src/pixie/images.nim @@ -62,6 +62,11 @@ proc getRgbaUnsafe*(image: Image, x, y: int): ColorRGBA {.inline.} = ## Failure in the assumptions will case unsafe memory reads. result = image.data[image.width * y + x] +proc getAddr*(image: Image, x, y: int): pointer {.inline.} = + ## Gets a address of the color from (x, y) coordinates. + ## Unsafe make sure x, y are in bounds. + addr image.data[image.width * y + x] + proc `[]`*(image: Image, x, y: int): ColorRGBA {.inline.} = ## Gets a pixel at (x, y) or returns transparent black if outside of bounds. if image.inside(x, y): @@ -107,12 +112,13 @@ proc invert*(image: Image): Image = 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 + ## TODO handle images out of bounds faster + # 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)) + result.setRgbaUnsafe(x2, y2, image[x2 + x, y2 + y]) proc minifyBy2*(image: Image): Image = ## Scales the image down by an integer scale. @@ -144,13 +150,13 @@ proc magnifyBy2*(image: Image, scale2x: int): Image = proc magnifyBy2*(image: Image): Image = image.magnifyBy2(2) -func lerp(a, b: Color, v: float): Color {.inline.} = +func lerp(a, b: Color, v: float32): 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.} = +proc getRgbaSmooth*(image: Image, x, y: float32): ColorRGBA {.inline.} = ## Gets a pixel as (x, y) floats. proc toAlphy(c: Color): Color = @@ -231,7 +237,7 @@ proc drawBlend*(a: Image, b: Image, mat: Mat3, blendMode: BlendMode): Image = var rgba = a.getRgbaUnsafe(x, y) let rgba2 = b.getRgbaUnsafe(srcPos.x.floor.int, srcPos.y.floor.int) if blendMode.hasEffect(rgba2): - rgba = blendMode.mix(rgba, rgba2) + rgba = blendMode.mix2(rgba, rgba2) result.setRgbaUnsafe(x, y, rgba) else: @@ -250,7 +256,6 @@ proc drawBlendSmooth*(a: Image, b: Image, mat: Mat3, blendMode: BlendMode): Imag var matInv = mat.inverse() for y in 0 ..< a.height: for x in 0 ..< a.width: - let srcPos = matInv * vec2(x.float32, y.float32) if b.inside1px(srcPos.x, srcPos.y): var rgba = a.getRgbaUnsafe(x, y) @@ -265,20 +270,218 @@ proc drawBlendSmooth*(a: Image, b: Image, mat: Mat3, blendMode: BlendMode): Imag else: result.setRgbaUnsafe(x, y, rgba(0,0,0,0)) +proc drawCorrect*(a: Image, b: Image, mat: Mat3, blendMode: BlendMode): Image = + ## Draws one image onto another using matrix with color blending. + result = newImageNoInit(a.width, a.height) + + var + matInv = mat.inverse() + # compute movement vectors + h = 0.5.float32 + start = matInv * vec2(0 + h, 0 + h) + stepX = matInv * vec2(1 + h, 0 + h) - start + stepY = matInv * vec2(0 + h, 1 + h) - start + minFilterBy2 = max(stepX.length, stepY.length) + b = b + + while minFilterBy2 > 2.0: + b = b.minifyBy2() + start /= 2 + stepX /= 2 + stepY /= 2 + minFilterBy2 /= 2 + + for y in 0 ..< a.height: + for x in 0 ..< a.width: + let srcPos = matInv * vec2(x.float32 + h, y.float32 + h) + var rgba = a.getRgbaUnsafe(x, y) + let rgba2 = b.getRgbaSmooth(srcPos.x - h, srcPos.y - h) + rgba = blendMode.mix(rgba, rgba2) + result.setRgbaUnsafe(x, y, rgba) + +proc drawStepper*(a: Image, b: Image, mat: Mat3, blendMode: BlendMode): Image = + ## Draws one image onto another using matrix with color blending. + result = newImageNoInit(a.width, a.height) + + type Segment = object + ## A math segment from point "at" to point "to" + at*: Vec2 + to*: Vec2 + + proc segment(at, to: Vec2): Segment = + result.at = at + result.to = to + + proc intersects(a, b: Segment, at: var Vec2): bool = + ## Checks if the a segment intersects b segment. + ## If it returns true, at will have point of intersection + var s1x, s1y, s2x, s2y: float32 + s1x = a.to.x - a.at.x + s1y = a.to.y - a.at.y + s2x = b.to.x - b.at.x + s2y = b.to.y - b.at.y + + var s, t: float32 + s = (-s1y * (a.at.x - b.at.x) + s1x * (a.at.y - b.at.y)) / + (-s2x * s1y + s1x * s2y) + t = (s2x * (a.at.y - b.at.y) - s2y * (a.at.x - b.at.x)) / + (-s2x * s1y + s1x * s2y) + + if s >= 0 and s < 1 and t >= 0 and t < 1: + at.x = a.at.x + (t * s1x) + at.y = a.at.y + (t * s1y) + return true + return false + + var + matInv = mat.inverse() + # compute movement vectors + h = 0.5.float32 + start = matInv * vec2(0 + h, 0 + h) + stepX = matInv * vec2(1 + h, 0 + h) - start + stepY = matInv * vec2(0 + h, 1 + h) - start + minFilterBy2 = max(stepX.length, stepY.length) + b = b + + let corners = [ + mat * vec2(0, 0), + mat * vec2(b.width.float32, 0), + mat * vec2(b.width.float32, b.height.float32), + mat * vec2(0, b.height.float32) + ] + + let lines = [ + segment(corners[0], corners[1]), + segment(corners[1], corners[2]), + segment(corners[2], corners[3]), + segment(corners[3], corners[0]) + ] + + while minFilterBy2 > 2.0: + b = b.minifyBy2() + start /= 2 + stepX /= 2 + stepY /= 2 + minFilterBy2 /= 2 + + template forBlend( + mixer: proc(a, b: ColorRGBA): ColorRGBA, + getRgba: proc(a: Image, x, y: float32): ColorRGBA {.inline.}, + ) = + for y in 0 ..< a.height: + var + xMin = 0 + xMax = 0 + hasIntersection = false + for yOffset in [0.float32, 1]: + var scanLine = segment( + vec2(-100000, y.float32 + yOffset), + vec2(10000, y.float32 + yOffset) + ) + for l in lines: + var at: Vec2 + if intersects(l, scanLine, at): + if hasIntersection: + xMin = min(xMin, at.x.floor.int) + xMax = max(xMax, at.x.ceil.int) + else: + hasIntersection = true + xMin = at.x.floor.int + xMax = at.x.ceil.int + + xMin = xMin.clamp(0, a.width) + xMax = xMax.clamp(0, a.width) + + # for x in 0 ..< xMin: + # result.setRgbaUnsafe(x, y, a.getRgbaUnsafe(x, y)) + if xMin > 0: + copyMem(result.getAddr(0, y), a.getAddr(0, y), 4*xMin) + + for x in xMin ..< xMax: + let srcV = start + stepX * float32(x) + stepY * float32(y) + var rgba = a.getRgbaUnsafe(x, y) + # TODO maybe remove inside check? + if b.inside((srcV.x - h).int, (srcV.y - h).int): + let rgba2 = b.getRgba(srcV.x - h, srcV.y - h) + rgba = mixer(rgba, rgba2) + result.setRgbaUnsafe(x, y, rgba) + + #for x in xMax ..< a.width: + # result.setRgbaUnsafe(x, y, a.getRgbaUnsafe(x, y)) + if a.width - xMax > 0: + copyMem(result.getAddr(xMax, y), a.getAddr(xMax, y), 4*(a.width - xMax)) + + proc getRgba(a: Image, x, y: float32): ColorRGBA {.inline.} = + a.getRgbaUnsafe(x.int, y.int) + + # TODO check pos for fractional + if stepX.length == 1.0 and stepY.length == 1.0: + case blendMode + of bmNormal: forBlend(blendNormal, getRgba) + of bmDarken: forBlend(blendDarken, getRgba) + of bmMultiply: forBlend(blendMultiply, getRgba) + of bmLinearBurn: forBlend(blendLinearBurn, getRgba) + of bmColorBurn: forBlend(blendColorBurn, getRgba) + of bmLighten: forBlend(blendLighten, getRgba) + of bmScreen: forBlend(blendScreen, getRgba) + of bmLinearDodge: forBlend(blendLinearDodge, getRgba) + of bmColorDodge: forBlend(blendColorDodge, getRgba) + of bmOverlay: forBlend(blendOverlay, getRgba) + of bmSoftLight: forBlend(blendSoftLight, getRgba) + of bmHardLight: forBlend(blendHardLight, getRgba) + of bmDifference: forBlend(blendDifference, getRgba) + of bmExclusion: forBlend(blendExclusion, getRgba) + of bmHue: forBlend(blendHue, getRgba) + of bmSaturation: forBlend(blendSaturation, getRgba) + of bmColor: forBlend(blendColor, getRgba) + of bmLuminosity: forBlend(blendLuminosity, getRgba) + of bmMask: forBlend(blendMask, getRgba) + of bmOverwrite: forBlend(blendOverwrite, getRgba) + of bmSubtractMask: forBlend(blendSubtractMask, getRgba) + of bmIntersectMask: forBlend(blendIntersectMask, getRgba) + of bmExcludeMask: forBlend(blendExcludeMask, getRgba) + else: + case blendMode + of bmNormal: forBlend(blendNormal, getRgbaSmooth) + of bmDarken: forBlend(blendDarken, getRgbaSmooth) + of bmMultiply: forBlend(blendMultiply, getRgbaSmooth) + of bmLinearBurn: forBlend(blendLinearBurn, getRgbaSmooth) + of bmColorBurn: forBlend(blendColorBurn, getRgbaSmooth) + of bmLighten: forBlend(blendLighten, getRgbaSmooth) + of bmScreen: forBlend(blendScreen, getRgbaSmooth) + of bmLinearDodge: forBlend(blendLinearDodge, getRgbaSmooth) + of bmColorDodge: forBlend(blendColorDodge, getRgbaSmooth) + of bmOverlay: forBlend(blendOverlay, getRgbaSmooth) + of bmSoftLight: forBlend(blendSoftLight, getRgbaSmooth) + of bmHardLight: forBlend(blendHardLight, getRgbaSmooth) + of bmDifference: forBlend(blendDifference, getRgbaSmooth) + of bmExclusion: forBlend(blendExclusion, getRgbaSmooth) + of bmHue: forBlend(blendHue, getRgbaSmooth) + of bmSaturation: forBlend(blendSaturation, getRgbaSmooth) + of bmColor: forBlend(blendColor, getRgbaSmooth) + of bmLuminosity: forBlend(blendLuminosity, getRgbaSmooth) + of bmMask: forBlend(blendMask, getRgbaSmooth) + of bmOverwrite: forBlend(blendOverwrite, getRgbaSmooth) + of bmSubtractMask: forBlend(blendSubtractMask, getRgbaSmooth) + of bmIntersectMask: forBlend(blendIntersectMask, getRgbaSmooth) + of bmExcludeMask: forBlend(blendExcludeMask, getRgbaSmooth) proc draw*(a: Image, b: Image, mat: Mat3, blendMode = bmNormal): Image = ## Draws one image onto another using matrix with color blending. # Decide which ones of the draws best fit current parameters. - let ns = [-1.float32, 0, 1] - if mat[0, 0] in ns and mat[0, 1] in ns and - mat[1, 0] in ns and mat[1, 1] in ns and - mat[2, 0].fractional == 0.0 and mat[2, 1].fractional == 0.0: - if blendMode == bmOverwrite: - return drawOverwrite(a, b, mat) - else: - return drawBlend(a, b, mat, blendMode) - return drawBlendSmooth(a, b, mat, blendMode) + # let ns = [-1.float32, 0, 1] + # if mat[0, 0] in ns and mat[0, 1] in ns and + # mat[1, 0] in ns and mat[1, 1] in ns and + # mat[2, 0].fractional == 0.0 and mat[2, 1].fractional == 0.0: + # if blendMode == bmOverwrite: + # return drawOverwrite(a, b, mat) + # else: + # return drawBlend(a, b, mat, blendMode) + + # return drawCorrect(a, b, mat, blendMode) + + return drawStepper(a, b, mat, blendMode) proc draw*(a: Image, b: Image, pos = vec2(0, 0), blendMode = bmNormal): Image = a.draw(b, translate(pos), blendMode) diff --git a/tests/benchmark_draw.nim b/tests/benchmark_draw.nim index 224da9d..8e5bd22 100644 --- a/tests/benchmark_draw.nim +++ b/tests/benchmark_draw.nim @@ -1,52 +1,44 @@ import pixie, chroma, vmath, fidget/opengl/perf, pixie/fileformats/bmp -timeIt "drawOverwrite bmOverwrite": - var tmp = 0 - var c: Image - for i in 0 ..< 1000: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 255, 0, 255)) - c = a.drawOverwrite(b, translate(vec2(25, 25))) # Copy - tmp += c.width * c.height - c.writeFile("tests/images/bench.drawOverwrite.bmOverwrite.png") - echo tmp +# timeIt "drawOverwrite bmOverwrite": +# var tmp = 0 +# var c: Image +# for i in 0 ..< 1000: +# var a = newImageFill(100, 100, rgba(255, 0, 0, 255)) +# var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) +# c = a.drawOverwrite(b, translate(vec2(25, 25))) # Copy +# tmp += c.width * c.height +# c.writeFile("tests/images/bench.drawOverwrite.bmOverwrite.png") +# echo tmp -timeIt "drawBlend bmOverwrite": - var tmp = 0 - var c: Image - for i in 0 ..< 1000: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 255, 0, 255)) - c = a.drawBlend(b, translate(vec2(25, 25)), bmOverwrite) - tmp += c.width * c.height - c.writeFile("tests/images/bench.drawBlend.bmOverwrite.png") - echo tmp +# timeIt "drawBlend bmOverwrite": +# var tmp = 0 +# var c: Image +# for i in 0 ..< 1000: +# var a = newImageFill(100, 100, rgba(255, 0, 0, 255)) +# var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) +# c = a.drawBlend(b, translate(vec2(25, 25)), bmOverwrite) +# tmp += c.width * c.height +# c.writeFile("tests/images/bench.drawBlend.bmOverwrite.png") +# echo tmp -timeIt "drawBlendSmooth bmOverwrite": - var tmp = 0 - var c: Image - for i in 0 ..< 1000: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 255, 0, 255)) - c = a.drawBlendSmooth(b, translate(vec2(25, 25)), bmOverwrite) - tmp += c.width * c.height - c.writeFile("tests/images/bench.drawBlendSmooth.bmOverwrite.png") - echo tmp +# timeIt "drawBlendSmooth bmOverwrite": +# var tmp = 0 +# var c: Image +# for i in 0 ..< 1000: +# var a = newImageFill(100, 100, rgba(255, 0, 0, 255)) +# var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) +# c = a.drawBlendSmooth(b, translate(vec2(25, 25)), bmOverwrite) +# tmp += c.width * c.height +# c.writeFile("tests/images/bench.drawBlendSmooth.bmOverwrite.png") +# echo tmp timeIt "drawBlend bmNormal": var tmp = 0 var c: Image for i in 0 ..< 1000: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 255, 0, 255)) + var a = newImageFill(100, 100, rgba(255, 0, 0, 255)) + var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) c = a.drawBlend(b, translate(vec2(25, 25)), bmNormal) tmp += c.width * c.height c.writeFile("tests/images/bench.drawBlend.bmNormal.png") @@ -56,41 +48,57 @@ timeIt "drawBlendSmooth bmNormal": var tmp = 0 var c: Image for i in 0 ..< 1000: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 255, 0, 255)) + var a = newImageFill(100, 100, rgba(255, 0, 0, 255)) + var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) c = a.drawBlendSmooth(b, translate(vec2(25, 25)), bmNormal) tmp += c.width * c.height c.writeFile("tests/images/bench.drawBlendSmooth.bmNormal.png") echo tmp -timeIt "drawBlend bmSaturation": +timeIt "drawCorrect bmNormal": var tmp = 0 var c: Image for i in 0 ..< 1000: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 0, 0, 255)) - c = a.drawBlend(b, translate(vec2(25, 25)), bmSaturation) + var a = newImageFill(100, 100, rgba(255, 0, 0, 255)) + var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) + c = a.drawCorrect(b, translate(vec2(25, 25)), bmNormal) tmp += c.width * c.height - c.writeFile("tests/images/bench.drawBlend.bmSaturation.png") + c.writeFile("tests/images/bench.drawCorrect.bmNormal.png") echo tmp -timeIt "drawBlendSmooth bmSaturation": +timeIt "drawStepper bmNormal": var tmp = 0 var c: Image for i in 0 ..< 1000: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 0, 0, 255)) - c = a.drawBlendSmooth(b, translate(vec2(25, 25)), bmSaturation) + var a = newImageFill(100, 100, rgba(255, 0, 0, 255)) + var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) + c = a.drawStepper(b, translate(vec2(25, 25)), bmNormal) tmp += c.width * c.height - c.writeFile("tests/images/bench.drawBlendSmooth.bmSaturation.png") + c.writeFile("tests/images/bench.drawStepper.bmNormal.png") echo tmp +# timeIt "drawBlend bmSaturation": +# var tmp = 0 +# var c: Image +# for i in 0 ..< 1000: +# var a = newImageFill(100, 100, rgba(255, 0, 0, 255)) +# var b = newImageFill(100, 100, rgba(0, 0, 0, 255)) +# c = a.drawBlend(b, translate(vec2(25, 25)), bmSaturation) +# tmp += c.width * c.height +# c.writeFile("tests/images/bench.drawBlend.bmSaturation.png") +# echo tmp + +# timeIt "drawBlendSmooth bmSaturation": +# var tmp = 0 +# var c: Image +# for i in 0 ..< 1000: +# var a = newImageFill(100, 100, rgba(255, 0, 0, 255)) +# var b = newImageFill(100, 100, rgba(0, 0, 0, 255)) +# c = a.drawBlendSmooth(b, translate(vec2(25, 25)), bmSaturation) +# tmp += c.width * c.height +# c.writeFile("tests/images/bench.drawBlendSmooth.bmSaturation.png") +# echo tmp + # timeIt "benchDrawFast3 Rotation": # var tmp = 0 # var c: Image diff --git a/tests/benchmark_inplace.nim b/tests/benchmark_inplace.nim index 4bb6f9f..6c9a641 100644 --- a/tests/benchmark_inplace.nim +++ b/tests/benchmark_inplace.nim @@ -17,40 +17,193 @@ proc inPlaceDraw*(destImage: Image, srcImage: Image, mat: Mat3, blendMode = bmNo proc inPlaceDraw*(destImage: Image, srcImage: Image, pos = vec2(0, 0), blendMode = bmNormal) = destImage.inPlaceDraw(srcImage, translate(-pos), blendMode) -block: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 255, 0, 255)) - a.inPlaceDraw(b, pos=vec2(25, 25)) - writeFile("tests/images/inPlaceDraw.bmp", a.encodeBmp()) +proc drawStepperInPlace*(a: Image, b: Image, mat: Mat3, blendMode: BlendMode) = + ## Draws one image onto another using matrix with color blending. -block: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 255, 0, 255)) - var c = a.drawFast3(b, translate(vec2(25, 25)), bmNormal) - writeFile("tests/images/copyDraw.bmp", c.encodeBmp()) + type Segment = object + ## A math segment from point "at" to point "to" + at*: Vec2 + to*: Vec2 + + proc segment(at, to: Vec2): Segment = + result.at = at + result.to = to + + proc intersects(a, b: Segment, at: var Vec2): bool = + ## Checks if the a segment intersects b segment. + ## If it returns true, at will have point of intersection + var s1x, s1y, s2x, s2y: float32 + s1x = a.to.x - a.at.x + s1y = a.to.y - a.at.y + s2x = b.to.x - b.at.x + s2y = b.to.y - b.at.y + + var s, t: float32 + s = (-s1y * (a.at.x - b.at.x) + s1x * (a.at.y - b.at.y)) / + (-s2x * s1y + s1x * s2y) + t = (s2x * (a.at.y - b.at.y) - s2y * (a.at.x - b.at.x)) / + (-s2x * s1y + s1x * s2y) + + if s >= 0 and s < 1 and t >= 0 and t < 1: + at.x = a.at.x + (t * s1x) + at.y = a.at.y + (t * s1y) + return true + return false + + var + matInv = mat.inverse() + # compute movement vectors + h = 0.5.float32 + start = matInv * vec2(0 + h, 0 + h) + stepX = matInv * vec2(1 + h, 0 + h) - start + stepY = matInv * vec2(0 + h, 1 + h) - start + minFilterBy2 = max(stepX.length, stepY.length) + b = b + + let corners = [ + mat * vec2(0, 0), + mat * vec2(b.width.float32, 0), + mat * vec2(b.width.float32, b.height.float32), + mat * vec2(0, b.height.float32) + ] + + let lines = [ + segment(corners[0], corners[1]), + segment(corners[1], corners[2]), + segment(corners[2], corners[3]), + segment(corners[3], corners[0]) + ] + + while minFilterBy2 > 2.0: + b = b.minifyBy2() + start /= 2 + stepX /= 2 + stepY /= 2 + minFilterBy2 /= 2 + + template forBlend( + mixer: proc(a, b: ColorRGBA): ColorRGBA, + getRgba: proc(a: Image, x, y: float32): ColorRGBA {.inline.}, + ) = + for y in 0 ..< a.height: + var + xMin = 0 + xMax = 0 + hasIntersection = false + for yOffset in [0.float32, 1]: + var scanLine = segment( + vec2(-100000, y.float32 + yOffset), + vec2(10000, y.float32 + yOffset) + ) + for l in lines: + var at: Vec2 + if intersects(l, scanLine, at): + if hasIntersection: + xMin = min(xMin, at.x.floor.int) + xMax = max(xMax, at.x.ceil.int) + else: + hasIntersection = true + xMin = at.x.floor.int + xMax = at.x.ceil.int + + xMin = xMin.clamp(0, a.width) + xMax = xMax.clamp(0, a.width) + + # for x in 0 ..< xMin: + # result.setRgbaUnsafe(x, y, a.getRgbaUnsafe(x, y)) + # if xMin > 0: + # copyMem(a.getAddr(0, y), a.getAddr(0, y), 4*xMin) + + for x in xMin ..< xMax: + let srcV = start + stepX * float32(x) + stepY * float32(y) + var rgba = a.getRgbaUnsafe(x, y) + if b.inside((srcV.x - h).int, (srcV.y - h).int): + let rgba2 = b.getRgba(srcV.x - h, srcV.y - h) + rgba = mixer(rgba, rgba2) + a.setRgbaUnsafe(x, y, rgba) + + #for x in xMax ..< a.width: + # result.setRgbaUnsafe(x, y, a.getRgbaUnsafe(x, y)) + # if a.width - xMax > 0: + # copyMem(result.getAddr(xMax, y), a.getAddr(xMax, y), 4*(a.width - xMax)) + + proc getRgba(a: Image, x, y: float32): ColorRGBA {.inline.} = + a.getRgbaUnsafe(x.int, y.int) + + if stepX.length == 1.0 and stepY.length == 1.0: + case blendMode + of bmNormal: forBlend(blendNormal, getRgba) + of bmDarken: forBlend(blendDarken, getRgba) + of bmMultiply: forBlend(blendMultiply, getRgba) + of bmLinearBurn: forBlend(blendLinearBurn, getRgba) + of bmColorBurn: forBlend(blendColorBurn, getRgba) + of bmLighten: forBlend(blendLighten, getRgba) + of bmScreen: forBlend(blendScreen, getRgba) + of bmLinearDodge: forBlend(blendLinearDodge, getRgba) + of bmColorDodge: forBlend(blendColorDodge, getRgba) + of bmOverlay: forBlend(blendOverlay, getRgba) + of bmSoftLight: forBlend(blendSoftLight, getRgba) + of bmHardLight: forBlend(blendHardLight, getRgba) + of bmDifference: forBlend(blendDifference, getRgba) + of bmExclusion: forBlend(blendExclusion, getRgba) + of bmHue: forBlend(blendHue, getRgba) + of bmSaturation: forBlend(blendSaturation, getRgba) + of bmColor: forBlend(blendColor, getRgba) + of bmLuminosity: forBlend(blendLuminosity, getRgba) + of bmMask: forBlend(blendMask, getRgba) + of bmOverwrite: forBlend(blendOverwrite, getRgba) + of bmSubtractMask: forBlend(blendSubtractMask, getRgba) + of bmIntersectMask: forBlend(blendIntersectMask, getRgba) + of bmExcludeMask: forBlend(blendExcludeMask, getRgba) + else: + case blendMode + of bmNormal: forBlend(blendNormal, getRgbaSmooth) + of bmDarken: forBlend(blendDarken, getRgbaSmooth) + of bmMultiply: forBlend(blendMultiply, getRgbaSmooth) + of bmLinearBurn: forBlend(blendLinearBurn, getRgbaSmooth) + of bmColorBurn: forBlend(blendColorBurn, getRgbaSmooth) + of bmLighten: forBlend(blendLighten, getRgbaSmooth) + of bmScreen: forBlend(blendScreen, getRgbaSmooth) + of bmLinearDodge: forBlend(blendLinearDodge, getRgbaSmooth) + of bmColorDodge: forBlend(blendColorDodge, getRgbaSmooth) + of bmOverlay: forBlend(blendOverlay, getRgbaSmooth) + of bmSoftLight: forBlend(blendSoftLight, getRgbaSmooth) + of bmHardLight: forBlend(blendHardLight, getRgbaSmooth) + of bmDifference: forBlend(blendDifference, getRgbaSmooth) + of bmExclusion: forBlend(blendExclusion, getRgbaSmooth) + of bmHue: forBlend(blendHue, getRgbaSmooth) + of bmSaturation: forBlend(blendSaturation, getRgbaSmooth) + of bmColor: forBlend(blendColor, getRgbaSmooth) + of bmLuminosity: forBlend(blendLuminosity, getRgbaSmooth) + of bmMask: forBlend(blendMask, getRgbaSmooth) + of bmOverwrite: forBlend(blendOverwrite, getRgbaSmooth) + of bmSubtractMask: forBlend(blendSubtractMask, getRgbaSmooth) + of bmIntersectMask: forBlend(blendIntersectMask, getRgbaSmooth) + of bmExcludeMask: forBlend(blendExcludeMask, getRgbaSmooth) timeIt "inPlaceDraw": var tmp = 0 for i in 0 ..< 1000: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 255, 0, 255)) + var a = newImageFill(1000, 1000, rgba(0, 255, 0, 255)) + var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) a.inPlaceDraw(b, pos=vec2(25, 25)) tmp += a.width * a.height echo tmp -timeIt "copyDraw": +timeIt "drawStepper": var tmp = 0 for i in 0 ..< 1000: - var a = newImage(100, 100) - a.fill(rgba(255, 0, 0, 255)) - var b = newImage(100, 100) - b.fill(rgba(0, 255, 0, 255)) - var c = a.drawFast3(b, translate(vec2(25, 25)), bmNormal) + var a = newImageFill(1000, 1000, rgba(255, 0, 0, 255)) + var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) + var c = a.drawStepper(b, translate(vec2(25, 25)), bmNormal) tmp += c.width * c.height echo tmp + +timeIt "drawStepperInPlace": + var tmp = 0 + for i in 0 ..< 1000: + var a = newImageFill(1000, 1000, rgba(0, 255, 0, 255)) + var b = newImageFill(100, 100, rgba(0, 255, 0, 255)) + drawStepperInPlace(a, b, translate(vec2(25, 25)), bmNormal) + tmp += a.width * a.height + echo tmp diff --git a/tests/images/bench.drawBlend.bmNormal.png b/tests/images/bench.drawBlend.bmNormal.png index 43b43aa..ec3debd 100644 Binary files a/tests/images/bench.drawBlend.bmNormal.png and b/tests/images/bench.drawBlend.bmNormal.png differ diff --git a/tests/images/bench.drawBlend.bmOverwrite.png b/tests/images/bench.drawBlend.bmOverwrite.png index 43b43aa..ec3debd 100644 Binary files a/tests/images/bench.drawBlend.bmOverwrite.png and b/tests/images/bench.drawBlend.bmOverwrite.png differ diff --git a/tests/images/bench.drawBlendSmooth.bmNormal.png b/tests/images/bench.drawBlendSmooth.bmNormal.png index 43b43aa..ec3debd 100644 Binary files a/tests/images/bench.drawBlendSmooth.bmNormal.png and b/tests/images/bench.drawBlendSmooth.bmNormal.png differ diff --git a/tests/images/bench.drawBlendSmooth2.bmNormal.png b/tests/images/bench.drawBlendSmooth2.bmNormal.png new file mode 100644 index 0000000..ec3debd Binary files /dev/null and b/tests/images/bench.drawBlendSmooth2.bmNormal.png differ diff --git a/tests/images/bench.drawCorrect.bmNormal.png b/tests/images/bench.drawCorrect.bmNormal.png new file mode 100644 index 0000000..ec3debd Binary files /dev/null and b/tests/images/bench.drawCorrect.bmNormal.png differ diff --git a/tests/images/bench.drawOverwrite.bmOverwrite.png b/tests/images/bench.drawOverwrite.bmOverwrite.png index 43b43aa..ec3debd 100644 Binary files a/tests/images/bench.drawOverwrite.bmOverwrite.png and b/tests/images/bench.drawOverwrite.bmOverwrite.png differ diff --git a/tests/images/bench.drawStepper.bmNormal.png b/tests/images/bench.drawStepper.bmNormal.png new file mode 100644 index 0000000..ec3debd Binary files /dev/null and b/tests/images/bench.drawStepper.bmNormal.png differ