Цветосмешение

Размытие как оно есть
Размытие как оно есть

Современный дизайн просто помешан на спецэффектах, он, можно сказать, только ими и живёт. И это ничего, лишь бы со вкусом. Приходится иногда делать такую штуку как размытие или fade-out. Видимо из-за некоторых проблем с реализацией text-overflow:ellipsis в браузерах, этот спецэффект получил некоторую популярность. Суть его сводится к тому, чтобы положить на объекты, которые не помещаются в рамки родителя, некую картинку, плавно переходящую от прозрачности к сплошному цвету. Мол, не поместилось — прокрутите или сделайте браузер пошире. Простая штука, на самом деле.

И всё бы ничего, но в какой-то момент я стал замечать, что выведенный из цвета в ноль градиент как-то не слишком точно ложится на тот же самый цвет. Покрутил-повертел: вроде не цветовые профили, да и цвета не напутал. Отложил, забылось.

И тут сегодня мне приходит письмо: мол, такие дела — как-то всё странно и плохо накладывается, вот вам тестовый пример. Спасибо, — говорю, — посмотрим. Посмотрел: и правда, какая-то чертовщина. Скажу сразу, внятного объяснения я так и не нашёл. Так вот…

Эксперимент

Сначала рисуем в Фотошопе пять квадратов:

Квадратные шейпы положены рядом, под каждой из них помещён растровый квадрат со сплошной заливкой, соответствующей основному цвету градиента. Режим смешения (blending mode) каждого из слоёв выставлен в «Normal». Тем самым, мы избавляемся от любых визуальных проявлений градиента. Экспортированная в PNG-8 картинка получается такой:

Пять цветных квадратов, нарисованных в Фотошопе
Пять цветных квадратов, нарисованных в Фотошопе

Любые попытки потрогать её пипеткой, приводят ровно к пяти цветам, как и ожидалось. Дальше верстаем такие же пять квадратов размером 200 × 200 пикселов. Каждому из них присваивается класс, накладывающий градиентную картинку (полупрозрачный PNG-24, почищенный при помощи ImageOptim) на соответствующий фоновый цвет:

	.color-000 {
	    background:#000 url(000.png) no-repeat;
	    }
	.color-CCC {
	    background:#CCC url(CCC.png) no-repeat;
	    }
	.color-C00 {
	    background:#C00 url(C00.png) no-repeat;
	    }
	.color-090 {
	    background:#090 url(090.png) no-repeat;
	    }
	.color-069 {
	    background:#069 url(069.png) no-repeat;
	    }

Такое наложение имитирует слои в Фотошопе. В браузерах вышла следующая картина:

Пять цветных квадратов, свёрстанных в браузере
Пять цветных квадратов, свёрстанных в браузере

Свёрстанная страница выглядит так же, как экспортированная картинка. Даже если очень сильно приблизить. Но это если на первый взгляд, а если повозить по этому делу пипеткой (попробуйте сами), то получается что-то странное: цвет начинает дрожать. Например, мой любимый оттенок красного #CC0000 прыгает красной составляющей в #CB0000 (или 204–203, если в системе RGB). Исключение составил чёрный квадрат и, как выяснилось, другие простые цвета (#00F, #FF0 и так далее) — они состоят из одного сплошного цвета.

Проблемы, как известно, передвигаются стаями. Поэтому давайте усложним задачу и протестируем это цветосмешение в разных браузерах. Сделаем скриншоты в семи браузерах, составим их вместе и удалим из картинок основной цвет каждого из квадратов, чтобы понять по какому принципу происходит смешение. Результат обескураживает:

Сравнение пяти цветных квадратов в семи разных браузерах
Сравнение пяти цветных квадратов в семи разных браузерах

Смешение разнится не только в разных браузерах и разных системах, но ещё и для разных цветов. На этом этапе любые эксперименты хочется прекратить и громко спросить кого-то: «Что за дела, шеф?!» Ответа, как обычно, не следует. Поэтому попробуем предположить что-нибудь самостоятельно.

Никаких проблем с гаммой, цветовыми профилями и другими чанками в PNG быть не может — файлы почищены всеми возможными утилитами. Единственное, что приходит мне в голову — это нехватка цветов в некоторых цветовых диапазонах, которая заставляет браузеры делать смешение (dithering) пограничных цветов для имитации недостающего. Но почему в некоторых диапазонах всё проходит гладко, да и сам Фотошоп справляется на «отлично»…

Кажется у меня ещё осталась «Помощь зала». Идеи?

UPD: В комментариях были предложены весьма достойные версии происходящего: первая и вторая — обе, в общем-то, об одном и том же: сложности округления.

Комментарии

26

mozzy, вот объект и затухает потихоньку. Или вот словари говорят:

fade-out ['feidaut] сущ.
1) постепенное исчезновение изображения; постепенное уменьшение и затухание звука (в кино, по радио и пр.)
2) тех. периодическое затухание радио- и телесигналов (как правило из-за помех передачи)
3) кино съёмка "в затемнение"

Ну, если это не фотошоп так рисует градиенты, то возможно, последствия округления, особенности реализаций таковы, что дают подобный эффект.

Вот примерно те же вычисления, что происходят в браузере, в качестве выражения python:

>>> [204.0 * x/255.0 + (1-x/255.0)*204.0 for x in range(0,255)]

И вот результат:

[204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 203.99999999999997, 204.0, 203.99999999999997, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.0, 203.99999999999997, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.00000000000003, 204.0, 204.0, 204.0, 204.0, 203.99999999999997, 204.0, 203.99999999999997, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 203.99999999999997, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 203.99999999999997, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.0, 203.99999999999997, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.00000000000003, 203.99999999999997, 204.0, 204.0, 204.0, 204.0, 204.0, 204.0, 204.00000000000003, 204.0, 204.0, 204.0]

Так что скорее всего это особенности округления, причем видно, что разные браузеры делают его по-разному (оптимизируют?).

Цветовые профили, гамма и прочее? Хотя профили, я смотрю, ты почикал.

Возможно, фотошоп немного коряво сохраняет. Может быть что-то со сжатием перемудривает.
А дрожание видно даже без пипетки.
У меня нет под рукой optipng и прочих pngkrush - интересно, после ихней оптимизации фича исчезнет или нет.

Возможно это происходит из-за применения в алгоритмах смешивания MMX команд. В таком случае у разработчиков есть выбор, пожертвовать точностью на один бит, чтобы сумма 2-х старших байтов от произведений 2-х 8-и битных чисел поместилась в 8 бит, либо пожертвовать скоростью наложения картинок почти в 2 раза. Как видно, скоростью никто жертвовать не хочет :)

m1ron, это не фотошоп.
Переход от прозрачности к цвету равномерный - проблема именно в отображении браузерами. Если наложить эту же картинку на другой фон - никакого дрожания нет.

после обработки всех имиджей PNGOUT (pngout image.png /c6 /f5 /s0) проблема иcчезает и картинки теряют в весе

Никита, спасибо, что порвали вёрстку — был повод её починить ;) Для больших фрагментов кода лучше использовать <source>, а не <code>

http://www.w3.org/TR/PNG-GammaAppendix (гамма-коррекция должна быть by default)
Ну если вкратце, по-умолчанию в PNG включается гамма-коррекция с значением 2.5. Каждый броузер (и система) почему-то понимает это значение по-своему. PNGOUT корректирует изображение в соответствии с информацией о значении гамма-коррекции, а само значение гамма-коррекции выкашивает. Если я все более-менее правильно понимаю... :) Подобную операцию делает любой png-оптимизатор. Попутно они еще и сам png перепаковывают "как-то хитрее" - файл "легчает", хотя и не всегда.

Я считаю, что проблема действительно в алгоритме браузера, который отвечает за смешивание цветов при обработке полупрозрачных изображений. Думаю, многим известна проблема с округлением значения ширины, указанной в процентах в ИЕ. Здесь что-то похожее.

Цвет в изображении описывается в rgba. Если разбить градиент на уровни, то каждый уровень будет отличаться только значением a (уровнем прозрачности) т. к. значение rgb у нас везде одинаковое. Это значение прозрачности участвует в формуле, по которой рассчитывается выводимый на экран цвет, а разница и итоговом цвете объясняется алгоритмом округления браузера.

Пристально посмотрел на картинку. Никаких гамм там нет, цвета указаны чётко, меняется только прозрачность. Так что дело исключительно в отображении.

Хм, погорячился, был не прав: пути на файлы слетели - не заметил. Посыпаю голову пеплом.
Баг повторяется. Действительно, похоже на ошибку при просчете RGBA.
Кстати, Вадим, в тексте таки нет упоминания ImageOptim'a, иначе бы я в сторону pngout и не взглянул.
Извиняюсь за внесенную в обсуждение дезу.

Очевидно, что проблема исключительно в погрешности округления.

Например: возьмем код

var a=0, b=0;
for (var i=1;
     i>=0.00000000000000011;
     i /= 10)
  a += i;
for (var i=0.0000000000000001;
     i<=1.1;
     i *= 10)
  b += i;
var results = a.toString() + '\n' + b.toString();
alert(results);

Получилось:

1.1111111111111112
1.1111111111111118

А ведь складывались одни и те же числа в разном порядке!

Теперь прикинем, как мы могли бы считать смешение для цветов. Каждый канал содержит число в диапазоне [0..255], включая канал прозрачности - т.е. имеем целые числа, над которыми придется осуществлять операции деления, умножения, сложения и вычитания.

Алгоритм без округления такой:

resultColor =
color1*alpha/maxAlpha +
color2*(maxAlpha-alpha)/maxAlpha

А теперь подумаем, в скольких местах может происходить "естественное" округление:
1. После каждого деления (2 раза)
2. После каждого умножения (2 раза)

А теперь добавим к этому "искусственное", явное округление (при приведении типов и т.д.).

Очевидно, что при использовании чисел разной точности, а также при разном подходе к самому округлению ("отбрасывание остатка", "до ближайшего целого" и др.), результат будет разным.

Вот мы и имеем три разных результата. Никита Прокопов выше привел показательный пример погрешностей округления.

Очень удобно такую задачу решать с помощью canvas. Там градиент сделать от прозрачности до нужного цвета, нужной площади не проблема. Цвет в котором должно заканчиваться, тоже яваскриптом узнать можно. Работает кросбраузерно, даже в ie6(с применение excanvas.js).

Как уже написали другие - это косяки с округлением. Я не знаю как их исправляет фотошоп, но я тут делал сдвиг цвета (Hue) на JS и циклический сдвиг на одном изображении сделать невозможно - через несколько шагов погрешности убивают всю картинку. Через сотню - там просто мясо из случайных пикселей. А в фотошопе в 8-битном цвете всё ок.

Как это правильно разрулить и можно ли вообще (в JS математика такая убогая, что хоть пиши с нуля) - я не разбирался. Мне нужен был быстрый и эффективный код, а картинку я мог изначальную спокойно хранить сколько угодно.