## Blending modes. import chroma, common, math when defined(amd64) and not defined(pixieNoSimd): import nimsimd/sse2 # See https://www.w3.org/TR/compositing-1/ # See https://www.khronos.org/registry/OpenGL/extensions/KHR/KHR_blend_equation_advanced.txt type BlendMode* = enum bmNormal bmDarken bmMultiply # bmLinearBurn bmColorBurn bmLighten bmScreen # bmLinearDodge bmColorDodge bmOverlay bmSoftLight bmHardLight bmDifference bmExclusion bmHue bmSaturation bmColor bmLuminosity bmMask ## Special blend mode that is used for masking bmOverwrite ## Special blend mode that just copies pixels bmSubtractMask ## Inverse mask bmExcludeMask Blender* = proc(backdrop, source: ColorRGBX): ColorRGBX {.gcsafe, raises: [].} ## Function signature returned by blender. Masker* = proc(backdrop, source: uint8): uint8 {.gcsafe, raises: [].} ## Function signature returned by masker. when defined(release): {.push checks: off.} proc min(a, b: uint32): uint32 {.inline.} = if a < b: a else: b proc alphaFix(backdrop, source, mixed: ColorRGBA): ColorRGBA = ## After mixing an image, adjust its alpha value to be correct. let sa = source.a.uint32 ba = backdrop.a.uint32 t0 = sa * (255 - ba) t1 = sa * ba t2 = (255 - sa) * ba let r = t0 * source.r.uint32 + t1 * mixed.r.uint32 + t2 * backdrop.r.uint32 g = t0 * source.g.uint32 + t1 * mixed.g.uint32 + t2 * backdrop.g.uint32 b = t0 * source.b.uint32 + t1 * mixed.b.uint32 + t2 * backdrop.b.uint32 a = sa + ba * (255 - sa) div 255 if a == 0: return 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 alphaFix(backdrop, source, mixed: Color): Color = ## After mixing an image, adjust its alpha value to be correct. result.a = (source.a + backdrop.a * (1.0 - source.a)) if result.a == 0: return let t0 = source.a * (1 - backdrop.a) t1 = source.a * backdrop.a t2 = (1 - source.a) * backdrop.a result.r = t0 * source.r + t1 * mixed.r + t2 * backdrop.r result.g = t0 * source.g + t1 * mixed.g + t2 * backdrop.g result.b = t0 * source.b + t1 * mixed.b + t2 * backdrop.b result.r /= result.a result.g /= result.a result.b /= result.a proc blendAlpha*(backdrop, source: uint8): uint8 {.inline.} = ## Blends alphas of backdrop, source. source + ((backdrop.uint32 * (255 - source)) div 255).uint8 proc screen(backdrop, source: uint32): uint8 {.inline.} = ((backdrop + source).int32 - ((backdrop * source) div 255).int32).uint8 proc hardLight( backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint32 ): uint8 {.inline.} = if sourceColor * 2 <= sourceAlpha: (( 2 * sourceColor * backdropColor + (sourceColor * (255 - backdropAlpha)) + (backdropColor * (255 - sourceAlpha)) ) div 255).uint8 else: screen(backdropColor, sourceColor) proc softLight(backdrop, source: float32): float32 {.inline.} = ## Pegtop (1 - 2 * source) * backdrop ^ 2 + 2 * source * backdrop proc `+`(c: Color, v: float32): Color {.inline.} = result.r = c.r + v result.g = c.g + v result.b = c.b + v result.a = c.a + v proc `+`(v: float32, c: Color): Color {.inline.} = c + v proc `*`(c: Color, v: float32): Color {.inline.} = result.r = c.r * v result.g = c.g * v result.b = c.b * v result.a = c.a * v proc `/`(c: Color, v: float32): Color {.inline.} = result.r = c.r / v result.g = c.g / v result.b = c.b / v result.a = c.a / v proc `-`(c: Color, v: float32): Color {.inline.} = result.r = c.r - v result.g = c.g - v result.b = c.b - v result.a = c.a - v proc Lum(C: Color): float32 {.inline.} = 0.3 * C.r + 0.59 * C.g + 0.11 * C.b proc ClipColor(C: var Color) {.inline.} = let L = Lum(C) n = min([C.r, C.g, C.b]) x = max([C.r, C.g, C.b]) if n < 0: C = L + (((C - L) * L) / (L - n)) if x > 1: C = L + (((C - L) * (1 - L)) / (x - L)) 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 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.} = let satC = Sat(C) if satC > 0: result = (C - min([C.r, C.g, C.b])) * s / satC proc blendNormal*(backdrop, source: ColorRGBX): ColorRGBX = if backdrop.a == 0 or source.a == 255: return source if source.a == 0: return backdrop let k = (255 - source.a.uint32) result.r = source.r + ((backdrop.r.uint32 * k) div 255).uint8 result.g = source.g + ((backdrop.g.uint32 * k) div 255).uint8 result.b = source.b + ((backdrop.b.uint32 * k) div 255).uint8 result.a = blendAlpha(backdrop.a, source.a) proc blendDarken(backdrop, source: ColorRGBX): ColorRGBX = proc blend( backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint8 ): uint8 {.inline.} = min( backdropColor + ((255 - backdropAlpha).uint32 * sourceColor) div 255, sourceColor + ((255 - sourceAlpha).uint32 * backdropColor) div 255 ).uint8 result.r = blend(backdrop.r, backdrop.a, source.r, source.a) result.g = blend(backdrop.g, backdrop.a, source.g, source.a) result.b = blend(backdrop.b, backdrop.a, source.b, source.a) result.a = blendAlpha(backdrop.a, source.a) proc blendMultiply(backdrop, source: ColorRGBX): ColorRGBX = proc blend( backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint8 ): uint8 {.inline.} = (( (255 - backdropAlpha).uint32 * sourceColor + (255 - sourceAlpha).uint32 * backdropColor + backdropColor.uint32 * sourceColor ) div 255).uint8 result.r = blend(backdrop.r, backdrop.a, source.r, source.a) result.g = blend(backdrop.g, backdrop.a, source.g, source.a) result.b = blend(backdrop.b, backdrop.a, source.b, source.a) result.a = blendAlpha(backdrop.a, source.a) # proc blendLinearBurn(backdrop, source: ColorRGBX): ColorRGBX = # let # backdrop = backdrop.toStraightAlpha() # source = source.toStraightAlpha() # result.r = min(0, backdrop.r.int32 + source.r.int32 - 255).uint8 # result.g = min(0, backdrop.g.int32 + source.g.int32 - 255).uint8 # result.b = min(0, backdrop.b.int32 + source.b.int32 - 255).uint8 # result = alphaFix(backdrop, source, result) # result = result.toPremultipliedAlpha() proc blendColorBurn(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba() source = source.rgba() proc blend(backdrop, source: uint32): uint8 {.inline.} = if backdrop == 255: 255.uint8 elif source == 0: 0 else: 255 - min(255, (255 * (255 - backdrop)) div source).uint8 var blended: ColorRGBA blended.r = blend(backdrop.r, source.r) blended.g = blend(backdrop.g, source.g) blended.b = blend(backdrop.b, source.b) result = alphaFix(backdrop, source, blended).rgbx() proc blendLighten(backdrop, source: ColorRGBX): ColorRGBX = proc blend( backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint8 ): uint8 {.inline.} = max( backdropColor + ((255 - backdropAlpha).uint32 * sourceColor) div 255, sourceColor + ((255 - sourceAlpha).uint32 * backdropColor) div 255 ).uint8 result.r = blend(backdrop.r, backdrop.a, source.r, source.a) result.g = blend(backdrop.g, backdrop.a, source.g, source.a) result.b = blend(backdrop.b, backdrop.a, source.b, source.a) result.a = blendAlpha(backdrop.a, source.a) proc blendScreen(backdrop, source: ColorRGBX): ColorRGBX = result.r = screen(backdrop.r, source.r) result.g = screen(backdrop.g, source.g) result.b = screen(backdrop.b, source.b) result.a = blendAlpha(backdrop.a, source.a) # proc blendLinearDodge(backdrop, source: ColorRGBX): ColorRGBX = # let # backdrop = backdrop.toStraightAlpha() # source = source.toStraightAlpha() # result.r = min(backdrop.r.uint32 + source.r, 255).uint8 # result.g = min(backdrop.g.uint32 + source.g, 255).uint8 # result.b = min(backdrop.b.uint32 + source.b, 255).uint8 # result = alphaFix(backdrop, source, result) # result = result.toPremultipliedAlpha() proc blendColorDodge(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba() source = source.rgba() proc blend(backdrop, source: uint32): uint8 {.inline.} = if backdrop == 0: 0.uint8 elif source == 255: 255 else: min(255, (255 * backdrop) div (255 - source)).uint8 var blended: ColorRGBA blended.r = blend(backdrop.r, source.r) blended.g = blend(backdrop.g, source.g) blended.b = blend(backdrop.b, source.b) result = alphaFix(backdrop, source, blended).rgbx() proc blendOverlay(backdrop, source: ColorRGBX): ColorRGBX = result.r = hardLight(source.r, source.a, backdrop.r, backdrop.a) result.g = hardLight(source.g, source.a, backdrop.g, backdrop.a) result.b = hardLight(source.b, source.a, backdrop.b, backdrop.a) result.a = blendAlpha(backdrop.a, source.a) proc blendSoftLight(backdrop, source: ColorRGBX): ColorRGBX = # proc softLight(backdrop, source: int32): uint8 {.inline.} = # ## Pegtop # ( # ((255 - 2 * source) * backdrop ^ 2) div 255 ^ 2 + # (2 * source * backdrop) div 255 # ).uint8 let backdrop = backdrop.rgba() source = source.rgba() var rgba: ColorRGBA when defined(amd64) and not defined(pixieNoSimd): let vb = mm_setr_ps( backdrop.r.float32, backdrop.g.float32, backdrop.b.float32, 0 ) vs = mm_setr_ps(source.r.float32, source.g.float32, source.b.float32, 0) v2 = mm_set1_ps(2) v255 = mm_set1_ps(255) v255sq = mm_set1_ps(255 * 255) vm = ((v255 - v2 * vs) * vb * vb) / v255sq + (v2 * vs * vb) / v255 values = cast[array[4, uint32]](mm_cvtps_epi32(vm)) rgba.r = values[0].uint8 rgba.g = values[1].uint8 rgba.b = values[2].uint8 # proc alphaFix(backdrop, source, mixed: ColorRGBX): ColorRGBX {.inline.} = # if backdrop.a == 0 and source.a == 0: # return # let # vb = mm_setr_ps(backdrop.r.float32, backdrop.g.float32, backdrop.b.float32, 0) # vs = mm_setr_ps(source.r.float32, source.g.float32, source.b.float32, 0) # vm = mm_setr_ps(mixed.r.float32, mixed.g.float32, mixed.b.float32, 0) # alphaFix(backdrop, source, vb, vs, vm) let sa = source.a.float32 ba = backdrop.a.float32 a = sa + ba * (255 - sa) / 255 if a == 0: return let t0 = mm_set1_ps(sa * (255 - ba)) t1 = mm_set1_ps(sa * ba) t2 = mm_set1_ps((255 - sa) * ba) va = mm_set1_ps(a) final = cast[array[4, uint32]]( mm_cvtps_epi32((t0 * vs + t1 * vm + t2 * vb) / va / v255) ) rgba.r = final[0].uint8 rgba.g = final[1].uint8 rgba.b = final[2].uint8 rgba.a = a.uint8 else: let b = backdrop.color s = source.color var blended: Color blended.r = softLight(b.r, s.r) blended.g = softLight(b.g, s.g) blended.b = softLight(b.b, s.b) blended = alphaFix(b, s, blended) rgba = blended.rgba result = rgba.rgbx() proc blendHardLight(backdrop, source: ColorRGBX): ColorRGBX = result.r = hardLight(backdrop.r, backdrop.a, source.r, source.a) result.g = hardLight(backdrop.g, backdrop.a, source.g, source.a) result.b = hardLight(backdrop.b, backdrop.a, source.b, source.a) result.a = blendAlpha(backdrop.a, source.a) proc blendDifference(backdrop, source: ColorRGBX): ColorRGBX = proc blend( backdropColor, backdropAlpha, sourceColor, sourceAlpha: uint8 ): uint8 {.inline.} = ((backdropColor + sourceColor).int32 - 2 * (min( backdropColor.uint32 * sourceAlpha, sourceColor.uint32 * backdropAlpha ) div 255).int32 ).uint8 result.r = blend(backdrop.r, backdrop.a, source.r, source.a) result.g = blend(backdrop.g, backdrop.a, source.g, source.a) result.b = blend(backdrop.b, backdrop.a, source.b, source.a) result.a = blendAlpha(backdrop.a, source.a) proc blendExclusion(backdrop, source: ColorRGBX): ColorRGBX = proc blend(backdrop, source: uint32): uint8 {.inline.} = let v = (backdrop + source).int32 - ((2 * backdrop * source) div 255).int32 max(0, v).uint8 result.r = blend(backdrop.r.uint32, source.r.uint32) result.g = blend(backdrop.g.uint32, source.g.uint32) result.b = blend(backdrop.b.uint32, source.b.uint32) result.a = blendAlpha(backdrop.a, source.a) proc blendColor(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba().color source = source.rgba().color blended = SetLum(source, Lum(backdrop)) result = alphaFix(backdrop, source, blended).rgba.rgbx() proc blendLuminosity(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba().color source = source.rgba().color blended = SetLum(backdrop, Lum(source)) result = alphaFix(backdrop, source, blended).rgba.rgbx() proc blendHue(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba().color source = source.rgba().color blended = SetLum(SetSat(source, Sat(backdrop)), Lum(backdrop)) result = alphaFix(backdrop, source, blended).rgba.rgbx() proc blendSaturation(backdrop, source: ColorRGBX): ColorRGBX = let backdrop = backdrop.rgba().color source = source.rgba().color blended = SetLum(SetSat(backdrop, Sat(source)), Lum(backdrop)) result = alphaFix(backdrop, source, blended).rgba.rgbx() proc blendMask*(backdrop, source: ColorRGBX): ColorRGBX = let k = source.a.uint32 result.r = ((backdrop.r * k) div 255).uint8 result.g = ((backdrop.g * k) div 255).uint8 result.b = ((backdrop.b * k) div 255).uint8 result.a = ((backdrop.a * k) div 255).uint8 proc blendSubtractMask(backdrop, source: ColorRGBX): ColorRGBX = let a = (backdrop.a.uint32 * (255 - source.a)) div 255 result.r = ((backdrop.r * a) div 255).uint8 result.g = ((backdrop.g * a) div 255).uint8 result.b = ((backdrop.b * a) div 255).uint8 result.a = a.uint8 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 result.g = ((source.g * a) div 255).uint8 result.b = ((source.b * a) div 255).uint8 result.a = a.uint8 proc blendOverwrite(backdrop, source: ColorRGBX): ColorRGBX = source # proc blendWhite(backdrop, source: ColorRGBX): ColorRGBX = # ## For testing # rgbx(255, 255, 255, 255) proc blender*(blendMode: BlendMode): Blender {.raises: [].} = ## Returns a blend function for a given blend mode. case blendMode: of bmNormal: blendNormal of bmDarken: blendDarken of bmMultiply: blendMultiply # of bmLinearBurn: blendLinearBurn of bmColorBurn: blendColorBurn of bmLighten: blendLighten of bmScreen: blendScreen # of bmLinearDodge: blendLinearDodge of bmColorDodge: blendColorDodge of bmOverlay: blendOverlay of bmSoftLight: blendSoftLight of bmHardLight: blendHardLight of bmDifference: blendDifference of bmExclusion: blendExclusion of bmHue: blendHue of bmSaturation: blendSaturation of bmColor: blendColor of bmLuminosity: blendLuminosity of bmMask: blendMask of bmOverwrite: blendOverwrite of bmSubtractMask: blendSubtractMask of bmExcludeMask: blendExcludeMask proc maskNormal(backdrop, source: uint8): uint8 = ## Blending masks blendAlpha(backdrop, source) proc maskMask(backdrop, source: uint8): uint8 = ## Masking masks ((backdrop.uint32 * source) div 255).uint8 proc maskSubtract(backdrop, source: uint8): uint8 = ((backdrop.uint32 * (255 - source)) div 255).uint8 proc maskExclude(backdrop, source: uint8): uint8 = max(backdrop, source) - min(backdrop, source) proc maskOverwrite(backdrop, source: uint8): uint8 = source proc masker*(blendMode: BlendMode): Masker {.raises: [PixieError].} = ## Returns a blend masking function for a given blend masking mode. case blendMode: of bmNormal: maskNormal of bmMask: maskMask of bmOverwrite: maskOverwrite of bmSubtractMask: maskSubtract of bmExcludeMask: maskExclude else: raise newException(PixieError, "No masker for " & $blendMode) when defined(amd64) and not defined(pixieNoSimd): type BlenderSimd* = proc(blackdrop, source: M128i): M128i {.gcsafe, raises: [].} ## Function signature returned by blenderSimd. MaskerSimd* = proc(blackdrop, source: M128i): M128i {.gcsafe, raises: [].} ## Function signature returned by maskerSimd. proc blendNormalInlineSimd*(backdrop, source: M128i): M128i {.inline.} = let alphaMask = mm_set1_epi32(cast[int32](0xff000000)) oddMask = mm_set1_epi16(cast[int16](0xff00)) div255 = mm_set1_epi16(cast[int16](0x8081)) var sourceAlpha = mm_and_si128(source, alphaMask) backdropEven = mm_slli_epi16(backdrop, 8) backdropOdd = mm_and_si128(backdrop, oddMask) sourceAlpha = mm_or_si128(sourceAlpha, mm_srli_epi32(sourceAlpha, 16)) let k = mm_sub_epi32( mm_set1_epi32(cast[int32]([0.uint8, 255, 0, 255])), sourceAlpha ) backdropEven = mm_mulhi_epu16(backdropEven, k) backdropOdd = mm_mulhi_epu16(backdropOdd, k) backdropEven = mm_srli_epi16(mm_mulhi_epu16(backdropEven, div255), 7) backdropOdd = mm_srli_epi16(mm_mulhi_epu16(backdropOdd, div255), 7) mm_add_epi8( source, mm_or_si128(backdropEven, mm_slli_epi16(backdropOdd, 8)) ) proc blendNormalSimd(backdrop, source: M128i): M128i = blendNormalInlineSimd(backdrop, source) proc blendMaskSimd(backdrop, source: M128i): M128i = let alphaMask = mm_set1_epi32(cast[int32](0xff000000)) oddMask = mm_set1_epi16(cast[int16](0xff00)) div255 = mm_set1_epi16(cast[int16](0x8081)) var sourceAlpha = mm_and_si128(source, alphaMask) backdropEven = mm_slli_epi16(backdrop, 8) backdropOdd = mm_and_si128(backdrop, oddMask) sourceAlpha = mm_or_si128(sourceAlpha, mm_srli_epi32(sourceAlpha, 16)) backdropEven = mm_mulhi_epu16(backdropEven, sourceAlpha) backdropOdd = mm_mulhi_epu16(backdropOdd, sourceAlpha) backdropEven = mm_srli_epi16(mm_mulhi_epu16(backdropEven, div255), 7) backdropOdd = mm_srli_epi16(mm_mulhi_epu16(backdropOdd, div255), 7) mm_or_si128(backdropEven, mm_slli_epi16(backdropOdd, 8)) proc blendOverwriteSimd(backdrop, source: M128i): M128i = source proc blenderSimd*(blendMode: BlendMode): BlenderSimd {.raises: [PixieError].} = ## Returns a blend function for a given blend mode with SIMD support. case blendMode: of bmNormal: blendNormalSimd of bmMask: blendMaskSimd of bmOverwrite: blendOverwriteSimd else: raise newException(PixieError, "No SIMD blender for " & $blendMode) proc hasSimdBlender*(blendMode: BlendMode): bool {.inline, raises: [].} = ## Is there a blend function for a given blend mode with SIMD support? blendMode in {bmNormal, bmMask, bmOverwrite} proc maskNormalSimd(backdrop, source: M128i): M128i = ## Blending masks let oddMask = mm_set1_epi16(cast[int16](0xff00)) v255high = mm_set1_epi16(cast[int16](255.uint16 shl 8)) div255 = mm_set1_epi16(cast[int16](0x8081)) var sourceEven = mm_slli_epi16(source, 8) sourceOdd = mm_and_si128(source, oddMask) let evenK = mm_sub_epi16(v255high, sourceEven) oddK = mm_sub_epi16(v255high, sourceOdd) var backdropEven = mm_slli_epi16(backdrop, 8) backdropOdd = mm_and_si128(backdrop, oddMask) # backdrop * k backdropEven = mm_mulhi_epu16(backdropEven, evenK) backdropOdd = mm_mulhi_epu16(backdropOdd, oddK) # div 255 backdropEven = mm_srli_epi16(mm_mulhi_epu16(backdropEven, div255), 7) backdropOdd = mm_srli_epi16(mm_mulhi_epu16(backdropOdd, div255), 7) # Shift from high to low bits sourceEven = mm_srli_epi16(sourceEven, 8) sourceOdd = mm_srli_epi16(sourceOdd, 8) let blendedEven = mm_add_epi16(sourceEven, backdropEven) blendedOdd = mm_add_epi16(sourceOdd, backdropOdd) mm_or_si128(blendedEven, mm_slli_epi16(blendedOdd, 8)) proc maskMaskSimd(backdrop, source: M128i): M128i = let oddMask = mm_set1_epi16(cast[int16](0xff00)) div255 = mm_set1_epi16(cast[int16](0x8081)) sourceEven = mm_slli_epi16(source, 8) sourceOdd = mm_and_si128(source, oddMask) var backdropEven = mm_slli_epi16(backdrop, 8) backdropOdd = mm_and_si128(backdrop, oddMask) # backdrop * source backdropEven = mm_mulhi_epu16(backdropEven, sourceEven) backdropOdd = mm_mulhi_epu16(backdropOdd, sourceOdd) # div 255 backdropEven = mm_srli_epi16(mm_mulhi_epu16(backdropEven, div255), 7) backdropOdd = mm_srli_epi16(mm_mulhi_epu16(backdropOdd, div255), 7) mm_or_si128(backdropEven, mm_slli_epi16(backdropOdd, 8)) proc maskerSimd*(blendMode: BlendMode): MaskerSimd {.raises: [PixieError].} = ## Returns a blend masking function with SIMD support. case blendMode: of bmNormal: maskNormalSimd of bmMask: maskMaskSimd of bmOverwrite: blendOverwriteSimd else: raise newException(PixieError, "No SIMD masker for " & $blendMode) proc hasSimdMasker*(blendMode: BlendMode): bool {.inline, raises: [].} = ## Is there a blend masking function with SIMD support? blendMode in {bmNormal, bmMask, bmOverwrite} when defined(release): {.pop.}