Как стать автором
Обновить

Комментарии 83

Напоролся на именно этот баг (особенность?) в cairo (графическая библиотека, которая используется, например, в Firefox). Вот, например, svg-шная картинка:
image
Там на чёрном фоне переход полностью прозрачного цвета в белый. Казалось бы, должен быть градиент между чёрным и белым. А вот фиг! Прозрачный задан как rgba(255,0,0,1), и в результате посередине явственно виден красный. Я уже сделал патч и описание, хотел отправить разрабам cairo… А потом посмотрел ту же картинку в Хроме. и увидел то же самое. И в последнем Эксплорере. Так что это уже не баг, а фича, и чинить это, к сожалению, никто не будет.
У вас в вашей свг такой градиент (посмотрите код ее). Естественно перед будет с красного прозрачного в белый непрозрачный на черном фоне. Поведение на 100% закономерное. Если хотите чтобы не было красного — замените 255 на 0.
<linearGradient xmlns="http://www.w3.org/2000/svg" id="MyGradient">
        <stop offset="0%" stop-color="rgba(255,0,0,0)"/>
        <stop offset="100%" stop-color="white"/>
</linearGradient>
Неважно, какие там RGB, если прозрачность 100%. При переходе с полностью прозрачного на белый (или любой другой) цвет должен быть только этот конечный цвет, и никакой другой. Об этом, собственно, и статья, и в этом я совершенно согласен с её автором.
цвет должен быть только этот конечный цвет, и никакой другой

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

Об этом, собственно, и статья

Статья, конечно, близка, но не об этом.
А если я хочу выйти из прозрачного красного и в прозрачный белый? Я вот так часто в Юнити в частичках делаю. Там даже настраивается альфа-канал — отдельно, а все остальное — отдельно.

Прозрачный красный — это как? Полностью прозрачный он один, у него нет цвета. Если же имеется в виду «из прозрачного через полупрозрачный красный и в белый», то запросто:
        <stop offset="0%" stop-color="rgba(0,0,0,0)"/>
        <stop offset="50%" stop-color="rgba(255,0,0,0.5)"/>
        <stop offset="100%" stop-color="white"/>
Стало интересно, накодил сравнение: https://jsfiddle.net/cjysxbc2/.

Прозрачный красный — это абстрактное понятие. Вас же не смущает мнимая единица, которая в квадрате внезапно превращается в -1? Почему же вас смущает прозрачный красный, который при изменении прозрачности внезапно становится красным, а не белым / чёрным?

К слову, какой цвет вы предлагаете на роль «настоящего прозрачного»? Чёрный прозрачный или белый прозрачный? Или, может быть, серый прозрачный?
А как по вашему должен высчитываться цвет по середине, только на основании второго цвета? Или вы предлагаете отдельно считать случай, когда первый цвет полностью прозрачен и когда он не полностью прозрачен? Интерполяция же идет по всем четырем каналам одинаково, так что «баг» в вашем случае, не баг, а вполне ожидаемое поведение.
В статье есть чёткий и понятный ответ, к которому я когда-то самостоятельно пришёл (см. пункт «Если вы программист: используйте Premultiplied Alpha!»).
используйте Premultiplied Alpha

Это вообще бредовый совет. Вот у меня есть красный цвет. Я ходу сделать красный с полупрозрачность 25%. Так вот — по формуле, данной в статье у меня вместо «rgba(255,255,255,0.25)» будет «rgba(51,51,51,0.2)». То есть теперь этот цвет будет мало того, что прозрачным, так еще и чернить и он будет некорректно орисовываться поверх белых поверхностей! Premultimlied alpha можно использовать только если рисовать более светлое поверх более темного, а не всегда. Посмотрите сами на его картинку-пример — эта картинка испорчена!

До:
image

После:
image

А поверх белого фона это будет еще более заметно.
То есть теперь этот цвет будет мало того, что прозрачным, так еще и чернить и он будет некорректно орисовываться поверх белых поверхностей!

Это неправда. Premultiplied Alpha нужна для вычислений, а не для вывода на экран. Для вывода на альфу нужно обратно разделить. Когда вы после вычислений получите rgba(51,51,51,0.2) и разделите на альфу, вы получите обратно rgba(255,255,255,0.2).

Это должен быть выбор в момент наложения, а не единственный способ в момент сохранения. Какой смысл терять 80% информации при записи в файл?

Зачем такой выбор, какой в нем смысл? О потере какой информации идет речь и при чем тут запись в файл, если мы говорим об обработке и выводе на экран?


Давайте вернемся к вашему исходному утверждению: «теперь этот цвет будет мало того, что прозрачным, так еще и чернить и он будет некорректно орисовываться поверх белых поверхностей». В браузерах кроме Сафари background-градиенты строятся именно с помощью приумноженного альфаканала. Сделав градиент, например, от красного непрозрачного, к зеленому прозрачному, вы можете убедиться, что никакого черного там нет.

Процитирую вам из топика:
Идея очень проста: вместо хранения текстуры как [RGBα] нужно хранить её как [α∗Rα∗Gα∗Bα]


О выводе не говорится. Говорится о хранении. Если хранить картинки в таком виде, то в большом количестве целевых устройств будет именно черный цвет. Именно этот эффект мы видим на картинке «после».
Для premultiplied обычно меняют режим блендинга.
Обычный блендинг это:
dectColor * (1 — srcAlpha) + srcColor * srcAlpha
То есть чтобы смешать 2 картинки видеокарта потом все равно делает это умножение на альфу (srcColor * srcAlpha). Поэтому режим смешивания для premultiplied делают таким, чтобы этого умножения не было. Если использовать OpenGL, то вместо glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) просто делают glBlendFunc(GL_SRC_ONE, GL_ONE_MINUS_SRC_ALPHA).
Так что чернить оно не будет.
Но есть другой неприятный момент, смещение цвета при семплинге. Я об этом написал чуть ниже в комментарии. Он имеет место быть для картинок, которые сильно растягиваются, например текстуры GUI. На выходе можем получить не совсем то, что задумывал дизайнер.

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

Premultiply нужно осторожно делать, ибо он меняет изображение, и может все испортить. Покажу на примере:

Возмем 2 RGBA пикселя:
{1, 0, 0, 0.1} {0, 0, 1, 0.9}
Допустим мы семплим ровно между пикселей, это будет что-то типа lerp(c1, c2, 0.5), и пиксель после семплинга будет:
{0.5, 0, 0.5, 0.5}

А теперь делаем Premultiply для этих пикселей:
{0.1, 0, 0, 0.1} {0, 0, 0.9, 0.9}
Аналогично семплим, и получаем другой результат:
{0.05, 0, 0.45, 0.5}
Уже как бы видно, что результат другой, если разделим на 0.5 то получим:
{0.1, 0, 0.9, 0.5}, согласитесь, это значительно отличается от {0.5, 0, 0.5, 0.5}

Соглашусь, значительно отличается. Не понимаю, что тут испорчено, это верный результат.

Если у вас текстура градиента скажем 2*2 пикселя, которую вы растягиваете на 200 * 200, то результат может быть сильно не такой, какой задумывал художник.
Предлагаете потом художнику доказывать, что он не прав, и у нас в рендере результат верный?

Понял о чем вы. Растягивать текстуру 2*2 — это грубо говоря наложить градиент. И дизайнеру может хотеться, чтобы градиент шел из красного непрозрачного в зеленый прозрачный. При умножении такого трудно добиться. Но это не значит, что такое поведение без умножения — один из вариантов нормы. Нет, правильный скейлинг и субпиксельный рендеринг может быть только с умножением. А то, что вам иногда нужно другое поведение — это изобразительный прием. Изобразительные приемы бывают разные и вы можете реализовать их через шейдеры. К скейлингу это не будет иметь отношения.

Другими словами, Premultiply нужно делать там, где художник не проверял результат интерполяции между соседними пикселями?

Посмотрите сами на его картинку-пример — эта картинка испорчена!

Картинка тут конечно зря приведена, она только запутывает

Неважно, какие там RGB, если прозрачность 100%.
А если прозрачность 90%? А если 99%? А если у нас формат float'ы использует и прозрачность 99.999%?

В какой момент и почему вдруг становится «неважно какие там RGB»?
Повторюсь: в статье есть чёткий и понятный ответ (см. пункт «Если вы программист: используйте Premultiplied Alpha!»).
причина понятна, а вот вывод сомнительный
эту проблему должен решать программист — а именно обойти\заблокировать фильтрацию, а не художник
Напоминает недавний спор про гомеопатические препараты. Вещества в нем нет, но на самом деле как бы есть, но очень мало…
Хочется просто сказать: «Ты хочешь 0, но принципиально пишешь 255, а потом винишь компьютер, что он не угадал твоё желание — не надо так!»
Не подскажете как перевести ARGB->RGB. Вроде бы и примеры в сети есть. Но если одно из значений ARGB 0 — получаются обрытный результат. Допускаю, что у меня дело в кривизне рук и желании 'решить' побыстрому.
Не подскажете как перевести ARGB->RGB.

Никак. Оно не «переводится». Вы можете наложить ARGB изображение на что-то (сплошной цвет или другое сплошное изображение), после чего альфа везде будет равна единице и тогда вы её сможете отбросить.

Спасибо

цвет канала OR (альфа-канал XOR цвет подложки).
OR — побитовое ИЛИ
XOR — побитовое исключающее ИЛИ
В коде будет что-то типа:


background = 0xFF # Белая подложка
new_red = red | (alpha ^ background)
Спасибо
Я по этому поводу даже когда-то тулзу написал. Строю по прозрачным областям DistanceField и заливаю ближайшим цветом из непрозрачной области.
Вот например была такая текстура:
Заголовок спойлера


А вот она без альфаканала. Вверху до, внизу — обработанная моей тулзой:
Заголовок спойлера


Вот тут исходный код тулзы:
https://github.com/MrShoor/AvalancheProject/tree/master/Tools/PNGEdger
А вот я загрузил собранную версию (если кому-то понадобится): http://www.gamedev.ru/files/?id=125762
По идее же можно заполнить таким образом только полностью прозрачные пиксели, которые соседствуют с не полностью прозрачными. Тогда такие текстуры будут гораздо лучше сжиматься :)
Ну тулза вроде это и делает. Насчет сжиматься не уверен (если имеется ввиду размер файла), но мипы по ним строить будет можно без опаски.
У вас в примере все прозрачные пиксели заполнены разными цветами. Но по идее достаточно сгенерировать подобные цвета только для тонкой линии вокруг не полностью прозрачных пикселей, а остальное залить одним цветом (прозрачным белым или чёрным, например).

Это плохая идея. При разных операциях при формировании конечного пикселя может учитываться разное количество исходных. При ресайзе будут учитываться 2×scale×W исходных пикселей, где scale — коэффициент уменьшения, а W — окно фильтра. При Гауссовом размытии до трех сигма в каждую сторону.

Если пиксели строго прозрачные или строго непрозрачные (как в примере с плюсом), можно особо не заморачиваться: достаточно сделать всё прозрачное "прозрачным чёрным" и выбрать подходящую glBlendFunc. (В некоторых графических редакторах "прозрачный чёрный" сам по себе появляется безо всяких усилий со стороны редактирующего).
В случае с полупрозрачными пикселями на краях так не прокатит, придётся делать как в статье.

А можно поподробнее, т.е. не совсем понял бывает прозрачный черный и прозрачный белый(не черный)? Просто не очень хорошо разбираюсь в цветовых пространствах.

Прозрачный черный: 0x00000000 (или 0xff000000, в зависимости от значения, вкладываемого в альфа-канал)
Прозрачный белый: 0x00ffffff (или 0xffffffff, в зависимости от значения, вкладываемого в альфа-канал)


Это особенность не столько цветового пространства, сколько формата представления цвета.

Вот автор пишет формулу для смешивания(линейной интерполяции) RGB цветов,
но ведь вроде для корректного смешивания надо возвести в квадрат, смешать, и извлечь корень?
https://www.youtube.com/watch?v=LKnqECcg6Gw
http://stsievert.com/blog/2015/04/23/image-sqrt/
Это нужно только для «гамма-скорректированных» изображений.
В играх текстуры вполне могут быть сразу в линейном sRGB. Не делать же гамма-коррекцию для каждой операции, когда разумнее загрузить в линейном виде, сделать фильтрацию, и уже на выводе финального изображения откалибровать для монитора.
Современные игры учитывают гамму, претензии скорее нужно направлять к софту: графическим редакторам, браузерам.
https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch24.html
Не делать же гамма-коррекцию для каждой операции, когда разумнее загрузить в линейном виде, сделать фильтрацию, и уже на выводе финального изображения откалибровать для монитора.
Делать. Точность в sRGB будет выше. Иначе зачем бы заморачиваться с sRGB вообще. Сегодня такое конвертирование крайне дешевое, потому что делается аппаратно на видеокарте отдельными блоками.
Меня превратно поняли. Мне проще в псевдокоде объяснится:
Вместо:
Загрузить две картинки

Возвести значения цветовых каналов изображений в квадрат
  Наложить изображения
Найти корень

Опять возвести в квадрат
  Сделать тонирование
Опять найти корень

Вывести финальное изображение

Так:
Загрузить изображения
Возвести значения в корень

Наложить изображения
Тонировать

Найти корень
Вывести результат

То есть, я хочу сказать, что гамма-коррекция не относится к интерполяции или другой обработке, и не следует её пихать в каждый фильтр, а делать непосредственно при операциях ввода-вывода.
и не следует её пихать в каждый фильтр, а делать непосредственно при операциях ввода-вывода.
Если промежуточные вычисления хранятся в большей размености, то я с вами соглашусь. Если промежуточные вычисления хранятся в byte, то придется каждый раз при отправке в byte и извлечении обратно конвертировать.
Удивляет что в редакторах правильно все отображает (масштабирование, то, се) а в движках вылезает.
Спасибо, а мы с этими артефактами бодались-бодались, а дело вот в чем!
Наконец-то кто-то это написал нормально. Мы именно так и решали это на нескольких проектах, но так руки и не доходили это нормально написать, теперь есть хорошая дока на которую всех сбрасывать можно. Из дополнений только то, что в WebGl например сжатие текстур поддерживается только DXT3 и DXT5, а они не поддерживают премультиплаед альфу, а экономия видеопамяти в 4 раза стоит того. В итоге приходится делать экструзию(то что в статье предлагается делать через soldify-b) граничных пикселей на этапе экспорта ресуров. В нашем случае пришлось реализовать это самим руками, не так сложно как кажется.

Добрый день!
Почему то сразу вспомнил сайт UFO. Метод "вставки чёрных кадров" ( https://www.testufo.com/#test=blackframes ) — это то же, что описано в статье?

Получается, это баг видеокарт. И вы предлагаете его костылить подготовкой изображения. При софтовом рендеринге его легко можно избежать.
Это принцип работы видеокарт и мониторов (в связке), и предлагается его понимать и уметь правильно с ним работать
Из статьи я понял что при отображении изображений, если доверять рендеринг чёрному ящику с невнятными алгоритмами отрисовки субпикселей, могут возникнуть баги. Путей избежать баги два. Первый — написать правильное отображение субпикселей, которое умеет использовать маску, или в случае чёрного ящика с багом, обходить его подготовкой изображений (в вашем случае).
Не могу согласиться с вами и назвать баг принципом работы. Это недоработка, которая тянется из игнорирования альфа-канала при субпиксельном рендеринге.
Из статьи вы должны были понять, что прозрачный пиксель — тоже пиксель, который не рисуется, но используется в вычислених при сглаживании. Сглаживание — не баг видеокарты/рендеринга, а один из аспектов работы.
Что мешает использовать маску прозрачности, вычисляя прозрачные пиксели?
Тот факт, что эпоха GIF'ов с одним прозрачным цветом в палитре давно ушла в прошлое и сегодня прозрачный цвет — это цвет с уровнем прозрачности 100% (или, что то же самое, уровнем непрозрасности (opacity) — 0%). И вот если вводить правила, пристойно работающие для прозрачности 90%, 99%, 99.9%, то на 100% они иногда будут давать неожиданные, для неподготовленного человека, результаты.
В каком месте я утверждаю, что пиксели должны иметь дискретную прозрачность? Я предлагаю брать ближние пиксели для вычислений из фона, а не из скрытого альфа-слоем основного слоя.
Получается, это баг видеокарт.

Что вы называете видеокартой? Видеокарта — это набор железа. Еще есть драйвера, графические API, ваш код шейдеров. Все это заставляет видеокарту работать так или иначе. Видеокарта делает то, что ей скажут. Статья о том, как заставить работать правильно.

Видеокарта — это железо. Понять не могу, как так можно запрограммировать баг, описанный в статье.
Ведь понятно, что усреднять пиксели надо принимая во внимание все слои. В статье же описывается, как не принимать во внимание слой фона, на котором рисуется картинка со сглаживанием и субпиксельным отображением. Но в чём проблема принимать во внимание все слои? Отпадёт необходимость городить кучу описанных костылей.
image
В указанном в статье примере субпиксели какого-то лешего учитывают соседние пиксели только основного слоя, которые должны быть прозрачны под альфа-слоем. Хотя соседние пиксели надо брать из сочетания фона и основного слоя (в примере фон белый, учитывая что прозрачность полная, то совершенно не важно что там в основном слое).

При чем тут вообще субпиксели? И как вы собираетесь брать сочетание фона и основного слоя — если именно это сочетание и строится неправильно?

Переходные пиксели по-вашему как называются?
Ошибка в построении вызвана тем, что граничащие пиксели берутся из слоя, который не видно вместо того чтобы браться из фона. Основной слой становится прозрачным через маску альфа-слоя.
Не понимаю, как вы не понимаете, что баг легко поправить, просто учитывая фон, а не городить огород.

Предложите формулу, раз это так легко сделать.

Формула та же, ничего не меняется. Просто учитывается фон.

Берём красный крест 255,0,0 с синим ключевым цветом 0,0,255 (что не важно, учитывая альфа канал, который превращает его в цвет фона)
Подсчёт пикселей на 50% перекрытии на белом фоне 255,255,255:
255,0,0 + 255,255,255 => 255,128,128, то есть розовый. Никакого синего или фиолетового.

Это для однородного фона так все просто. А если на фоне уже что-то нарисовано?

А какая разница? Синий цвет, который скрыт маской не участвует в вычислениях. Откройте любой редактор, поддерживающий слои и субпиксельные сдвиги, тот же photoshop. Почему там не наблюдается подобных эффектов?

Разница в том, что прежде чем "просто учитывать цвет фона в формуле", надо понять какой из пикселей фона нужно брать. Вы это забыли сказать.

Точно так же. При перекрытии 30% на 70%, беру 30% левого пикселя фона, 70% правого и смешиваю. Результат считаю пикселем фона. Неоткуда там взяться синему «ключевому» цвету.

Что такое "левый пиксель фона" и "правый пиксель фона"?

image

На этом изображении указанный пиксель перекрывает 2 пикселя фона. Один неизбежно становится левым, второй правым.

Подождите, но вы что и на чем рисуете? Мне почему-то на этой картинке виден один пиксель фона, перекрытый двумя пикселями изображения...

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

То есть вы предлагаете фактически сначала отрисовать фон на изображении — а потом изображение уже без альфа-канала — на фоне?


При таком способе отрисовки любое изображение будет немного размывать фон — а это неправильно.

То есть правильно смотреть на ключевой синий цвет?

Правильно делать как написано в этом посте. Смотреть на синий ключевой цвет вам никто и не предлагает.

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


В статье предполагается, что сэмплирование происходит перед наложением на фон. Поэтому фон считается неопределённым и возникает артифакт. Я же предлагаю объединить эти процессы. При семплировании брать ближайшие пиксели уже с фона. Откройте photoshop или gimp. Там нет таких проблем. Прозрачный фон считается прозрачным.

Нет, в статье предлагается два способа решения проблемы, один из которых — костыль, а второй нормальный.


Вы предлагаете более сложный способ.

Да никто вам не мешает объеденить две операции, если у вас две операции. Просто в статье речь об одной операции, той, которая создает пролемы. В статье общий способ, вы даже сказали что формула та же. Вы же говорите «я знаю, как можно проще» и предлагаете частный.

Формула та же, ничего не меняется. Просто учитывается фон.
Нифига ж себе «просто». А ничего что понятие «фона» в общем случае неопределено?

Рассмотрите простой пример: возьмите два ваших креста и расположите их друг над другом. Сдвиньте один из них на 1/3 пикселя, а второй — на 2/3. И? Что тут будет с фоном?

Подсчёт пикселей на 50% перекрытии на белом фоне 255,255,255:
Если бы в природе был только белый фон и не было бы никакого другого — то никому нафиг бы не сдалась вся эта конструкция. Но что делать, если у вас кроме обьекта и белого фона есть ещё что-нибудь? Другие обьекты, в частности, полупрозрачные.

Вы уж, пожалуйста, изобретите что-нибудь работающее не только для одного частного случая, а?
Расположите объекты наложения слоями и подсчитывайте по-очереди, считая фоном результат подсчёта. Фон не определён в общем случае. Но во время построения изображения фоном будет самый нижний слой.
Расположите объекты наложения слоями и подсчитывайте по-очереди, считая фоном результат подсчёта.
А если обьекты того, отказываются «слоями» располагаться? Мы тут про игру, в общем-то говорим.

Но во время построения изображения фоном будет самый нижний слой.
Это если он существует. А если у вас обьекты расположены так, что нельзя сказать какой из них «ниже»?
Я уже уловил, за что меня минусуют. Я не уловил контекста статьи. И того что всем непременно надо сэмплировать текстуры до их рендеринга, иначе я несу ересь по мнению большинства.
И всё же что вы говорите — это те самые условия, которых я предлагаю избегать.

Ещё во времена экранного режима 0x13h, когда вся видеопамять умещалась в 64к не было необходимости делать всё как у всех, было интересно экспериментировать, можно было с интересом написать свой хитрый движок рендеринга и даже свою поделку (игрой назвать сложно) на этом движке. И вот тогда мои слова про то, что подготовку текстур можно делать непосредственно во время рендеринга сцены, вместе с сэмплированием и фильтрациями были бы уместны.

Но теперь многие функции положены на видеокарты, что стало стандартом и проще не изобретать велосипед, а идти по проверенному пути, в котором одни грабли обходятся заранее подготовленными костылями и все так делают, о чём собственно статья. Да, в ней всё верно написано. Но с учётом того частного случая, который стал повсеместно употребляться. Статья как раз о том, как готовить текстуры, шлифуя костыль до блеска.
С этим эффектом столкнулись люди давным-давно-предавно, ещё во времена, когда текстуры были маленькие — и тогда баги были хорошо заметны. Суть: альфа-канал растягивается отдельно, rgb-слои отдельно, и следовательно, из-под альфы по границе обязательно вылазит фон спрайта; а если не дай боже фон был залит каким-то ключевым цветом (ярко-зеленым например), текстуры будут выглядеть просто чудовищно.

Кстати, а ведь маленькие текстуры до сих пор используются. Где? Да везде! Mip-mapping. )) А когда поверх текстуры еще и DXT-сжатие прошлось, баги множатся и расцветают во всей красе.

Вот как мы боролись с этим в Half-Life; по сути, точно такое же заполнение, как рекомендуют здесь в комментариях, но с упрощенным алгоритмом, без distance field.
Суть: альфа-канал растягивается отдельно, rgb-слои отдельно, и следовательно, из-под альфы по границе обязательно вылазит фон спрайта;

собственно такой должна быть статья плюс картинки, а то воды налили
Надо будет учесть инфу, спасибо. Начинающим особенно пригодится.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации