Как интерполировать значения оттенков в цветовом пространстве HSV?
Я пытаюсь интерполировать между двумя цветами в цветовом пространстве HSV, чтобы добиться плавного цветового градиента.
Я использую линейную интерполяцию, например:
h = (1 - p) * h1 + p * h2
s = (1 - p) * s1 + p * s2
v = (1 - p) * v1 + p * v2
(где p - процент, а h1, h2, s1, s2, v1, v2 - компоненты оттенка, насыщенности и значения двух цветов)
Это дает хороший результат для s и v, но не для h. Поскольку компонент оттенка представляет собой угол, расчет должен выработать кратчайшее расстояние между h1 и h2, а затем выполнить интерполяцию в правильном направлении (по часовой или против часовой стрелки).
Какую формулу или алгоритм я должен использовать?
EDIT: Следуя рекомендациям Джека, я изменил функцию JavaScript-градиента, и он работает хорошо. Для всех, кого это интересует, вот что я закончил:
// create gradient from yellow to red to black with 100 steps
var gradient = hsbGradient(100, [{h:0.14, s:0.5, b:1}, {h:0, s:1, b:1}, {h:0, s:1, b:0}]);
function hsbGradient(steps, colours) {
var parts = colours.length - 1;
var gradient = new Array(steps);
var gradientIndex = 0;
var partSteps = Math.floor(steps / parts);
var remainder = steps - (partSteps * parts);
for (var col = 0; col < parts; col++) {
// get colours
var c1 = colours[col],
c2 = colours[col + 1];
// determine clockwise and counter-clockwise distance between hues
var distCCW = (c1.h >= c2.h) ? c1.h - c2.h : 1 + c1.h - c2.h;
distCW = (c1.h >= c2.h) ? 1 + c2.h - c1.h : c2.h - c1.h;
// ensure we get the right number of steps by adding remainder to final part
if (col == parts - 1) partSteps += remainder;
// make gradient for this part
for (var step = 0; step < partSteps; step ++) {
var p = step / partSteps;
// interpolate h, s, b
var h = (distCW <= distCCW) ? c1.h + (distCW * p) : c1.h - (distCCW * p);
if (h < 0) h = 1 + h;
if (h > 1) h = h - 1;
var s = (1 - p) * c1.s + p * c2.s;
var b = (1 - p) * c1.b + p * c2.b;
// add to gradient array
gradient[gradientIndex] = {h:h, s:s, b:b};
gradientIndex ++;
}
}
return gradient;
}
Ответы
Ответ 1
Вам нужно просто выяснить, какой из них является самым коротким путем, начиная от оттенка до оттенка. Это можно сделать легко, так как значения оттенков варьируются от 0 до 255.
Вы можете сначала вычесть нижний оттенок из более высокого, затем добавить 256 в нижний, чтобы снова проверить разницу с обмениваемыми операндами.
int maxCCW = higherHue - lowerHue;
int maxCW = (lowerHue+256) - higherHue;
Итак, вы получите два значения, чем больше вы решите, нужно ли идти по часовой стрелке или против часовой стрелки. Затем вам нужно будет найти способ сделать интерполяцию работать по модулю 256 оттенка, поэтому, если вы интерполируете от 246
до 20
, если коэффициент равен >= 0.5f
, вы должны reset hue to 0 ( так как он достигает 256 и hue = hue%256
в любом случае).
На самом деле, если вы не заботитесь о оттенке при интерполяции по 0, но просто применяйте оператор modulo после вычисления нового оттенка, он должен работать в любом случае.
Ответ 2
Хотя этот ответ задерживается, принятый неверен, указав, что оттенок должен быть в пределах [0, 255]; также более справедливым может быть сделано с более ясным объяснением и кодом.
Оттенок является значением angular в интервале [0, 360]; полный круг, где 0 = 360. Цветовое пространство HSV легче визуализировать и более интуитивно понятное для людей, чем RGB. HSV образует цилиндр, из которого срез показан во многих сборщиках цветов, а RGB - действительно куб и на самом деле не является хорошим выбором для выбора цвета; большинство из них, которые его используют, должны будут использовать больше ползунков, чем требуется для сборщика HSV.
Требование при интерполировании оттенка состоит в том, что меньшая дуга выбирается для достижения от одного оттенка к другому. Поэтому, учитывая два значения оттенка, есть четыре возможности, приведенные ниже с примерами углов:
Δ | ≤ 180 | > 180
--|---------|---------
+ | 40, 60 | 310, 10
− | 60, 40 | 10, 310
if Δ = 180 then both +/− rotation are valid options
Возьмем +
как против часовой стрелки, а −
- по часовой стрелке. Если разница в абсолютном значении превышает 180, то нормализуйте его на ± 360, чтобы убедиться, что величина находится в пределах 180; это также верно меняет направление.
var d = h2 - h1;
var delta = d + ((Math.abs(d) > 180) ? ((d < 0) ? 360 : -360) : 0);
Теперь просто разделите delta
на необходимое количество шагов, чтобы получить вес каждой итерации цикла, чтобы добавить к начальному углу во время интерполяции.
var new_angle = start + (i * delta);
Релевантная функция, взятая из следующего следующего кода:
function interpolate(h1, h2, steps) {
var d = h2 - h1;
var delta = (d + ((Math.abs(d) > 180) ? ((d < 0) ? 360 : -360) : 0)) / (steps + 1.0);
var turns = [];
for (var i = 1; d && i <= steps; ++i)
turns.push(((h1 + (delta * i)) + 360) % 360);
return turns;
}
"use strict";
function interpolate(h1, h2, steps) {
var d = h2 - h1;
var delta = (d + ((Math.abs(d) > 180) ? ((d < 0) ? 360 : -360) : 0)) / (steps + 1.0);
var turns = [];
for (var i = 1; d && i <= steps; ++i)
turns.push(((h1 + (delta * i)) + 360) % 360);
return turns;
}
function get_results(h1, h2, steps) {
h1 = norm_angle(h1);
h2 = norm_angle(h2);
var r = "Start: " + h1 + "<br />";
var turns = interpolate(h1, h2, steps);
r += turns.length ? "Turn: " : "";
r += turns.join("<br />Turn: ");
r += (turns.length ? "<br />" : "") + "Stop: " + h2;
return r;
}
function run() {
var h1 = get_angle(document.getElementById('h1').value);
var h2 = get_angle(document.getElementById('h2').value);
var steps = get_num(document.getElementById('steps').value);
var result = get_results(h1, h2, steps);
document.getElementById('res').innerHTML = result;
}
function get_num(s) {
var n = parseFloat(s);
return (isNaN(n) || !isFinite(n)) ? 0 : n;
}
function get_angle(s) {
return get_num(s) % 360;
}
function norm_angle(a) {
a %= 360;
a += (a < 0) ? 360 : 0;
return a;
}
<h1 id="title">Hue Interpolation</h1>
Angle 1
<input type="text" id="h1" />
<br />Angle 2
<input type="text" id="h2" />
<br />
<br />Intermediate steps
<input type="text" id="steps" value="5" />
<br />
<br/>
<input type="submit" value="Run" onclick="run()" />
<p id="res"></p>