sw_engine: support hue, color, saturation, luminosity, hardmix blends

- Hue: Creates a result color with the luminance and saturation
  of the base color and the hue of the blend color.

- Color: Creates a result color with the luminance of the base color
  and the hue and saturation of the blend color. This preserves the gray
  levels in the image and is useful for coloring monochrome images and
  for tinting color images.

- Luminosity: reates a result color with the hue and saturation of the base color
  and the luminance of the blend color. This mode creates the inverse effect of
  Color mode.

- Saturation: Creates a result color with the luminance and hue of
  the base color and the saturation of the blend color. Painting with this mode
  in an area with no (0) saturation (gray) causes no change.

- HardMix: Adds the bottom & top. If the resulting sum for a channel is 255 or
  greater, it receives a value of 255; if less than 255, a value of 0.

issue: https://github.com/thorvg/thorvg/issues/2701
This commit is contained in:
Hermet Park 2025-07-22 10:49:17 +09:00
parent 16604a873a
commit 4768331b3b
8 changed files with 143 additions and 22 deletions

View file

@ -162,12 +162,12 @@ struct UserExample : tvgexam::Example
blender(canvas, "SoftLight", tvg::BlendMethod::SoftLight, 900.0f, 0.0f, data);
blender(canvas, "Difference", tvg::BlendMethod::Difference, 900.0f, 150.0f, data);
blender(canvas, "Exclusion", tvg::BlendMethod::Exclusion, 900.0f, 300.0f, data);
blender(canvas, "Hue (Not Supported)", tvg::BlendMethod::Hue, 900.0f, 450.0f, data);
blender(canvas, "Saturation (Not Supported)", tvg::BlendMethod::Saturation, 900.0f, 600.0f, data);
blender(canvas, "Color (Not Supported)", tvg::BlendMethod::Color, 900.0f, 750.0f, data);
blender(canvas, "Luminosity (Not Supported)", tvg::BlendMethod::Luminosity, 900.0f, 900.0f, data);
blender(canvas, "Hue", tvg::BlendMethod::Hue, 900.0f, 450.0f, data);
blender(canvas, "Saturation", tvg::BlendMethod::Saturation, 900.0f, 600.0f, data);
blender(canvas, "Color", tvg::BlendMethod::Color, 900.0f, 750.0f, data);
blender(canvas, "Luminosity", tvg::BlendMethod::Luminosity, 900.0f, 900.0f, data);
blender(canvas, "Add", tvg::BlendMethod::Add, 900.0f, 1050.0f, data);
blender(canvas, "HardMix (Not Supported)", tvg::BlendMethod::HardMix, 900.0f, 1200.0f, data);
blender(canvas, "HardMix", tvg::BlendMethod::HardMix, 900.0f, 1200.0f, data);
free(data);

View file

@ -209,12 +209,12 @@ enum class BlendMethod : uint8_t
SoftLight, ///< The same as Overlay but with applying pure black or white does not result in pure black or white. (255 - 2 * S) * (D * D) + (2 * S * D)
Difference, ///< Subtracts the bottom layer from the top layer or the other way around, to always get a non-negative value. (S - D) if (S > D), otherwise (D - S)
Exclusion, ///< The result is twice the product of the top and bottom layers, subtracted from their sum. S + D - (2 * S * D)
Hue, ///< Reserved. Not supported.
Saturation, ///< Reserved. Not supported.
Color, ///< Reserved. Not supported.
Luminosity, ///< Reserved. Not supported.
Hue, ///< Combine with HSL(Sh + Ds + Dl) then convert it to RGB.
Saturation, ///< Combine with HSL(Dh + Ss + Dl) then convert it to RGB.
Color, ///< Combine with HSL(Sh + Ss + Dl) then convert it to RGB.
Luminosity, ///< Combine with HSL(Dh + Ds + Sl) then convert it to RGB.
Add, ///< Simply adds pixel values of one layer with the other. (S + D)
HardMix, ///< Reserved. Not supported.
HardMix, ///< Adds S and D; result is 255 if the sum is greater than or equal to 255, otherwise 0.
Composition = 255 ///< Used for intermediate composition. @since 1.0
};

View file

@ -27,11 +27,6 @@ namespace tvg
void hsl2rgb(float h, float s, float l, uint8_t& r, uint8_t& g, uint8_t& b)
{
s = tvg::clamp(s, 0.0f, 1.0f);
l = tvg::clamp(l, 0.0f, 1.0f);
auto tr = 0.0f, tg = 0.0f, tb = 0.0f;
if (tvg::zero(s)) {
r = g = b = (uint8_t)nearbyint(l * 255.0f);
return;
@ -53,6 +48,7 @@ void hsl2rgb(float h, float s, float l, uint8_t& r, uint8_t& g, uint8_t& b)
auto vsf = v * sv * f;
auto t = p + vsf;
auto q = v - vsf;
float tr, tg, tb;
switch (i) {
case 0: tr = v; tg = t; tb = p; break;
@ -61,7 +57,7 @@ void hsl2rgb(float h, float s, float l, uint8_t& r, uint8_t& g, uint8_t& b)
case 3: tr = p; tg = q; tb = v; break;
case 4: tr = t; tg = p; tb = v; break;
case 5: tr = v; tg = p; tb = q; break;
default: break;
default: tr = tg = tb = 0.0f; break;
}
r = (uint8_t)nearbyint(tr * 255.0f);
g = (uint8_t)nearbyint(tg * 255.0f);

View file

@ -645,7 +645,7 @@ static bool _toColor(const char* str, uint8_t& r, uint8_t&g, uint8_t& b, char**
hsl.l /= 100.0f;
brightness = _skipSpace(brightness + 1, nullptr);
if (brightness && brightness[0] == ')' && brightness[1] == '\0') {
hsl2rgb(hsl.h, hsl.s, hsl.l, r, g, b);
hsl2rgb(hsl.h, tvg::clamp(hsl.s, 0.0f, 1.0f), tvg::clamp(hsl.l, 0.0f, 1.0f), r, g, b);
return true;
}
}

View file

@ -26,6 +26,7 @@
#include <algorithm>
#include "tvgCommon.h"
#include "tvgMath.h"
#include "tvgColor.h"
#include "tvgRender.h"
#define SW_CURVE_TYPE_POINT 0
@ -564,6 +565,80 @@ static inline uint32_t opBlendSoftLight(uint32_t s, uint32_t d)
return BLEND_PRE(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a);
}
void rasterRGB2HSL(uint8_t r, uint8_t g, uint8_t b, float* h, float* s, float* l);
static inline uint32_t opBlendHue(uint32_t s, uint32_t d)
{
RenderColor o;
if (!BLEND_UPRE(d, o)) return s;
float sh, ds, dl;
rasterRGB2HSL(C1(s), C2(s), C3(s), &sh, 0, 0);
rasterRGB2HSL(o.r, o.g, o.b, 0, &ds, &dl);
uint8_t r, g, b;
hsl2rgb(sh, ds, dl, r, g, b);
return BLEND_PRE(JOIN(255, r, g, b), s, o.a);
}
static inline uint32_t opBlendSaturation(uint32_t s, uint32_t d)
{
RenderColor o;
if (!BLEND_UPRE(d, o)) return s;
float dh, ss, dl;
rasterRGB2HSL(C1(s), C2(s), C3(s), 0, &ss, 0);
rasterRGB2HSL(o.r, o.g, o.b, &dh, 0, &dl);
uint8_t r, g, b;
hsl2rgb(dh, ss, dl, r, g, b);
return BLEND_PRE(JOIN(255, r, g, b), s, o.a);
}
static inline uint32_t opBlendColor(uint32_t s, uint32_t d)
{
RenderColor o;
if (!BLEND_UPRE(d, o)) return s;
float sh, ss, dl;
rasterRGB2HSL(C1(s), C2(s), C3(s), &sh, &ss, 0);
rasterRGB2HSL(o.r, o.g, o.b, 0, 0, &dl);
uint8_t r, g, b;
hsl2rgb(sh, ss, dl, r, g, b);
return BLEND_PRE(JOIN(255, r, g, b), s, o.a);
}
static inline uint32_t opBlendLuminosity(uint32_t s, uint32_t d)
{
RenderColor o;
if (!BLEND_UPRE(d, o)) return s;
float dh, ds, sl;
rasterRGB2HSL(C1(s), C2(s), C3(s), 0, 0, &sl);
rasterRGB2HSL(o.r, o.g, o.b, &dh, &ds, 0);
uint8_t r, g, b;
hsl2rgb(dh, ds, sl, r, g, b);
return BLEND_PRE(JOIN(255, r, g, b), s, o.a);
}
static inline uint32_t opBlendHardMix(uint32_t s, uint32_t d)
{
RenderColor o;
if (!BLEND_UPRE(d, o)) return s;
auto f = [](uint8_t s, uint8_t d) {
return (s + d >= 255) ? 255 : 0;
};
return BLEND_PRE(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a);
}
int64_t mathMultiply(int64_t a, int64_t b);
int64_t mathDivide(int64_t a, int64_t b);
@ -679,4 +754,6 @@ bool effectTint(SwCompositor* cmp, const RenderEffectTint* params, bool direct);
void effectTritoneUpdate(RenderEffectTritone* effect);
bool effectTritone(SwCompositor* cmp, const RenderEffectTritone* params, bool direct);
#endif /* _TVG_SW_COMMON_H_ */

View file

@ -1769,4 +1769,40 @@ void rasterXYFlip(uint32_t* src, uint32_t* dst, int32_t stride, int32_t w, int32
}
}
}
}
}
//TODO: can be moved in tvgColor
void rasterRGB2HSL(uint8_t r, uint8_t g, uint8_t b, float* h, float* s, float* l)
{
auto rf = r / 255.0f;
auto gf = g / 255.0f;
auto bf = b / 255.0f;
auto maxVal = std::max(std::max(rf, gf), bf);
auto minVal = std::min(std::min(rf, gf), bf);
auto delta = maxVal - minVal;
//lightness
float t;
if (l || s) {
t = (maxVal + minVal) * 0.5f;
if (l) *l = t;
}
if (tvg::zero(delta)) {
if (h) *h = 0.0f;
if (s) *s = 0.0f;
} else {
//saturation
if (s) {
*s = (t < 0.5f) ? (delta / (maxVal + minVal)) : (delta / (2.0f - maxVal - minVal));
}
//hue
if (h) {
if (maxVal == rf) *h = (gf - bf) / delta + (gf < bf ? 6.0f : 0.0f);
else if (maxVal == gf) *h = (bf - rf) / delta + 2.0f;
else *h = (rf - gf) / delta + 4.0f;
*h *= 60.0f; //directly convert to degrees
}
}
}

View file

@ -564,9 +564,24 @@ bool SwRenderer::blend(BlendMethod method)
case BlendMethod::Exclusion:
surface->blender = opBlendExclusion;
break;
case BlendMethod::Hue:
surface->blender = opBlendHue;
break;
case BlendMethod::Saturation:
surface->blender = opBlendSaturation;
break;
case BlendMethod::Color:
surface->blender = opBlendColor;
break;
case BlendMethod::Luminosity:
surface->blender = opBlendLuminosity;
break;
case BlendMethod::Add:
surface->blender = opBlendAdd;
break;
case BlendMethod::HardMix:
surface->blender = opBlendHardMix;
break;
default:
TVGLOG("SW_ENGINE", "Non supported blending option = %d", (int) method);
surface->blender = nullptr;

View file

@ -429,10 +429,7 @@ uint8_t Paint::opacity() const noexcept
Result Paint::blend(BlendMethod method) noexcept
{
//TODO: Remove later
if (method == BlendMethod::Hue || method == BlendMethod::Saturation || method == BlendMethod::Color || method == BlendMethod::Luminosity || method == BlendMethod::HardMix) return Result::NonSupport;
if (method == BlendMethod::Composition || method <= BlendMethod::HardMix) {
if (method <= BlendMethod::HardMix || method == BlendMethod::Composition) {
pImpl->blend(method);
return Result::Success;
}