aboutsummaryrefslogtreecommitdiffhomepage
path: root/gm/hsl.cpp
diff options
context:
space:
mode:
authorGravatar Mike Klein <mtklein@chromium.org>2017-05-06 14:01:22 -0400
committerGravatar Skia Commit-Bot <skia-commit-bot@chromium.org>2017-05-06 19:20:54 +0000
commit5b4e2775554c893a4b620b613a301f8c82da5238 (patch)
treea78487eb264afa828d176aafa8bfd35c045bce1d /gm/hsl.cpp
parentc6820383b2526de95296ed8436f76333e0651d75 (diff)
update hsl GM
- Compare against reference versions of the blend modes. - Point out differences between KHR and web versions. It looks like the KHR ClipColor() is indeed wrong. Change-Id: I650821b2d4815c493dddeb836c69875650926d7b Reviewed-on: https://skia-review.googlesource.com/15654 Commit-Queue: Mike Klein <mtklein@chromium.org> Reviewed-by: Mike Klein <mtklein@chromium.org>
Diffstat (limited to 'gm/hsl.cpp')
-rw-r--r--gm/hsl.cpp180
1 files changed, 167 insertions, 13 deletions
diff --git a/gm/hsl.cpp b/gm/hsl.cpp
index 2ffc0092f3..8d200f7aac 100644
--- a/gm/hsl.cpp
+++ b/gm/hsl.cpp
@@ -12,22 +12,172 @@
// They nominally convert their inputs to unpremul, then to HSL, then
// mix-and-match H,S,and L from Src and Dst, then convert back, then premul.
//
+// In practice that's slow, so instead we pick the color with the correct
+// Hue, and then (approximately) apply the other's Saturation and/or Luminosity.
+// This isn't just an optimization... it's how the modes are specified.
+//
// Each mode's name describes the Src H,S,L components to keep, taking the
// others from Dst, where Color == Hue + Saturation. Color and Luminosity
// are each other's complements; Hue and Saturation have no complement.
//
+// All these modes were originally designed to operate on gamma-encoded values,
+// and that's what everyone's used to seeing. It's unclear wehther they make
+// any sense in a gamma-correct world. They certainly won't look at all similar.
+//
// We have had many inconsistent implementations of these modes.
// This GM tries to demonstrate unambigously how they should work.
//
-// TODO: double- and triple-check expected colors
-// TODO: how does gamma-correction factor into this?
+// To go along with our inconsistent implementations, there are two slightly
+// inconsistent specifications of how to perform these blends,
+// web: https://www.w3.org/TR/compositing-1/#blendingnonseparable
+// KHR: https://www.khronos.org/registry/OpenGL/extensions/KHR/KHR_blend_equation_advanced.txt
+// It looks like these are meant to be identical, but they disagree on at least ClipColor().
+// I think the KHR version is just wrong... it produces values >1.
+//
+// We'll draw reference colors according to both specs, testing colors where they differ.
+
+static float min(float r, float g, float b) { return SkTMin(r, SkTMin(g, b)); }
+static float max(float r, float g, float b) { return SkTMax(r, SkTMax(g, b)); }
+
+static float sat(float r, float g, float b) { return max(r,g,b) - min(r,g,b); }
+static float lum(float r, float g, float b) { return r*0.30f + g*0.59f + b*0.11f; }
+
+// The two SetSat() routines in the specs look different, but they're logically equivalent.
+// Both map the minimum channel to 0, maximum to s, and scale the middle proportionately.
+// The KHR version has done a better job at simplifying its math, so we use it here.
+static void set_sat(float* r, float* g, float* b, float s) {
+ auto channel = [&](float c) {
+ float mn = min(*r,*g,*b),
+ mx = max(*r,*g,*b);
+ return mx == mn ? 0
+ : (c - mn) * s / (mx - mn);
+ };
+ *r = channel(*r);
+ *g = channel(*g);
+ *b = channel(*b);
+}
+
+static void set_lum(float* r, float* g, float* b, float l) {
+ float diff = l - lum(*r,*g,*b);
+ *r += diff;
+ *g += diff;
+ *b += diff;
+ // We do clip_color() here as specified, just moved from set_lum() to blend().
+}
+
+static void clip_color_web(float* r, float* g, float* b) {
+ auto clip = [&](float c) {
+ float l = lum(*r,*g,*b),
+ mn = min(*r,*g,*b),
+ mx = max(*r,*g,*b);
+ if (mn < 0) { c = l + (c - l) * ( l) / (l - mn); }
+ if (mx > 1) { c = l + (c - l) * (1 - l) / (mx - l); } // <--- notice "1-l"
+ SkASSERT(0 <= c);
+ SkASSERT(c <= 1);
+ return c;
+ };
+ *r = clip(*r);
+ *g = clip(*g);
+ *b = clip(*b);
+}
+static void clip_color_KHR(float* r, float* g, float* b) {
+ auto clip = [&](float c) {
+ float l = lum(*r,*g,*b),
+ mn = min(*r,*g,*b),
+ mx = max(*r,*g,*b);
+ if (mn < 0) { c = l + (c - l) * l / (l - mn); }
+ if (mx > 1) { c = l + (c - l) * l / (mx - l); } // <--- notice "l"
+ SkASSERT(0 <= c);
+ //SkASSERT(c <= 1);
+ return c;
+ };
+ *r = clip(*r);
+ *g = clip(*g);
+ *b = clip(*b);
+}
+
+
+static void hue(float dr, float dg, float db,
+ float* sr, float* sg, float* sb) {
+ // Hue of Src, Saturation and Luminosity of Dst.
+ float R = *sr,
+ G = *sg,
+ B = *sb;
+ set_sat(&R,&G,&B, sat(dr,dg,db));
+ set_lum(&R,&G,&B, lum(dr,dg,db));
+ *sr = R;
+ *sg = G;
+ *sb = B;
+}
+
+static void saturation(float dr, float dg, float db,
+ float* sr, float* sg, float* sb) {
+ // Saturation of Src, Hue and Luminosity of Dst
+ float R = dr,
+ G = dg,
+ B = db;
+ set_sat(&R,&G,&B, sat(*sr,*sg,*sb));
+ set_lum(&R,&G,&B, lum( dr, dg, db)); // This may seem redundant. TODO: is it?
+ *sr = R;
+ *sg = G;
+ *sb = B;
+}
+
+static void color(float dr, float dg, float db,
+ float* sr, float* sg, float* sb) {
+ // Hue and Saturation of Src, Luminosity of Dst.
+ float R = *sr,
+ G = *sg,
+ B = *sb;
+ set_lum(&R,&G,&B, lum(dr,dg,db));
+ *sr = R;
+ *sg = G;
+ *sb = B;
+}
+
+static void luminosity(float dr, float dg, float db,
+ float* sr, float* sg, float* sb) {
+ // Luminosity of Src, Hue and Saturation of Dst.
+ float R = dr,
+ G = dg,
+ B = db;
+ set_lum(&R,&G,&B, lum(*sr,*sg,*sb));
+ *sr = R;
+ *sg = G;
+ *sb = B;
+}
+
+static SkColor blend(SkColor dst, SkColor src,
+ void (*mode)(float,float,float, float*,float*,float*),
+ void (*clip_color)(float*,float*,float*)) {
+
+ SkASSERT(SkColorGetA(dst) == 0xff
+ && SkColorGetA(src) == 0xff); // Not fundamental, just simplifying for this GM.
+
+ float dr = SkColorGetR(dst) * (1/255.0f),
+ dg = SkColorGetG(dst) * (1/255.0f),
+ db = SkColorGetB(dst) * (1/255.0f);
+ float sr = SkColorGetR(src) * (1/255.0f),
+ sg = SkColorGetG(src) * (1/255.0f),
+ sb = SkColorGetB(src) * (1/255.0f);
+
+ mode(dr,dg,db, &sr,&sg,&sb);
+ clip_color(&sr,&sg,&sb);
+
+ // We need to be a little careful here to show off clip_color_KHR()'s overflow
+ // while avoiding SkASSERTs inside SkColorSetRGB().
+ return SkColorSetRGB((int)(sr * 255.0f + 0.5f) & 0xff,
+ (int)(sg * 255.0f + 0.5f) & 0xff,
+ (int)(sb * 255.0f + 0.5f) & 0xff);
+}
DEF_SIMPLE_GM(hsl, canvas, 600, 100) {
SkPaint label;
sk_tool_utils::set_portable_typeface(&label);
label.setAntiAlias(true);
- const char* comment = "HSL blend modes are correct when you see no circles in the squares.";
+ const char* comment = "HSL blend modes are correct when you see no circles in the squares. "
+ "2 circles means the standards disagree.";
canvas->drawText(comment, strlen(comment), 10,10, label);
// Just to keep things reaaaal simple, we'll only use opaque colors.
@@ -37,14 +187,14 @@ DEF_SIMPLE_GM(hsl, canvas, 600, 100) {
struct {
SkBlendMode mode;
- SkColor expected;
+ void (*reference)(float,float,float, float*,float*,float*);
} tests[] = {
- { SkBlendMode::kSrc, fg.getColor() },
- { SkBlendMode::kDst, bg.getColor() },
- { SkBlendMode::kHue, 0xffff00ff }, // bright magenta, H = 300°, S = 100%, L = 50%
- { SkBlendMode::kSaturation, 0xff55aa55 }, // a duller green, H = 120°, S = ~33%, L = 50%
- { SkBlendMode::kColor, 0xffaa55aa }, // a lighter fg, H = 300°, S = ~33%, L = 50%
- { SkBlendMode::kLuminosity, 0xff00bd00 }, // a darker bg, H = 120°, S = 100%, L = ~37%
+ { SkBlendMode::kSrc, nullptr },
+ { SkBlendMode::kDst, nullptr },
+ { SkBlendMode::kHue, hue },
+ { SkBlendMode::kSaturation, saturation },
+ { SkBlendMode::kColor, color },
+ { SkBlendMode::kLuminosity, luminosity },
};
for (auto test : tests) {
canvas->drawRect({20,20,80,80}, bg);
@@ -52,9 +202,13 @@ DEF_SIMPLE_GM(hsl, canvas, 600, 100) {
fg.setBlendMode(test.mode);
canvas->drawRect({20,20,80,80}, fg);
- SkPaint expected;
- expected.setColor(test.expected);
- canvas->drawCircle(50,50, 20, expected);
+ if (test.reference) {
+ SkPaint web,KHR;
+ web.setColor(blend(bg.getColor(), fg.getColor(), test.reference, clip_color_web));
+ KHR.setColor(blend(bg.getColor(), fg.getColor(), test.reference, clip_color_KHR));
+ canvas->drawCircle(50,50, 20, web);
+ canvas->drawCircle(50,50, 10, KHR); // This circle may be very wrong.
+ }
const char* name = SkBlendMode_Name(test.mode);
canvas->drawText(name, strlen(name), 20,90, label);