Автор задачи Евгений Бовыкин.
Стандартно любой цвет кодируется шестью шестнадцатеричными цифрами, например белый это ffffff
, красный — ff0000
, зеленый — 00ff00
, синий — 0000ff
. Все остальные цвета получаются их смешением: серый = белый + черный
, желтый = красный + зеленый
и т.д.
Напишите функцию mixColors
, которая принимает два цвета и возвращает новый цвет - их смесь.
mixColors('000000', 'ffffff'); // 808080
mixColors('ff0000', '00ff00'); // 808000
mixColors('ff00ff', 'ff00ff'); // ff00ff
mixColors('ff0000', '0000ff'); // 800080
mixColors('777777', '999999'); // 888888
mixColors('111222', '222444'); // 1a1b33
mixColors('abcdef', 'fedcba'); // d5d5d5
mixColors('000000', '000222'); // 000111
Подсказка: смешивать стоит отдельно красную составляющую, отдельно зеленую, отдельно синюю.
Дополнительное задание: улучшить функцию, чтобы в аргументах можно было передавать и цвета в формате #xxxxxx
. Возвращается всегда цвет с ведущим #
.
mixColors('000000', 'ffffff'); // #808080
mixColors('#ff0000', '00ff00'); // #808000
mixColors('ff00ff', '#ff00ff'); // #ff00ff
mixColors('#ff0000', '#0000ff'); // #800080
Для начала поймем, почему посчитать среднее арифметическое между двумя цветами это нехорошее решение. Сделать это легко - достаточно перевести оба цвета в десятичную систему счисления, сложить и поделить пополам, не забыв округлить. Теперь возьмем два цвета - темно-зеленый #000100
и черный #000000
. Переведем оба числа в десятичную, получим 256 и 0. Сложим, поделим, получим 128. Переведем обратно. Получим синий #000080
. Не совсем то, чего мы ожидали от смешения темно-зеленого и черного. Так получилось потому, что цвета не отображаются в числа линейно. Вот у нас есть насыщенный синий #0000ff
. Прибавим к нему единицу, получим темно-зеленый #000100
. Прибавим еще 255, и цвет превратится в синий с зеленоватым оттенком #001ff
. Что ж, наивное решение не прошло.
Теперь попробуем избавиться от этого эффекта и будем смешивать каждый цвет отдельно. Поскольку строки, которые мы получаем, всегда имею длину строго 6, мы можем смело "отсекать" каждые два символа, и работать только с ними.
function mixColors(color1, color2){
var color = '';
var length = color1.length;
for(var i = 0; i < length; i += 2){
}
return color;
}
Для выделения подстроки из строки существует функция slice.
function mixColors(color1, color2){
var color = '';
var length = color1.length;
for(var i = 0; i < length; i += 2){
var partColor1 = color1.slice(i, i+2);
var partColor2 = color2.slice(i, i+2);
}
return color;
}
Теперь это дело нам нужно перевести из шестнадцатеричной строки в десятичное число. Напишем небольшую вспомогательную функцию.
function hexToInt(h){
return parseInt(h, 16);
}
Теперь можно смело смешивать каждый из спектров наших цветов, не боясь, что один повлияет на другой. Поскольку может получится десятичная дробь, округляем в ближайшую сторону.
...
var partColor1 = color1.slice(i, i+2);
var partColor2 = color2.slice(i, i+2);
var mixed = Math.round((hexToInt(partColor1) + hexToInt(partColor2))/2);
...
Стоит заметить, что может получится число, состоящее всего из одного разряда. При переводе его обратно в цвет нам нужно добавить один ведущий ноль, если требуется. Например, чтобы цвет не выглядел как #fab1
, а как #f0ab01
.
Теперь наш новый спектр можно смело добавлять к конечному цвету.
Конечный код, после небольшого укорочения, может выглядеть примерно так. Здесь я считаю важным оставить комментарий, чтобы облегчить понимание кода себе и другим разработчикам, когда кто-нибудь будет его читать в будущем.
function toHexInt(i){
return parseInt(i, 16);
}
function mixColors(color1, color2){
var color = "";
/*
* Сначала считаем среднее по красному цвету - xx---- + yy----
* Затем по зеленому --xx-- + --yy--
* И по синему ----xx + ----yy
*/
for(var i = 0; i < color1.length; i += 2){
var partColor = Math.round((toHexInt(color1.slice(i, i+2)) + toHexInt(color2.slice(i, i+2)))/2).toString(16);
color += (partColor.length === 1 ? "0" + partColor : partColor);
}
return color;
}
Допишем функцию, чтобы она опционально могла принимать цвета с ведущими '#'. Тут все просто. Перемеинуем текущую функцию в _mixColors
, а новую назовем прежним именем mixColors
. Новая mixColors
должна снова принимать два цвета, если в них есть ведущий #
убирать его, получать результат из _mixColors
, и возвращать его, опять добавив #
.
function mixColors(color1, color2){
var c1 = color1[0] === "#" ? color1.slice(1) : color1;
var c2 = color2[0] === "#" ? color2.slice(1) : color2;
return "#" + _mixColors(c1, c2);
}
Альтернативное решение предложил Сергей Рыжук. В нем мы сразу преобразовываем наши цвета в числа, однако не просто считаем среднее, а все также разбиваем его на 3 спектра и работаем с каждым отдельно. Проделано это с помощью побитовых сдвигов. Заметим, что 6 шестнадцатеричных цифр будут представлены как 24 бита, по 4 на каждую цифру. Для получения красного спектра "отбросим" первые (правые) 16 бит, и побитово умножим на 255. Почему именно так? Напишем красный #ff0000
в битах |11111111|00000000|00000000
. Линиями я разделил все три спектра. Отбросим первые 16 бит |тут могут оказаться|не только нули|11111111
. Поскольку слева от единиц могут быть не только нули, побитово умножим наше число на 255 - |000...000|11111111
. Так мы превратим в нули все биты, кроме первых восьми, а их самих не тронем.
function mixColors(c1, c2) {
var i1 = parseInt(c1, 16),i2 = parseInt(c2, 16);
return [
double((Math.ceil((((i1 >> 16) & 255) + ((i2 >> 16) & 255)) / 2)).toString(16)),
double((Math.ceil((((i1 >> 8) & 255) + ((i2 >> 8) & 255)) / 2)).toString(16)),
double((Math.ceil(((i1 & 255) + (i2 & 255)) / 2)).toString(16))].join("");
}
function double(v) {
var a = v.toString();
while(a.length < 2)
a = "0" + a;
return a;
}