Цветовая модель HSV(HSB)
Что происходит? Где наш привычный RGB?
Что же такое RGB? Почему мы используем именно его повсюду, и чем он мне не угодил?
Цитата:
Цветовая модель RGB - это аддитивная цветовая модель, в которой красный, зеленый и синий свет объединяются различными способами для воспроизведения широкого спектра цветов. Название модели происходит от инициалов трех основных основных цветов: red-красного, green-зеленого и blue-синего.
Удобно ли это - думать о светодиодах, работая с компонентами цвета?
Цитата:
Основная цель цветовой модели RGB - ощущение, представление и отображение изображений в электронных системах, таких как телевизоры и компьютеры, хотя оно также используется в обычной фотографии. Перед электронным периодом цветовая модель RGB уже имела за собой прочную теорию, основанную на человеческом восприятии цветов.
Кто-то уже привык иметь дело с RGB, но можете ли вы однозначно представить в уме любой цвет, взглянув на его код?
Hue-Saturation-Value, HSV.
Что-ж, в связи с неудобствами, связанными с моделью RGB, была
/*довольно давно*/
Цитата:
Модель была создана Элви Реем Смитом, одним из основателей Pixar, в 1978 году.
Цитата:
HSV (англ. Hue, Saturation, Value — тон, насыщенность, значение) или HSB (англ. Hue, Saturation, Brightness — тон, насыщенность, яркость) — цветовая модель, в которой координатами цвета являются:
- Шкала оттенков — Hue
Hue — цветовой тон, (например, красный, зелёный или сине-голубой). Варьируется в пределах 0—360°, однако иногда приводится к диапазону 0—100 или 0—1. - Saturation — насыщенность. Варьируется в пределах 0—100 или 0—1. Чем больше этот параметр, тем «чище» цвет, поэтому этот параметр иногда называют чистотой цвета. А чем ближе этот параметр к нулю, тем ближе цвет к нейтральному серому.
- Value (значение цвета) или Brightness — яркость. Также задаётся в пределах 0—100 или 0—1.
UPD: Она - абсолютно правильная. То, что я представлял думая о HSV раньше на самом деле является HSL, и, скорее всего, в будущем будет статья об нём(мне он кажется наиболее привлекательным).
Градиенты! Теперь цвет будет переливаться всеми цветами радуги, путешествуя по цветовому колесу при интерполяции от одного значения HSV до другого!
Зачем это нужно? Ну, разумеется, это нужно никому другому больше, чем художникам и дизайнерам, работающим с кодом.
Достаточно отнять от 360 компонент Hue, чтобы получить 2 комплиментарных(complementary) цвета.
Достаточно отнять от каждого одинаковое число в обе стороны, чтобы получить разделённые комплиментарные(split-complementary) цвета, и проделать то же с обратной стороной, чтобы получить двойные разделённые комплиментарные(double complementary) цвета.
Для тех, кто не знаком с теорией цвета, это называется цветовой гармонией или цветовой схемой, и определённым образом выстраивая фигуры на цветовом колесе можно получить неплохо сочетающиеся друг с другом цвета.
Изображение взято отсюда: https://coloursandmaterials.wordpres...colour-system/
Конечно, в использовании этой модели есть свои недостатки. Например,
Цитата:
При целочисленном кодировании для каждого цвета в HSV есть соответствующий цвет в RGB. Однако обратное утверждение не является верным: некоторые цвета в RGB нельзя выразить в HSV так, чтобы значение каждого компонента было целым. Фактически, при таком кодировании доступна только 1/256 часть цветового пространства RGB.
Мы будем сегодня кодить?
Да, да, конечно, я хочу понять, как преобразовать RGB->HSV и обратно, поэтому давайте возьмём вот эту формулу из Википедии:
Здесь всё просто, сначала находим минимум и максимум.
var rgb:Object = {r:0xFF, g:0xAA, b:0x22}; //Сочный оранжевый цвет rgb.r /= 0xFF; rgb.g /= 0xFF; rgb.b /= 0xFF; //Нам нужны значения от нуля до единицы var min:Number = Math.min(rgb.r, rgb.g, rgb.b); //Находим минимум var max:Number = Math.max(rgb.r, rgb.g, rgb.b); //Находим максимум
var h:Number; if (max == min) h = 0; //если цвет серый, ничего не надо считать else if ((max == rgb.r) && (rgb.g >= rgb.b)) h = (60 * (rgb.g - rgb.b)) / (max - min); //если красного больше, всего, и зелёного больше или столько же, сколько и синего else if ((max == rgb.r) && (rgb.g < rgb.b)) h = (60 * (rgb.g - rgb.b)) / (max - min) + 360; //если красного больше всего и зелёного меньше, чем синего else if (max == rgb.g) h = (60 * (rgb.b - rgb.r)) / (min - max) + 120; //если зелёного больше всего else h = (60 * (rgb.r - rgb.g)) / (max - min) + 240; //если синего больше всего
Далее - насыщенность.
Здесь всё просто: чем больше разбросаны компоненты цвета, тем больше насыщенность, чем все они ближе друг к другу, тем менее насыщенность. Например, #000000 - чёрный, #ffffff - белый, #dddddd - светло-серый.
Здесь мы делим минимальное на максимальное, чтобы получить соотношение того, насколько цвет обесцвеченный(0 - вообще не обесцвеченный, может быть при красном, зелёном и синем, также жёлтом , магента и циан(F00, 0F0, 00F, FF0, F0F, 0FF), 1 - полностью обесцвеченный, будет у серых тонов, чёрного и белого, и затем инвертируем, чтобы получить насколько цвет насыщенный, отнимая от единицы обесцвеченность).
Ну и конечно же значение цвета.
HSV->RGB
Оттуда же возьмём формулу для преобразования цвета обратно из HSV в RGB:
Сначала считаем значения Hi, Vmin, Vinc, Vdec
var hsv:Object = {h:h, s:s*100, v:v*100}; var hi:int = int(hsv.h / 60) % 6; //делим тон на 6 промежутков, для каждого отдельно будем считать компоненты RGB var vMin:int = (100 - hsv.s) * hsv.v / 100; var a:Number = (hsv.v - vMin) * (hsv.h % 60) / 60; var vInc:Number = vMin + a; var vDec:Number = hsv.v - a;
var rgb:Object = {r:0, g:0, b:0}; switch(hi) { case 0: rgb.r = hsv.v; rgb.g = vInc; rgb.b = vMin; break; case 1: rgb.r = vDec; rgb.g = hsv.v; rgb.b = vMin; break; case 2: rgb.r = vMin; rgb.g = hsv.v; rgb.b = vInc; break; case 3: rgb.r = vMin; rgb.g = vDec; rgb.b = hsv.v; break; case 4: rgb.r = vInc; rgb.g = vMin; rgb.b = hsv.v; break; case 5: rgb.r = hsv.v; rgb.g = vMin; rgb.b = vDec; break; } rgb.r = int(rgb.r/100*255); rgb.g = int(rgb.g/100*255); rgb.b = int(rgb.b/100*255);
var rgb:Object = {r:0xFF, g:0xAA, b:0x22}; //Сочный оранжевый цвет rgb.r /= 0xFF; rgb.g /= 0xFF; rgb.b /= 0xFF; //Нам нужны значения от нуля до единицы var min:Number = Math.min(rgb.r, rgb.g, rgb.b); //Находим минимум var max:Number = Math.max(rgb.r, rgb.g, rgb.b); //Находим максимум var h:Number; if (max == min) h = 0; //если цвет серый, ничего не надо считать else if ((max == rgb.r) && (rgb.g >= rgb.b)) h = (60 * (rgb.g - rgb.b)) / (max - min); //если красного больше, всего, и зелёного больше или столько же, сколько и синего else if ((max == rgb.r) && (rgb.g < rgb.b)) h = (60 * (rgb.g - rgb.b)) / (max - min) + 360; //если красного больше всего и зелёного меньше, чем синего else if (max == rgb.g) h = (60 * (rgb.b - rgb.r)) / (min - max) + 120; //если зелёного больше всего else h = (60 * (rgb.r - rgb.g)) / (max - min) + 240; //если синего больше всего var s:Number; if(max == 0) s = 0; else s = 1 - min / max; var v:Number = max; var hsv:Object = {h:h, s:s*100, v:v*100}; var hi:int = int(hsv.h / 60) % 6; //делим тон на 6 промежутков, для каждого отдельно будем считать компоненты RGB var vMin:int = (100 - hsv.s) * hsv.v / 100; var a:Number = (hsv.v - vMin) * (hsv.h % 60) / 60; var vInc:Number = vMin + a; var vDec:Number = hsv.v - a; rgb = {r:0, g:0, b:0}; switch(hi) { case 0: rgb.r = hsv.v; rgb.g = vInc; rgb.b = vMin; break; case 1: rgb.r = vDec; rgb.g = hsv.v; rgb.b = vMin; break; case 2: rgb.r = vMin; rgb.g = hsv.v; rgb.b = vInc; break; case 3: rgb.r = vMin; rgb.g = vDec; rgb.b = hsv.v; break; case 4: rgb.r = vInc; rgb.g = vMin; rgb.b = hsv.v; break; case 5: rgb.r = hsv.v; rgb.g = vMin; rgb.b = vDec; break; } rgb.r = Math.round(rgb.r / 100 * 255); rgb.g = Math.round(rgb.g / 100 * 255); rgb.b = Math.round(rgb.b / 100 * 255); trace(rgb.r, rgb.g, rgb.b); //FF AA 21 - синий компонент ошибся на одну единицу, ну что-ж, не так уж и страшно.
Закончил с вот таким классом. Давайте теперь склепаем пару демок!
package com.zackmercury.test { /** * ... * @author ZackMercury */ public class ColorHSV { private var _hue:Number; private var _saturation:Number; private var _value:Number; public function ColorHSV(hue:Number, saturation:Number, value:Number) { this.hue = hue; this.saturation = saturation; this.value = value; } public function getRGB():Object { var hi:int = int(_hue / 60) % 6; //делим тон на 6 промежутков, для каждого отдельно будем считать компоненты RGB var vMin:int = (100 - _saturation * 100) * _value; var a:Number = (_value * 100 - vMin) * (_hue % 60) / 60; var vInc:Number = vMin + a; var vDec:Number = _value * 100 - a; var rgb:Object = {r:0, g:0, b:0}; switch(hi) { case 0: rgb.r = _value; rgb.g = vInc / 100; rgb.b = vMin / 100; break; case 1: rgb.r = vDec / 100; rgb.g = _value; rgb.b = vMin / 100; break; case 2: rgb.r = vMin / 100; rgb.g = _value; rgb.b = vInc / 100; break; case 3: rgb.r = vMin / 100; rgb.g = vDec / 100; rgb.b = _value; break; case 4: rgb.r = vInc / 100; rgb.g = vMin / 100; rgb.b = _value; break; case 5: rgb.r = _value; rgb.g = vMin / 100; rgb.b = vDec / 100; break; } return rgb; } public function setRGB(rgb:Object):void { if (rgb.r > 1) rgb.r = 1; else if (rgb.r < 0) rgb.r = 0; if (rgb.g > 1) rgb.g = 1; else if (rgb.g < 0) rgb.g = 0; if (rgb.b > 1) rgb.b = 1; else if (rgb.b < 0) rgb.b = 0; //корректировка значений var min:Number = Math.min(rgb.r, rgb.g, rgb.b); //Находим минимум var max:Number = Math.max(rgb.r, rgb.g, rgb.b); //Находим максимум if (max == min) _hue = 0; //если цвет серый, ничего не надо считать else if ((max == rgb.r) && (rgb.g >= rgb.b)) _hue = (60 * (rgb.g - rgb.b)) / (max - min); //если красного больше, всего, и зелёного больше или столько же, сколько и синего else if ((max == rgb.r) && (rgb.g < rgb.b)) _hue = (60 * (rgb.g - rgb.b)) / (max - min) + 360; //если красного больше всего и зелёного меньше, чем синего else if (max == rgb.g) _hue = (60 * (rgb.b - rgb.r)) / (min - max) + 120; //если зелёного больше всего else _hue = (60 * (rgb.r - rgb.g)) / (max - min) + 240; //если синего больше всего if(max == 0) _saturation = 0; else _saturation = 1 - min / max; _value = max; } public function setHex(hex:uint):void { var rgb:Object = { r: (hex >> 16) & 0xFF, g: (hex >> 8) & 0xFF, b: hex & 0xFF }; rgb.r /= 0xFF; rgb.g /= 0xFF; rgb.b /= 0xFF; //Нам нужны значения от нуля до единицы setRGB(rgb); } public function getHex():uint { var rgb:Object = getRGB(); return (Math.round(rgb.r * 0xFF) << 16) | (Math.round(rgb.g * 0xFF) << 8) | (Math.round(rgb.b * 0xFF)); } //GET/SET public function get hue():Number { return _hue; } public function set hue(val:Number):void { if (val < 0) //Немного математики для зацикливания от 0 до 360 для всех значений. val += Math.ceil(-val / 360) * 360; if (val >= 360) val -= int(val / 360) * 360; _hue = val; } public function get saturation():Number { return _saturation; } public function set saturation(val:Number):void { if (val < 0) val = 0; if (val > 1) val = 1; //корректировка значений _saturation = val; } public function get value():Number { return _value; } public function set value(val:Number):void { if (val < 0) val = 0; if (val > 1) val = 1; //корректировка значений _value = val; } } }
package com.zackmercury.test { import com.bit101.components.Label; import com.bit101.components.PushButton; import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.Point; import flash.text.Font; import flash.text.TextFormat; import flash.text.TextFormatAlign; import flash.utils.getTimer; /** * ... * @author ZackMercury */ public class Main extends Sprite { private var clr:ColorHSV; [Embed(source="../../../../fonts/segoeuil.ttf", fontName = "SegoeUILight", fontWeight = "normal", fontStyle = "normal", mimeType = "application/x-font", advancedAntiAliasing="true", embedAsCFF="false")] public static const SEGOEUILIGHT_FONT:Class; public static const W:int = 640, H:int = 480; public function Main() { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point var lbl:Label = new Label(this, 180, H / 2 - 20, ""); var font:Font = new SEGOEUILIGHT_FONT() as Font; var tf:TextFormat = new TextFormat(font.fontName, 20, 0xFFFFFF, null, null, null, null, null, TextFormatAlign.CENTER); lbl.textField.width = W; lbl.textField.defaultTextFormat = tf; lbl.text = "Привет! Мы настраиваем ваш ПК"; clr = new ColorHSV(0, 1, 1); addEventListener(Event.ENTER_FRAME, update); } private function update(e:Event):void { graphics.clear(); clr.hue = getTimer() / 100; graphics.beginFill(clr.getHex()); graphics.drawRect(0, 0, W, H); graphics.endFill(); } } }
package com.zackmercury.test { import com.bit101.components.Label; import com.bit101.components.PushButton; import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.Point; import flash.text.Font; import flash.text.TextFormat; import flash.text.TextFormatAlign; import flash.utils.getTimer; /** * ... * @author ZackMercury */ public class Main extends Sprite { private var clr:ColorHSV; [Embed(source="../../../../fonts/segoeuil.ttf", fontName = "SegoeUILight", fontWeight = "normal", fontStyle = "normal", mimeType = "application/x-font", advancedAntiAliasing="true", embedAsCFF="false")] public static const SEGOEUILIGHT_FONT:Class; public static const W:int = 640, H:int = 480; public function Main() { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } private function distance(x1:Number, y1:Number, x2:Number, y2:Number):Number { return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); } private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point clr = new ColorHSV(0, 1, 1); var innerRadius:Number = 100, outerRadius:Number = 199; var bmp:Bitmap = new Bitmap(new BitmapData(400, 400, true, 0x00000000), "auto", true); for (var ox:int = 0; ox < bmp.width; ox ++) for (var oy:int = 0; oy < bmp.height; oy ++) if (distance(ox, oy, bmp.width / 2, bmp.height / 2) <= outerRadius) { var angle:Number = Math.atan2(oy - bmp.height / 2, ox - bmp.width / 2); clr.hue = angle / Math.PI * 180; clr.value = Math.sqrt(distance(ox, oy, bmp.width / 2, bmp.height / 2) / innerRadius); bmp.bitmapData.setPixel32(ox, oy, clr.getHex() | 0xFF000000); } addChild(bmp); } } }
Всего комментариев 3
Комментарии
16.10.2017 10:26 | |
Было бы неплохо увидеть такое же колесо реализованное работой исключительно с RGB. Ну так, чтобы видеть очевидную(или не очень) разницу в градиенте. Потому что как-то не хочется верить в скриншот видео из ютуба. Мало ли, как там поработали над картинкой в угоду HSV. Я не придираюсь, просто когда не с чем сравнить, то наглядности нет.
А вообще, если скриншот сравнения не врет, то и мне тоже стоит посмотреть в сторону HSV, потому что его градиент выглядит очень вкусно и элегантно на фоне градиента RGB |
|
Обновил(-а) Dzzirtuoz 16.10.2017 в 10:28
(Дополнил)
|
16.10.2017 12:48 | |
Dzzirtuoz, но при этом учтите, что HSV градиента между двумя цветами может быть 2 - по двум сторонам между цветами, ну и не всегда такой градиент будет тем, что нам нужно.
Но в случаях, когда расстояния между цветами небольшие - да, это всегда будет выглядеть лучше. Для этого, прежде всего, нужно находить минимальный путь между цветами. Реализовать колесо с помощью RGB - это уже задача не программистов. Можно считать, что с помощью RGB этого сделать нельзя без преобразования его в что-то из семейства Hue-Saturation. Градиенты в RGB делаются с помощью обычной интерполяции компонентов, поэтому они могут переходить в серый по пути Вот мой градиент с довольно удалёнными цветами друг от друга, около половины круга. Если взять цвета, которые ближе друг к другу, то выглядит это приятнее. Конечно, это может понадобиться довольно редко, но на каждый инструмент найдётся мастер. Этот класс мне больше помог в автоматическом подборе цветов для множества разных команд игроков без работы с цветами вручную. Я просто писал что-то в духе var clr:ColorHSV = new ColorHSV(0, 1, 1); _groups.splice(0); for (var i:int = 0; i < _stones; i ++) { clr.hue = (i / (_stones)) * 360 + _options.hueSlider.value; clr.value = _options.valueShiftSlider.value + Math.pow(Math.cos(i+_options.valueWaveShiftSlider.value), 2) / 3 * _options.valueAmplitudeSlider.value; clr.saturation = _options.saturationShiftSlider.value + Math.pow(Math.cos(i+_options.saturationWaveShiftSlider.value), 2) / 3 * _options.saturationAmplitudeSlider.value; _groups.push(clr.getHex()); } |
|
Обновил(-а) ZackMercury 16.10.2017 в 15:03
|
Последние записи от ZackMercury
- Вывод формулы для бесконечного цикла. (11.01.2019)
- Как заменить цикл на формулу. (10.01.2019)
- Конечные и бесконечные суммы, Ч. 1 (08.01.2019)
- Как легко запомнить тригонометрические функции (07.01.2019)
- Движение по треугольнику, квадрату, пентагону, хексагону, ... (05.01.2019)