Смесь цветов

Стандартно любой цвет кодируется шестью шестнадцатеричными цифрами, например белый это 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;
}

Комментарии