/*
~~~~~~
(most) Krita blendmodes !!
v0.2
these are all as accurate as I could get them, though I did go and simplify some for readability. I haven't gone through and tested any of them. if there are any errors, please let me know.
'src' is the "source" color, what you're drawing
'dst' is the "destination" color, what you're drawing over
throughout this, you can assume both 'src' and 'dst' to be the individual rgb components. most of these blendmodes let you represent them as a vec3, though only when branches are not involved. in most of *those* cases, you can avoid the branch as well.
all numbers are floating points (ie, float).
if a function is preceded by ':' like ':addition()', then it's refering to a blendmode. the first argument is 'src', the second is 'dst'.
in all of these, it is assumed that 'src' and 'dst' are between 0 and 1. some blendmodes may, by themselves, output a value outside 0 - 1. make sure to take this into account.
these blendmodes only really apply to the rgb channels, not alpha. you can find a run down on alpha blending here: https://en.wikipedia.org/wiki/Alpha_compositing#Description
(I'm 90% sure that is (basically) what Krita uses)
most of these are just impossible to implement with opengl's blendmodes (ie, gpu_set_blendmode_ext()). here is a rundown on implementing blendmode in shaders: https://mini.gmshaders.com/p/mini-blendmodes
I listed every blendmode in the order as they appear in the reference: https://docs.krita.org/en/reference_manual/blending_modes.html
you can find the source for Krita's blendmodes here: https://lxr.kde.org/source/graphics/krita/libs/pigment/compositeops/KoCompositeOpFunctions.h
some of Krita's blendmode reference explanations contradict what the source code does. I don't know how to explain this.
... have fun :3
:: parchii
~~~~~~
*/
// ~~~~ arithmetic ~~~~
addition := src + dst
divide := dst / src
inverse-subtract := dst - (1 - src)
multiply := src * dst
subtract := dst - src
// ~~~~ darken ~~~~
// in source and otherwise known as "color burn"
burn := {
if src == 0 return dst == 1 ? 1 : 0
return 1 - (1 - dst) / src
}
// vec3 friendly
burn := 1 - (1 - dst) / max(src, 0.0001)
easy-burn := 1 - pow(1 - min(src, 0.999999999999), dst * 1.039999999)
fog-darken-ifsillusions := {
if src < 0.5
return (1 - src) * src + src * dst
return src * dst + src - pow(src, 2)
}
// named "darken only" in source
darken := min(src, dst)
gamma-dark := {
if src == 0 return 0
return pow(dst, 1 / src)
}
// vec3 friendly
gamma-dark := pow(dst, 1 / max(src, 0.0001))
linear-burn := src + dst - 1
shade-ifsillusions := 1 - ((1 - dst) * src + sqrt(1 - src))
// ~~~~ lighten ~~~~
color-dodge := {
if src == 1 return dst == 0 ? 0 : 1
return dst / (1 - src)
}
// vec3 friendly
color-dodge := dst / (1 - min(src, 0.9999))
gamma-illumination := 1 - :gamma-dark(1 - src, 1 - dst)
gamma-light := pow(dst, src)
hard-light := {
a = src + src
if src > 0.5
a -= 1
return a + dst - a * dst
return a * dst
}
// named "lighten only" in source
lighten := max(src, dst)
// exactly the same as addition
linear-dodge := :addition(src, dst)
easy-dodge := pow(dst, (1 - min(src, 0.999999999999)) * 1.039999999)
flat-light := {
if src == 0 return 0
if :hard-mix-photoshop(1 - src, dst) == 1
return :penumbra-b(src, dst)
return :penumbra-a(src, dst)
}
fog-lighten-ifsillusions := {
if src < 0.5
return (1 - (1 - src) * src) - (1 - dst) * (1 - src)
return src - (1 - dst) * (1 - src) + pow(1 - src, 2)
}
linear-light := src * 2 + dst - 1
pnorm-a :=
pow(
pow(dst, 2.3333333333333333) +
pow(src, 2.3333333333333333),
0.428571428571434
)
pnorm-b := pow(pow(dst, 4) + pow(src, 4), 0.25)
pin-light := {
a = src * 2
return max(a - 1, min(dst, a))
}
screen := src + dst - src * dst
// this is how Krita does it. screen is more traditionally known as `1 - (1 - src) * (1 - dst)`, though it's easy to prove that both are the same.
soft-light := {
if src > 0.5
return dst + (2 * src - 1) * (sqrt(dst) - dst)
return dst - (1 - 2 * src) * dst * (1 - dst)
}
soft-light-svg := {
if src > 0.5
if dst > 0.25
a = sqrt(dst)
else
a = ((16 * dst - 12) * dst + 4) * dst
return dst + (2 * src - 1) * (a - dst)
return dst - (1 - 2 * src) * dst * (1 - dst)
}
soft-light-ifsillusions := pow(dst, pow(2, w * (0.5 - src)))
soft-light-pegtopdelphi := {
a = dst * :screen(src, dst)
b = src * dst * (1 - dst)
return a + b
}
super-light := {
if src < 0.5
a = pow(1 - dst, 2.875)
b = pow(1 - 2 * src, 2.875)
return 1 - pow(a + b, 1 / 2.875)
a = pow(dst, 2.875)
b = pow(2 * src - 1, 2.875)
return pow(a + b, 1 / 2.875)
}
tint-ifsillusions := src * (1 - dst) + sqrt(dst)
vivid-light := {
if src < 0.5
if src == 0
return dst == 1 ? 1 : 0
return 1 - (1 - dst) / (src * 2)
if src == 1
return dst == 0 ? 0 : 1
return dst / (2 * (1 - src))
}
// ~~~~ mix ~~~~
allanon := (dst + src) * 0.5
interpolation := {
if src == 0 or dst == 0 return 0
return 0.5 - 0.25 * cos(pi * src) - 0.25 * cos(pi * dst)
}
// vec3 friendly
interpolation :=
0.5 - 0.25 * cos(pi * max(src, 0.0001)) - 0.25 * cos(pi * max(dst, 0.0001))
// named "interpolationB" in source
interpolation2 :=
:interpolation(
:interpolation(src, dst),
:interpolation(src, dst)
)
geometric-mean := sqrt(dst * src)
grain-extract := dst - src + 0.5
grain-merge := dst + src - 0.5
hard-mix := dst > 0.5 ? :color-dodge(src, dst) : :color-burn(src, dst)
hard-mix-photoshop := src + dst > 1 ? 1 : 0
hard-mix-softer-photoshop := 3 * dst - 2 * (1 - src)
hard-overlay := {
if src == 1 return 1
if src > 0.5
return :divide(1 - (2 * src - 1), dst)
return 2 * src * dst
}
overlay := :hard-light(dst, src)
parallel := {
if src == 0 or dst == 0 return 0
return 2 / (1 / dst + 1 / src)
}
// vec3 friendly
parallel := 2 / (1 / max(src, 0.0001) + 1 / max(dst, 0.0001))
penumbra-a := :penumbra-b(dst, src)
penumbra-b := {
if dst == 1 return 1
if dst + src < 1
return :color-dodge(dst, src) / 2
if src == 0 return 0
return 1 - (1 - dst) / src / 2
}
penumbra-c := :penumbra-d(dst, src)
penumbra-d := {
if dst == 1 return 1
return :arc-tangent(src, 1 - dst)
}
// vec3 friendly version
penumbra-d := :arc-tangent(src, 1 - dst)
// ~~~~ negative ~~~~
additive-subtractive := abs(sqrt(dst) - sqrt(src))
arc-tangent := {
if dst == 0 return src == 0 ? 0 : 1
return 2 * atan(src / dst) / pi
}
// vec3 friendly version
arc-tangent := 2 * atan(src / max(dst, 0.0001)) / pi
difference := max(src, dst) - min(src, dst)
equivalence := 1 - abs(dst - src)
exclusion := dst + src - (src * dst * 2)
negation := 1 - abs(1 - src - dst)
// "quadratic"
freeze := :heat(dst, src)
// named "reeze" in source
freeze-reflect := :glow-heat(dst, src)
glow := {
if dst == 1 return 1
return src * src / (1 - dst)
}
// vec3 friendly
glow := src * src / max(1 - dst, 0.0001)
// named "gleat" in source
glow-heat := {
if dst == 1 return 1
if :hard-mix-photoshop(src, dst) == 1
return :glow(src, dst)
return :heat(src, dst)
}
heat := {
if src == 1 return 1
if dst == 0 return 0
a = 1 - src
return 1 - (a * a / dst)
}
// vec3 friendly
heat := {
a = 1 - src
return 1 - (a * a / max(dst, 0.0001))
}
// named "helow" in source
heat-glow := {
if :hard-mix-photoshop(src, dst) == 1
return :heat(src, dst)
if src == 0 return 0
return :glow(src, dst)
}
// named "fhyrd" in source
heat-glow-freeze-reflect-hybrid :=
:allanon(:freeze-reflect(src, dst), :heat-glow(src, dst))
reflect := :glow(dst, src)
// named "frect" in source
freeze-reflect := {
if hard-mix-photoshop(src, dst) == 1
return :freeze(src, dst)
if dst == 0 return 0
return :reflect(src, dst)
}