• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2017 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "gm.h"
9 #include "sk_tool_utils.h"
10 
11 // Hue, Saturation, Color, and Luminosity blend modes are oddballs.
12 // They nominally convert their inputs to unpremul, then to HSL, then
13 // mix-and-match H,S,and L from Src and Dst, then convert back, then premul.
14 //
15 // In practice that's slow, so instead we pick the color with the correct
16 // Hue, and then (approximately) apply the other's Saturation and/or Luminosity.
17 // This isn't just an optimization... it's how the modes are specified.
18 //
19 // Each mode's name describes the Src H,S,L components to keep, taking the
20 // others from Dst, where Color == Hue + Saturation.  Color and Luminosity
21 // are each other's complements; Hue and Saturation have no complement.
22 //
23 // All these modes were originally designed to operate on gamma-encoded values,
24 // and that's what everyone's used to seeing.  It's unclear wehther they make
25 // any sense in a gamma-correct world.  They certainly won't look at all similar.
26 //
27 // We have had many inconsistent implementations of these modes.
28 // This GM tries to demonstrate unambigously how they should work.
29 //
30 // To go along with our inconsistent implementations, there are two slightly
31 // inconsistent specifications of how to perform these blends,
32 //   web: https://www.w3.org/TR/compositing-1/#blendingnonseparable
33 //   KHR: https://www.khronos.org/registry/OpenGL/extensions/KHR/KHR_blend_equation_advanced.txt
34 // It looks like these are meant to be identical, but they disagree on at least ClipColor().
35 //
36 // I think the KHR version is just wrong... it produces values >1.  So we use the web version.
37 
min(float r,float g,float b)38 static float min(float r, float g, float b) { return SkTMin(r, SkTMin(g, b)); }
max(float r,float g,float b)39 static float max(float r, float g, float b) { return SkTMax(r, SkTMax(g, b)); }
40 
sat(float r,float g,float b)41 static float sat(float r, float g, float b) { return max(r,g,b) - min(r,g,b); }
lum(float r,float g,float b)42 static float lum(float r, float g, float b) { return r*0.30f + g*0.59f + b*0.11f; }
43 
44 // The two SetSat() routines in the specs look different, but they're logically equivalent.
45 // Both map the minimum channel to 0, maximum to s, and scale the middle proportionately.
46 // The KHR version has done a better job at simplifying its math, so we use it here.
set_sat(float * r,float * g,float * b,float s)47 static void set_sat(float* r, float* g, float* b, float s) {
48     float mn = min(*r,*g,*b),
49           mx = max(*r,*g,*b);
50     auto channel = [=](float c) {
51         return mx == mn ? 0
52                         : (c - mn) * s / (mx - mn);
53     };
54     *r = channel(*r);
55     *g = channel(*g);
56     *b = channel(*b);
57 }
clip_color(float * r,float * g,float * b)58 static void clip_color(float* r, float* g, float* b) {
59     float l  = lum(*r,*g,*b),
60           mn = min(*r,*g,*b),
61           mx = max(*r,*g,*b);
62     auto clip = [=](float c) {
63         if (mn < 0) { c = l + (c - l) * (    l) / (l - mn); }
64         if (mx > 1) { c = l + (c - l) * (1 - l) / (mx - l); }
65         SkASSERT(-0.0001f <  c);   // This may end up very slightly negative...
66         SkASSERT(       c <= 1);
67         return c;
68     };
69     *r = clip(*r);
70     *g = clip(*g);
71     *b = clip(*b);
72 }
set_lum(float * r,float * g,float * b,float l)73 static void set_lum(float* r, float* g, float* b, float l) {
74     float diff = l - lum(*r,*g,*b);
75     *r += diff;
76     *g += diff;
77     *b += diff;
78     clip_color(r,g,b);
79 }
80 
81 
hue(float dr,float dg,float db,float * sr,float * sg,float * sb)82 static void hue(float  dr, float  dg, float  db,
83                 float* sr, float* sg, float* sb) {
84     // Hue of Src, Saturation and Luminosity of Dst.
85     float R = *sr,
86           G = *sg,
87           B = *sb;
88     set_sat(&R,&G,&B, sat(dr,dg,db));
89     set_lum(&R,&G,&B, lum(dr,dg,db));
90     *sr = R;
91     *sg = G;
92     *sb = B;
93 }
94 
saturation(float dr,float dg,float db,float * sr,float * sg,float * sb)95 static void saturation(float  dr, float  dg, float  db,
96                        float* sr, float* sg, float* sb) {
97     // Saturation of Src, Hue and Luminosity of Dst
98     float R = dr,
99           G = dg,
100           B = db;
101     set_sat(&R,&G,&B, sat(*sr,*sg,*sb));
102     set_lum(&R,&G,&B, lum( dr, dg, db));  // This may seem redundant, but it is not.
103     *sr = R;
104     *sg = G;
105     *sb = B;
106 }
107 
color(float dr,float dg,float db,float * sr,float * sg,float * sb)108 static void color(float  dr, float  dg, float  db,
109                   float* sr, float* sg, float* sb) {
110     // Hue and Saturation of Src, Luminosity of Dst.
111     float R = *sr,
112           G = *sg,
113           B = *sb;
114     set_lum(&R,&G,&B, lum(dr,dg,db));
115     *sr = R;
116     *sg = G;
117     *sb = B;
118 }
119 
luminosity(float dr,float dg,float db,float * sr,float * sg,float * sb)120 static void luminosity(float  dr, float  dg, float  db,
121                        float* sr, float* sg, float* sb) {
122     // Luminosity of Src, Hue and Saturation of Dst.
123     float R = dr,
124           G = dg,
125           B = db;
126     set_lum(&R,&G,&B, lum(*sr,*sg,*sb));
127     *sr = R;
128     *sg = G;
129     *sb = B;
130 }
131 
blend(SkColor dst,SkColor src,void (* mode)(float,float,float,float *,float *,float *),bool legacy)132 static SkColor blend(SkColor dst, SkColor src,
133                      void (*mode)(float,float,float, float*,float*,float*),
134                      bool legacy) {
135 
136     SkASSERT(SkColorGetA(dst) == 0xff
137           && SkColorGetA(src) == 0xff);   // Not fundamental, just simplifying for this GM.
138 
139     auto to_float = [&](SkColor c) {
140         if (legacy) {
141             return SkColor4f{
142                 SkColorGetR(c) * (1/255.0f),
143                 SkColorGetG(c) * (1/255.0f),
144                 SkColorGetB(c) * (1/255.0f),
145                 1.0f,
146             };
147         }
148         return SkColor4f::FromColor(c);
149     };
150 
151     SkColor4f d = to_float(dst),
152               s = to_float(src);
153 
154     mode( d.fR,  d.fG,  d.fB,
155          &s.fR, &s.fG, &s.fB);
156 
157     if (legacy) {
158         return SkColorSetRGB(s.fR * 255.0f + 0.5f,
159                              s.fG * 255.0f + 0.5f,
160                              s.fB * 255.0f + 0.5f);
161     }
162     return s.toSkColor();
163 }
164 
165 DEF_SIMPLE_GM(hsl, canvas, 600, 100) {
166     SkPaint label;
167     sk_tool_utils::set_portable_typeface(&label);
168     label.setAntiAlias(true);
169 
170     const char* comment = "HSL blend modes are correct when you see no circles in the squares.";
171     canvas->drawText(comment, strlen(comment), 10,10, label);
172 
173     // Just to keep things reaaaal simple, we'll only use opaque colors.
174     SkPaint bg, fg;
175     bg.setColor(0xff00ff00);  // Fully-saturated bright green,  H = 120°, S = 100%, L =  50%.
176     fg.setColor(0xff7f3f7f);  // Partly-saturated dim magenta,  H = 300°, S = ~33%, L = ~37%.
177 
178     struct {
179         SkBlendMode mode;
180         void (*reference)(float,float,float, float*,float*,float*);
181     } tests[] = {
182         { SkBlendMode::kSrc,        nullptr    },
183         { SkBlendMode::kDst,        nullptr    },
184         { SkBlendMode::kHue,        hue        },
185         { SkBlendMode::kSaturation, saturation },
186         { SkBlendMode::kColor,      color      },
187         { SkBlendMode::kLuminosity, luminosity },
188     };
189     bool legacy = !canvas->imageInfo().colorSpace();
190     for (auto test : tests) {
191         canvas->drawRect({20,20,80,80}, bg);
192 
193         fg.setBlendMode(test.mode);
194         canvas->drawRect({20,20,80,80}, fg);
195 
196         if (test.reference) {
197             SkPaint ref;
198             ref.setColor(blend(bg.getColor(), fg.getColor(), test.reference, legacy));
199             canvas->drawCircle(50,50, 20, ref);
200         }
201 
202         const char* name = SkBlendMode_Name(test.mode);
203         canvas->drawText(name, strlen(name), 20,90, label);
204 
205         canvas->translate(100,0);
206     }
207 }
208