.NET 深度指南:Colors
作者 | Peter Huber
譯者 | 王強
策劃 | 丁曉昀
我不知道你們是什么情況,但我自己在過去多年中都因為.NET 色彩(Colors)類中可用的色彩數量有限而頭痛不已,為此我試圖用 ColorPickers 獲得匹配的色彩并努力去理解各種色彩模型。為了讓我的生活輕松一些,我寫了幾個小方法可以把任何色彩改成黑白的,還有一個方法可以混合色彩。有了這些方法后,我得到了匹配度很高的色彩,有點像 GradientBrush 中的漸變效果。
然后我給自己找了個麻煩,就是寫這篇文章來幫助其他人使用我的方法。為了解釋這些方法的原理,我別無選擇,只能詳細調查背后到底發生了什么。所以這篇文章主要是關于色彩、色彩模型、色調、亮度之類的東西,但解釋它們時用的是讓軟件開發者不需要數學或物理專業背景就能理解的簡單術語。
如果你已經對色彩有了扎實的了解,你可以直接跳到“精確生成你自己的色彩”這一章,那里有實際的代碼。
色彩空間 HSB:色調、飽和度和亮度
我們可能都知道,電腦屏幕上的色彩是由像素生成的,每個像素由 3 個點組成,分別發出紅、綠、藍三色光,所以它們被叫做 R、G、B 像素點。然而,這里已經有了第一個誤解,因為在實際情況下 G 不是 Colors.Green,而是 Colors.Lime(黃綠)。人眼對色彩有三種不同的感受器,所以人們選擇了 R、G 和 B 的色彩(色調)來與這些感受器很好地匹配起來,這里匹配的是黃綠而不是“正?!钡木G色。有了 RGB 像素,顯示器可以生成人眼可以感受到的大多數色彩——當然只是大多數。與太陽等光源相比,這些色彩的“強度”(亮度)相當有限。
每種色彩都是由 3 個點各自發出的光的多少來定義的。一個點的數值可以介于 0(不發光)和 255(或十六進制的 0xFF)之間,255 意味著全強度發光。為了看到其中一種原色,例如紅色,R 要被設置為 255,G 和 B 被設置為 0,這樣就可以得到最亮的紅色。如果我們想要一個暗一些的紅色,只需降低 R 的值即可。一旦 R 為 0,得到的色彩就是黑色的,因為沒有任何點發射出任何光線。這是一個改變色彩亮度的例子,而亮度是每一種色彩都有的三個屬性之一。有趣的是,亮度的定義不是從 0 到 255,而是從 0 到 1 或 0 到 100%。
色彩的另一個屬性被稱為色調。色調為黃色、橙色、紅色等色彩分配了不同的數字。當 3 個點中的一個是 255,一個是 0,而“中間”(第三個)為任何數值時,我們可以生成最純粹的色調。例如,R 225, G 255, B 0 結合了紅色和綠色,其結果是?奇怪的是,其結果是黃色!原因是,當兩盞燈照在同一個地方時,這個地方會變亮而不是變暗。如果我們把 R、G 和 B 混合在一起,都以最大亮度發光,我們就會得到白色的光。這與繪畫的色彩正好相反。如果我們把許多繪畫的色彩混合在一起,就會得到一些深灰色的東西,很難看。
R:FF G:FF B:00 = 黃色
R:00 G:FF B:FF = 青色
R:FF G:00 B:FF = 品紅
順便說一下,這些是顯示器可以生成的最亮的色彩,因為它們使用了 2 個全亮的點,而紅、綠、藍色只使用了 1 個點。對于其他所有色調,亮度第二強的點發出的亮度低于 255。正是這個既不是 255 也不是 0 的“中間”點定義了色調。將這個中間點的亮度從 0 緩慢遞增到 255,我們可以得到比如說在紅色(FF0000)和黃色(FFF00)之間的所有色調??偣灿?6 種這樣的轉換:
255 紅色和一些綠色:紅到黃
255 紅色和一些藍色:紅到紫
255 綠色和一些紅色:綠到黃
255 綠色和一些藍色:綠到藍綠
255 藍色和一些綠色:藍到藍綠
255 藍色和一些紅色:藍到紫
當我們逐漸改變 R、G 或 B 時,會得到類似彩虹的東西。
左邊和右邊邊界的色彩是紅色。由于這個原因,彩虹常常被畫成一個圓形。色調是以度數來定義的,紅色為 0 和 360 度。
第三個色彩屬性被稱為飽和度。到目前為止,我們只處理了完全飽和的色彩,即最暗的點亮度是 0。如果我們想讓一個完全飽和的色彩更亮,使其最終接近白色,我們需要降低飽和度,按比例增加所有三個點的亮度,使其接近 255。要把飽和度從 100% 降低到 50%,我們必須把現在的值與 255 之間的差值減半。舉例:
現值:R 255, G 128, B 0將飽和度從 1 降低到 0.5
新的 R 值 = 255 + 0.5 * (255-255) = 255新的 G 值 = 128 + 0.5 * (255-128) = 192(或 191,取決于四舍五入的情況)新的 B 值 = 0 + 0.5 * (255-0) = 128(或 127,取決于四舍五入的情況)將飽和度從 1 降低到 0(= 白色)。
新的 R 值 = 255 + 1 * (255-255) = 255新的 G 值 = 128 + 1 * (255-128) = 255新的?B?值?=?0?+?1?*?(255-0)?=?255紅色從 100% 的飽和度降到 0%:現在我們可以再畫一次彩虹,加入一些飽和度和亮度的變化。X 軸增加了從 0 到 360 的色調。在中間的是以 100% 的飽和度和 100% 的亮度顯示的每個色調。在上半部分,亮度保持在 100%,飽和度降低到 0%,這就形成了白色。在下半部分,飽和度保持 100%,亮度減少到 0%,這就形成了黑色。
注意黃色、青色和品紅在變成白色或黑色之前比其他色調能保持更長時間。它們是最強的色彩,因為有 2 個點在全亮度閃耀。
一個接近 0% 飽和度和 100% 亮度的色調看起來是白色的。白色的值是 FFFFF,色調和飽和度是未定義的。
一個飽和度為 100%、亮度接近 0% 的色調看起來是黑色的。黑色的值是 0000,色調和飽和度都沒有定義。
一個所有 3 個點都以相同強度發光的色彩看起來是灰色的。一個可能的值是 808080。
注意:對于灰色(即 R、G 和 B 有相同的值),色調和飽和度都沒有定義,只有亮度有意義的值。我們也可以說,黑色、灰色和白色都不是色彩。黑色 0000 的亮度為 0,白色 FFFFF 的亮度為 1。僅靠亮度來控制白色、灰色和黑色的外觀有一個奇怪的后果,我們可以在下一張圖中看到。
我們現在已經涵蓋了顯示器可以顯示的所有色彩了嗎?實際上,我們只展示了所有可能的 R、G 和 B 組合中的不到 1%,也就是說,只有那些一個點是 255(100% 亮度)或一個點是 0(100% 飽和度)的組合。比方說,我們首先將 FF8000(一種橙紅色)的飽和度改為 50%,得到 FFC080。當我們再把亮度改為 50% 時,得到 806040?,F在的色調仍然是橙紅色,但色彩更接近于深灰色。
下面是一張包含所有可能的飽和度和亮度組合的紅色圖片。
我想這是本文中最令人困惑的圖片。基本上,我想把 Y 軸(從上到下)上的色彩從紅色變成黑色,意味著亮度從 1 到 0,而 X 軸(從左到右)上的色彩從紅色變成白色,意味著飽和度從 1 到 0,我本來以為白色和黑色也會混合,右下角會變成灰色,但情況并非如此。只要 R、G、B 有相同的值,色調和飽和度就失去了意義。只有亮度對白色、灰色和黑色(右邊界)的色彩有影響。更糟糕的是整個下邊界只有黑色,因為一旦亮度為 0,色調和飽和度又變得毫無意義了。
如果你也感到困惑,我們就是一伙兒的了。但這就是 HSB 色彩方案的工作方式。當你只操作 RGB 值時,很難說出結果會是什么樣子(還記得 R 和 G 混合的結果是黃色嗎?)。在 HSB 色彩空間中操作色彩時,只要你只改變飽和度和亮度,黃色就一直是黃色,直到亮度變成 1(白色)或 0(黑色),這時色調和飽和度就會消失。
HSL 色彩空間
還有一個色彩空間叫做 HSL——色調(Hue)、飽和度(Saturation)和亮度(Luminosity)。它的色調與 HSB 相同,但飽和度不朝向白色,而是朝向灰色;光亮度則從 0= 黑色,0.5= 灰色到 1= 白色。HSL 在從黑白電視到彩色電視的過渡過程中非常有用。黑白電視只顯示 L 值,而彩色電視則使用 HSL。
取色器
在過去,我總是很難理解取色器是如何工作的,不知道為什么它們有時會失敗?,F在了解了色調、飽和度和亮度,以及它們與 RGB 色彩的關系后,取色器也就更容易理解了。
PowerPoint 2010 取色器
PowerPoint 使用 HSL 色彩空間。在色彩選擇區,它們水平顯示所有的色調,垂直顯示飽和度。在 HSL 色彩空間中,飽和度為 0 是灰色的,因此完整的下邊界是灰色的。右邊是一個滾動條,可以改變亮度,0 表示黑色,128 表示灰色,255 表示白色(它不使用 0-100%,而是 0-255)。在 0 和 255 時,色調和飽和度失去了意義。最純粹的色彩是在亮度 128。
例如,在 HSL 中選擇一個藍色,然后將亮度降低到 0,并切換到 RGB 顯示,正確顯示為 0,0,0。把 R 增加一點,然后再設置為 0,再切換回 HSV,現在顯示的是色調和飽和度為 0,當然,這應該是未定義的。色調為 0 意味著紅色,但黑色沒有色調。色彩區域的指針仍然顯示最初選擇的藍色,而不是色調為 0 的紅色。這不僅讓我感到困惑,而且當我不斷地鼓搗黑色、灰色和白色,并在 RGB 和 HSL 視圖之間切換時,我的 PowerPoint 終于崩潰了。
Paint.net 4.2 取色器
事實證明,paint.net 的取色器用起來更容易。他們使用的是 HSV 色彩模型,這與 HSB 色彩模型相同,他們只是把亮度這個詞改為體積(volume)。我很欣賞的一點是他們在同一個窗口中顯示 RGB 值和 HSV 值,這使我更容易理解改變一個 HSV 值是如何影響 RGB 值的。它在色彩圈的邊界上顯示所有可用的色調,中間是白色,意味著 0% 的飽和度。要使色彩變深,必須改變體積(亮度)參數的滑塊。當然,它的黑色、灰色和白色的色調也為零,但至少在我鼓搗這些數值時它沒有崩潰。
WinUI 取色器
可悲的事實是,WPF 沒有取色器。這很像微軟多年前放棄了 WPF,試圖強迫我們改為寫 UWP 應用程序的情況。許多開發者會說“不,謝謝你”,并留在了 WPF。所以現在微軟正在引入 XAML,允許 WPF 項目使用“較新”的控件,取色器就是一個例子——可這些控件從一開始就應該包含在 WPF 中才對。我還沒有在項目中用過 WinUI 取色器,但我在 XAML 控件庫中運行它是這樣的:
它的工作原理有點像 PowerPoint 中的取色器,但使用 HSV(HSB)代替,這意味著色彩區域的下部是白色的,而不是灰色的。下方的滾動條改變 V 值(亮度)。當設置為黑色時,色調和飽和度保持其最新值,即使后來在色彩區域選擇了不同的色彩也一樣。當值(亮度)增加時,色彩區的圓圈就會跳回原來的色調。這樣很奇怪,但至少沒有崩潰。
Web 取色器
下面是一個基于互聯網的取色器的例子,在 HTML 5 的 input 標簽中定義:
<input type="color">HTML 只定義了 input 標簽的功能,取色器的實際外觀取決于瀏覽器。下面是 Chrome 瀏覽器的截圖。
它使用一個彩虹滾動條來選擇色調。上面的矩形在所有可能的飽和度和亮度組合中顯示該色調。關于如何計算數值的詳細解釋,請看前面關于 HSB 的解釋,它的情況幾乎是一樣的,只是白角和紅角換了位置。得到的值可以顯示為 RGB(整數或十六進制)和 HSL。
.NET 的色彩類
色彩(Colors)類提供了一些標準色彩。它們是由委員會將一些不同的色彩方案混合起來選擇出來的,有時結果很奇怪。例如,Colors.Gray 比 Colors.DarkGray 要深一些。很奇怪,對嗎?
或者 2 個不同的名字代表的其實是同一種色彩,比如 Aqua(00FFFF)和 Cyan(00FFFF)或者 Fuchsia(FF00FF)和 Magenta(FF00FF)。不幸的是,Colors 幫助頁面是按字母順序顯示色彩的,如果你知道名字就很容易找到它們,但要分辨出哪些色彩彼此相近或相配卻非常困難。
所以我花了一些時間,按色調垂直排序,然后按亮度和飽和度水平排序。這里的結果列出了與 Colors 幫助頁面中完全相同的色彩:
精確生成定制色彩
讓色彩變亮或變暗(降低飽和度和 / 或亮度)
當我設計一個新的應用程序并決定要使用的色彩方案時,我通常不能使用色彩類提供的調色板。通常情況下,我需要帶有陰影的相同色調(不同的飽和度和亮度)。要做到這一點只需幾行代碼就能搞定了。下面是降低任何色彩的飽和度(使之更亮)或降低亮度(使之更暗)的方法。
/// <summary> /// Makes the color lighter if factor>0 and darker if factor<0. 1 returns white, -1 returns /// black. /// </summary> public static Color GetBrighterOrDarker(this Color color, double factor) {if (factor<-1) throw new Exception($"Factor {factor} must be greater equal -1.");if (factor>1) throw new Exception($"Factor {factor} must be smaller equal 1.");if (factor==0) return color;if (factor<0) {//make color darker, changer brightnessfactor += 1;return Color.FromArgb(color.A, (byte)(color.R*factor), (byte)(color.G*factor), (byte)(color.B*factor));} else {//make color lighter, change saturationreturn Color.FromArgb(color.A,(byte)(color.R + (255-color.R)*factor),(byte)(color.G + (255-color.G)*factor),(byte)(color.B + (255-color.B)*factor));} }令人驚訝的是只用幾行代碼就可以改變飽和度和亮度。有點困難的是如何正確計算飽和度,而這個方法就很方便。
紅、綠、藍,系數為 -1 到 1:
為了得到更亮的色彩,不要使用綠色,因為綠色不是 100% 的飽和度——而要使用黃色、品紅和青色。
請注意,先應用 0.5 的系數,然后再應用 -0.5 的系數是不會得到原來色彩的。第一次調用改變的是飽和度,第二次調用改變的是亮度。
我喜歡使用這種方法的原因是:
我可以用小幅度而可控的步驟增加、減少變化,并在 GUI 中看到結果。
我可以很容易地創建陰影和高光,它們應該有相同的色調,但飽和度和亮度則不同。
混合色調
通常情況下,一個用戶界面不應該使用太多的色調,有幾個色調并從中混合一些色調可能就夠了。下面兩種方法可以達到這個目的,第一種是將兩種色彩一半一半地混合,第二種是讓一種色彩多于另一種色彩。
/// <summary> /// Mixes 2 colors equally /// </summary> public static Color Mix(this Color color1, Color color2) {return Mix(color1, 0.5, color2); } /// <summary> /// Mixes factor*color1 with (1-factor)*color2. /// </summary> public static Color Mix(this Color color1, double factor, Color color2) {if (factor<0) throw new Exception($"Factor {factor} must be greater equal 0.");if (factor>1) throw new Exception($"Factor {factor} must be smaller equal 1.");if (factor==0) return color2;if (factor==1) return color1;var factor1 = 1 - factor;return Color.FromArgb((byte)((color1.A * factor + color2.A * factor1)),(byte)((color1.R * factor + color2.R * factor1)),(byte)((color1.G * factor + color2.G * factor1)),(byte)((color1.B * factor + color2.B * factor1))); }這就是生成良好匹配的色彩所需要的一切操作。第一張圖片顯示了每種“主”色如何與其他“主”色進行不同程度的混合,同樣是紅、綠和藍:
然而,如果你用黃色、品色和青色來代替可能會更好。
在這里,我真的覺得這些色彩比混合紅、綠、藍更漂亮。當然,它們可能太純粹了。GUI 經常使用灰暗的色調,使用 GetBrighterOrDarker() 可以很容易地在混合后做出灰暗的色調。
獲取 RGB 色彩的色調、飽和度和亮度
我還做了一些寫這篇文章時所需要的方法,可能也很有用。第一個方法是計算一個 RGB 色彩的色調、飽和度和亮度。
/// <summary> /// Returns the hue, saturation and brightness of color /// </summary> public static (int Hue, double Saturation, double Brightness)GetHSB(this Color color) {int max = Math.Max(color.R, Math.Max(color.G, color.B));int min = Math.Min(color.R, Math.Min(color.G, color.B));int hue = 0;//for black, gray or white, hue could be actually any number, but usually 0 is //assign, which means redif (max-min!=0) {//not black, gray or whiteint maxMinDif = max-min;if (max==color.R) {#pragma warning disable IDE0045 // Convert to conditional expressionif (color.G>=color.B) {#pragma warning restore IDE0045hue = 60 * (color.G-color.B)/maxMinDif;} else {hue = 60 * (color.G-color.B)/maxMinDif + 360;}} else if (max==color.G) {hue = 60 * (color.B-color.R)/maxMinDif + 120;} else if(max == color.B) {hue = 60 * (color.R-color.G)/maxMinDif + 240;}}double saturation = (max == 0) ? 0.0 : (1.0-((double)min/(double)max));return (hue, saturation, (double)max/0xFF); }我從“CodeProject:在.NET 中操縱色彩第 1 部分”中復制了這段代碼(其實它贏得了“2007 年 5 月最佳 C# 文章”),并對其做了點微小的“改進”。例如,我覺得整數 0 到 360 足以列舉所有的色調。原代碼使用浮點數,所以你可以有無限多的色調。這可能會讓計算不那么容易受到四舍五入錯誤的影響,但我想人們看不出這有什么不同。
將任何色彩的飽和度和亮度提高到 100%
在我選擇匹配色彩的方法中,最好從飽和度和亮度為 100% 的"純色"開始,然后再進行混合,使其變得更深或更亮。下面的方法接收任何 RGB 色彩,并返回一個具有相同色調的 RGB 色彩,但飽和度和亮度為 100%。
/// <summary> /// Returns a color with the same hue, but brightness and saturation increased to 100%. /// </summary> public static Color ToFullColor(this Color color) {//step 1: increase brightness to 100%var max = Math.Max(color.R, Math.Max(color.G, color.B));var min = Math.Min(color.R, Math.Min(color.G, color.B));if (max==min) {//for black, gray or white return whitereturn Color.FromArgb(color.A, 0xFF, 0xFF, 0xFF);}double rBright = (double)color.R * 255 / max;double gBright = (double)color.G * 255 / max;double bBright = (double)color.B * 255 / max;//step2: increase saturation to 100%//lower smallest R, G, B component to zero and adjust second smallest color accordingly//p = (smallest R, G, B component) / 255//(255-FullColor.SecondComponent) * p + FullColor.SecondComponent = color.SecondComponent//FullColor.SecondComponent = (color.SecondComponent-255p)/(1-p)if (color.R==max) {if (color.G==min) {double p = gBright / 255;return Color.FromArgb(color.A, 0xFF, 0, (byte)((bBright-gBright)/(1-p)));} else {double p = bBright / 255;return Color.FromArgb(color.A, 0xFF, (byte)((gBright-bBright)/(1-p)), 0);}} else if (color.G==max) {if (color.R==min) {double p = rBright / 255;return Color.FromArgb(color.A, 0, 0xFF, (byte)((bBright-rBright)/(1-p)));} else {double p = bBright / 255;return Color.FromArgb(color.A, (byte)((rBright-bBright)/(1-p)), 0xFF, 0);}} else {if (color.R==min) {double p = rBright / 255;return Color.FromArgb(color.A, 0, (byte)((gBright-rBright)/(1-p)), 0xFF);} else {double p = bBright / 255;return Color.FromArgb(color.A, (byte)((rBright-bBright)/(1-p)), 0, 0xFF);}} }這個方法是我自己寫的。這里的數學要求比較高,我希望我的計算是正確的。你可以使用任何取色器來輕松驗證它。如果你發現任何不一致的地方,請告訴我。
作者簡介:
Peter Huber 是 50 年前在 HP35 上開始學習編程的。然后他從 PL1、Assembler、Pascal、Modula、Java 轉到 C#,并且喜歡上 C# 語言已經有 15 年了。他已經在新加坡退休,他的大部分時間都花在編寫單用戶 WPF 應用程序上。
原文鏈接:
https://www.infoq.com/articles/colors-dotnet-guide/
總結
以上是生活随笔為你收集整理的.NET 深度指南:Colors的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 通过Dapr实现一个简单的基于.net的
- 下一篇: 如何判断 .NET Core 应用程序是