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

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

Очень интересно. А можно ли где-то посмотреть результат?

я боюсь как бы за рекламу не сочли) это бесплатный проект, так что вот ссылка на новый рендер с мои форматом видео и канвасом https://floor796.com/?render=2 , а просто по ссылке https://floor796.com/ пока что открывается формат прошлый, на mp4 файлах.

Супер! Очень круто, я даже пасхалку нашел)

Да там всё из пасхалок состоит! :D

Или вы какую-то прямо пасхально пасхальную нашли? Даже интересно.

пока что 2 интерактивные пасхалки есть. Обе обвел кружком)

Race-796 есть в чейнджлоге, а вот hev-charger действительно пасхалист. Спасибо, что сделали указатель, откуда были взяты персонажи: некоторых я узнаю, а с некоторыми частями культуры познакомиться не довелось.

Кстати, я не думаю, что сделаю что-то морально плохое, поделившись ссылкой, верно?

Круто, а "HA-Men" это опечатка или так и задумано? Еще "Mem реклама-мака" ссылается на удаленное видео.

Спасибо! HA-Men поправил на HE-Man, это была опечатка)

И мем из рекламы мака заменил на другое видео

такое рекламировать не только не стыдно, но и почетно

Офигенно! За последние пол года, лучшая статья. Отличная работа. Респект. Добавил в закладки.

Да, раньше качественных статей было вагон. Вот думаю даже сделать подборку лучших за год с 2009 по 2019, чтобы было что достойное почитать.

Было бы замечательно!

Это охрененно! И статья огонь и результат.

было бы хорошо дать возможность сохранить весь кадр одной картинкой с максимальным разрешением. неплохой вариант для принта. я бы себе такой постер напечатал. можно куда то залить такое?

Хорошая идея, я позже запилю такой функционал. Пока что простого быстрого способа так сделать нет под рукой

Будет круто, спасибо, жду.

Идея супер. То же жду.

Весь этот проект вызывает очень много положительных впечатлений, рекламирую среди друзей. )

@Flest@KrivisKrivaitis Готово, сохраняю в jpeg 100% качества. Опция доступна в разделе "Что это?". Можно скачать любой кадр. После каждого обновления крупного буду запускать регенерацию изображений.

Готово, сохраняю в jpeg 100% качества.

Спасибо!

Не знаю, может не нашел, но безумно хотелось бы получать ссылку на определеную секцию или прям координату. (Как найти Ждуна на ней????)

Под проектом неимоверно плюсую. Залип на приличное время)

Спасибо) Когда вы перемешаетесь по анимации в адресной строке меняется URL страницы. Так что достаточно просто переместится так, чтобы по центру экрана был интересующий вас элемент/комната и скопировать URL из адресной строки

Вот ссылка на ждуна, он сидит за кассой: https://floor796.com/#t0l2,495,857

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

Кодирование требовало очень много времени, распаковка тоже, но зато растровый файл сжимался из ГБ в сотни КБ.
Плюс картинка прекрасно масштабировалась, качество выходной картинки можно было варьировать в завимимости от FPS бюджета.

Пример работы на эту тему.

Fractal и Wavelet сжатия были в моде в конце 90-х.
Кажется оба нашли применение в форматах для специфических данных, например wavelet в рентгеновских снимках. Также нашло применение в формате JPEG2000 поддерживаемом в Acrobat PDF.

Налицо ещё одна особенность изображения — его изометрия. Не будет ли оптимальнее отыскивать повторяющиеся пикселы не в горизонтальной строке, а "по диагонали". Возможно корреляция будет выше.

И раз уж "пошла такая пьянка", даже более того, а не ввести ли не косые "диагональные" координаты пикселей для хранения, сжатия и обработки графики. Впрочем это добавляет необходимость пересчëта координат при окончательном выводе в canvas.

Я тоже думал использовать как-то аксонометрию. Но проблема в том, что тут не чистая изометрия и косые линии сильно дробно вычисляются, т.е. там очень много математики с дробными числами придется делать. С дробными вычислениями на JS проблема может быть в точности, да и ресурсов они будут больше требовать, так как по сути на каждый пиксель мне нужно будет вычислять его координату учитывая углы поворота. Всего этого можно почти не боятся, если перевести рендер на WebGL в фрагментный шейдер, но я так и не смог придумать как с меньшими затратами передавать на карту в виде текстур каждый кадр и раcпаковывать его на сильно ограниченном GLSL. Т.е. я бы хотел использовать эту сильную сторону графики (аксонометрию), но по расчетам получилось, что используя её я могу сильно проиграть в расходе CPU

А вот WebAsembler специально для видеокодеков был придуман, можно сказать

А сюда не подойдет алгоритм Брезенхэма для рисования линий, но использовать для обхода вдоль нее? Можно еще хранить таблицу смещений для одной линии, от которой дальше отталкиваться.

Думаю он бы подошел здесь) Интересную мысль подкинули, спасибо! В следующей попытке ещё сильнее улучшить сжатие обязательно попробую ваш способ. Пока что остановлюсь на том что есть.

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

Все верно, CPU расходуется значительно больше, чем с mp4. Из-за этого я планирую включать рендер на Mp4 по умолчанию на устройствах, где выполняется любое из условий:

  • слабый интернет

  • батареи меньше 50%

И то и другое можно узнать из js стандартными API браузера (правда поддержка ещё не полная). Если я вижу, что устройство слабое - включаю старый рендер на mp4, иначе - новый. При этом пользователям всегда будет дана возможность переключать между SD (на Mp4 файлах) и HD (на canvas).

А что за api такое для того чтобы узнать слабый интернет, не подскажите?

https://developer.mozilla.org/ru/docs/Web/API/Network_Information_API

В частности вот так буду принимать решение какой формат загружать пользователю

const isSlowNetwork = navigator?.connection?.downlink < 5;

Спасибо!

> Например, цвет rgb(250, 4, 0) и цвет rgb(255, 0, 0), которые зрительно неразличимы, оба станут цветом rgb(248, 0, 0)

Это очень плохо, 255 должен остаться 255 после квантования.

Да, согласен. Вся анимация стала чуточку темнее. Я это планировал поправить в рендере на днях, просто при распаковке пикселей буду добавлять +7 к каждому каналу:

r = (r << 3) + 7
g = (g << 3) + 7
b = (b << 3) + 7

Обычно растягивают либо так: Math.round(v / 31 * 255)) либо так: (v << 3) + (v >> 2). Но я считаю все способы неправильными.

Ещё можно на канвас прицепить filter:brightness(1.028). Но, по-моему, небольшое снижение яркости даже лучше для различимости мелких деталей в светах. Я попробовал поправить яркость, и сразу рельеф на светлых панелях стал менее заметен.

За работу глубочайший респект!

не совсем понял для чего надо синхронизировать отдельные фрагменты ?
почему нельзя кодировать всю картику 5000х5000 ?
почему нельзя передавать комнаду на сервер для передачи нужной видимой области ? и там же делать кропинг и zoom in ?

Ну, во-первых размер всей сцены все время растет, сегодня 5000x5000, через год будет 10000x10000, а через 10 лет страшно представить что будет) Любой знакомый мне формат графики имеет ограничения на максимальный размер картинки, поэтому деление в любом случае рано или поздно потребуется.

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

Что-либо налету отрезать на сервере - нереально для такой задачи, речь ведь про анимацию, а не статическую картинку.

Вы правы все форматы ограничены, в основном до 2^13 = 8192x8192 точек
но какое это имеет значение если вближайшие лет 10 не будет таких экранов ;-)
даже 5К ретина ... раза в 3 меньше :-)
Вы так же делите на области , этот процесс называеться croping достаточно простая операция и не зависящая от размера входной картинки ;-)
кропаете и компресируете ... к тому же можете прооптемизировать и эти операции для нескольких пользователей ... так как при большом спросе навярняка найдутся кто смотрит одно и тоже или почти одно и тоже , оконочателную подгонку можно оставить клиенской части.

в плане компресси вы конечно молодоце что изобрели PNG заного ...
все что вы описали именно так и делаеться в этом стандарте ...
этот формат конечно можно использовать и он так и родился для передачи картинок по сети когда интернет родился ;-)
но с тех пор воды много утекло
компресируетр хоть и без потерь но плохо , в 2-3 раза ... для общего случая, в вашем возможно 5-6 ... но любой мп4, h264,h265 и темболее av1 может и раз в 30-40-50 и качество будет на уровне
к тому же аппоратная поддержка как на серверах так и на клиентах ...
ну и на закуску CRF параметер не совсем подходит для потоковой передачи данных

ух ты ... аж -13 :-)
аж интересно что так не понравилолсь ? технические детали ? ;-)

Я не могу ставить минусы, но и не люблю когда минусы ставятся просто за мнение, без приведения контраргументов.

Вот мои 5 копеек:

Вы так же делите на области , этот процесс называеться croping достаточно простая операция и не зависящая от размера входной картинки ;-)

Чтобы сделать кроп PNG файла например для правого нижнего угла придётся распаковать всю картинку, потом взять кусок и так Х раз - где тут простота если у вас сложность будет O(N*N)?

в плане компресси вы конечно молодоце что изобрели PNG заного ...все что вы описали именно так и делаеться в этом стандарте ...

В PNG нет анимации совсем, а значит и придуманное в компрессии никак не может копировать PNG, который всего лишь Deflate. Тут же весьма интересный набор эвристик с заточкой под контекст.

но любой мп4, h264,h265 и темболее av1 может и раз в 30-40-50 и качество будет на уровне

Мне кажется вы совсем не читали статью.

Спасибо за внимание :-)

Чтобы сделать кроп PNG файла например для правого нижнего угла придётся распаковать всю картинку, потом взять кусок и так Х раз - где тут простота если у вас сложность будет O(N*N)?

в статье не указано откуда и как проявляются картинки , где то кто то их генерирует/рисует
но не указано что это PNG, этот формат автор использует как раз для хранения областей.
но это все все не суть важно, а важно что сначала картинка разбиваеться, потом как то сжимаеться и передается и потом показываеться небольшая часть из всего этого ...
выбор видимой области можно производить у клиента и запрашивать с сервера сразу.
разбитие на области это тот же самый cropping только один раз
Я так понимую Х это у вас количество пользователей ...
если да ... то для нескольких пользователей , если их будет немного больше чем областей у автора, будет возможно хуже , но при большом количестве пользователей будет много повторений или перекрытий по 90%,
что можно с оптимизировать делая кроп один раз для многих пользователей
а окончательную подгонку производить уже на клиенте

В PNG нет анимации совсем, а значит и придуманное в компрессии никак не может копировать PNG, который всего лишь Deflate. Тут же весьма интересный набор эвристик с заточкой под контекст.

PNG имеет нескольки уровней компресси и да нулевой действительно только deflat, но 1,2,3 включают как раз вычесление похожости соседей и стандарт и оговаривает 3 разных способа
* вычитание строк
* вычитание столбцов
* вычисление среднего из соседей и вычинание
что принципе по блок схеме приведенного алгоритма блок номер 2
а Deflat это аналог блока номер 4
но да, квантования PNG не поддерживает и вычитание соедних кадров ...
эти подходы используют MPEGи ну значит добавим еще изобретение и их тоже ;-)
кстати ... квантование у автора ... подозрительно напоминает RGB15 ;-)

Мне кажется вы совсем не читали статью.

конечно ... автору не совсем понравились цветовые артефакты вносимые компресией
с потерями, но вместо того что бы разобраться как MPEGами пользоваться придумал свой способ, ну что ж респект :-)
Кстати, если уж так и не получилось найти параметры которые устраивают по качеству
и, как кто то уже упоминал, YUV 4:4:4 имею ограниченую поддержку
можно кодировать R,G,B как отдельные Gray картинки теме же стандартыми кодэками как независимые потоки,могло бы получиться очень даже хорошо

ещё раз напомню основную причину ухода со стандартных видео-форматов - мне нужна пиксельная четкость (при масштабировании) и уход с синхронизации. Ни один формат этого не может позволить в браузере, поэтому создан свой с рендером в канвас. Вот тут https://floor796.com/?render=2 попробуйте зумом увеличить, будет пиксельная четкость, такого не добиться от mp4 или любого другого формата.

Повторюсь, что-либо перекладывать на сервер - не вариант. У меня бесплатный проект, я не могу позволить купить огромные вычислительные мощности и огромный канал, чтобы переложить кропинг/рендеринг с клиента на сервер. Как известно, клиентов в разы больше серверов, поэтому такие вещи нужно переводить на выполнение у клиента, а не на сервере.

Добавлю, я стараюсь не велосипедить, но когда безвыходная ситуация, то велосипед - это решение.

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

Использование стандартных формат позволяет использовать аппаратное ускорение на сервере что сэкономит оплату процесорного времени, так же как и уменьшение трафика

То есть остается «пиксельная четкость» и если я правильно понял конкретно цветная

И вот тут возможно правильное использование кодеков может помочь

вместо этого можно делать маленький кропинг специально для каждого пользователя 

На сервере? Если пользователь сдвинул сцену на +1px, то мне у сервера запрашивать новый кропинг под размер его экрана. Вы понимаете какая должна быть ширина канала у пользователя, и я уже молчу какая у сервера для того, чтобы налету без задержек все это делать. Вы открывали проект, смотрели вообще о чем речь?))

так же как и уменьшение трафика

С CPU согласен, а вот с трафиком нет. В статье я показал, что мой формат лучше сжимает без видимой потери качества, чем mp4 (h264): 116 МБ mp4 против 82МБ на моем формате. Однако сейчас я свой формат переключил с gzip на Brotli и в итоге вместо 82 МБ стало 57МБ. Т.е. в 2 раза лучше компрессия, чем у Mp4 (crf=1) и сопоставимая компрессия с crf=19 (45МБ).

И вот тут возможно правильное использование кодеков может помочь

Дело не в кодеках, дело в самой отрисовке. Нельзя зумить video элемент без размытия в браузерах. Ну и любой любой кодек будет все равно вносить искажения, так как ни один кодек не заточен под такую задачу, как у меня. По крайней мере ни один, что поддерживают браузеры.

Конечно прекрассно представляю как должно и может работать со сдвигом хоть на пиксел хоть на 2 , именно так работают облачные игровые платформы у NVIDIA, Microsoft , Amazon ....
И проэкт ваш открывал ...
искал где же нужно разбиение на области ... ненашел :-)

ваше решение - скачивать огромный файл на клиента и играть его из кеша ?
правильно ? и подгружать области по мере надобности при сдвигах ...
то есть 5 секунд (60 кадров по 12 в секунду) вы жмете секцию до 16МБ или в 2МБ в секунду такого трафика вполне достаточно что бы качество мпега было достаточное и в итоге не требовало загруски всех 80 или 50 МБ ...
зачем вам иметь у клиента все 5000х5000 точек если показываете в конкретный момент нааамного меньше ?
ну а если правильно найти параметры кодека так и в пол мегабайта можно уложиться ....
Если пользователь не сдинулся - можно начать играть с начала , вся анимация точно так же в кэше
если сдвинулся, запросить с сервера с какого нужно кадра , и он пришлет дельту
причем я уверен что она будет маленькой при сдвигах на пиксель ...

но да все это не совсем стандартное использывание кодеков

Зачем скачивать все сразу? При открытии скачивается только те секции, что видит пользователь. При этом серев вообще не работает, все отдается с CDN.

зачем вам иметь у клиента все 5000х5000 точек если показываете в конкретный момент нааамного меньше ?

Ещё раз повторюсь, показывается только конкретно те секции, что попадают во viewport браузера. Все остальные секции не загружены и ждут, когда до них пользователь проскролирует

Так а я о чем

Зачем еще бить на секции ?

Сжимать только то что видит пользователь

Один канвас один файл Никакой синхронизации

Можно отлично сжать и cdn точно так же закэширует

С переходами немного подшаманить и все ….

Ещё раз... Если пользователь сдвинет на 1px сцену, если я правильно понял вас, мне нужно будет запросить с сервера новый видео файл, который будет кропить с новых координат. Верно? Если так, то это самоубийство для сервера, простите) Это какие мне мощности нужно будет резервировать на сервере, чтобы выдерживать slash-dot эффекты, которые у меня часто. Эта статься - один из примеров причины слэшдот эффекта. Да и CDN очень дорогой, с секционированием у меня условно 100МБ кеша нужно, а с тем, что вы предлагаете там не хватит и пару терабайт.

Я с вами в корень не согласен, что нужно что-либо переносить на сервер.

Ну в общем да именно запросить новый файл

Но ведь можно не на каждый пиксел ;-)

Можно с самого начала запросить чуть больше видимой области и маленькие движения это перекроет

А если сделать привязку к каким то определенным позициям получиться что много пользователей будут запрашивать одну и туже область, а значит и cdn тут поможет.

Я конечно упустил что у вас всего 60 кадров и что можно всеее приготовить зарание, но поняв это стал еще сеньше понимать зачем вам несколько canvas ? Которые вы хотели синхронизировать …

Разбейти вашу анимацию на области скажем в 1.5 раза больше чем обычно видимая область.

Сначала разбейте от начала, а потом со сдвигом по обеим осям на половину от размера области. Получиться перекрытие . Каждую сожмите (к этому сейчас вернемся) …

Теперь прорисовка, разве нельзя декодировать не на канвас а в невидимый буфер ?

В худшем случае вам надо декодировать 4 области и скомпоновать конечную картинку.

Ну и на счет сжатии

Во сколько раз у вас получилось ужать ? Если отсчитывать от RGB по 3 байта на точку ?

Я уверен что можно сжать в 50 раз без видимых и серьезных искажений ….

Во сколько раз у вас получилось ужать ? Если отсчитывать от RGB по 3 байта на точку ?

В исходном виде - 4.5 ГБ (если RGB брать по 3 байта на пиксель), в упакованном моем формате - 57 МБ (это уже на Brotli, который сегодня применил). Сжатие в 79 раз без искажений, только квантование цветов слегка палитру сократило, но глазу незаметно.

Разбейти вашу анимацию на области скажем в 1.5 раза больше чем обычно видимая область.

Т.е. уже секционирование (разбитие) - это норм? )) Размер секции выбран так, чтобы на мобильных экранах было всего 2-3 секции во viewport. Экраны сильной разные, это могут быть мобильники, планшеты, ПК, поэтому нет какого-то среднего значения. Размер секции я выбрал такой, чтобы компрессия была наиболее оптимальной, про это в статье написано:

Чем меньше размер секции выберем, тем меньше размер файла и мы можем адрес указывать не как 4 байта (uint32), а как 3 байта (uint24), и это хорошо сокращает размер итогового файла.

У если ваш метод смог в 80 раз

Значит стандартные видео кодеки смогут еще :-)

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

Разбивать можно и просто но стандартным значениям, скажем 1920х1080 как hd видео, его все телефоны аппаратно точно поддерживают. Можно и 4к .

А вот с адресами секций я уже действительно потерял вас

Как десяток чисел существенно влияют на сжатие ?

Значит стандартные видео кодеки смогут еще :-)

не смогут с сохранением такого же качества, как у меня :P Если не верите, то докажите, иначе получается вы критикуете приводя ложные аргументы, типа "стандартные видео кодеки смогут еще". Если не заинтересованы доказать мне мою неправоту и несостоятельность моего формата фактами какими-то, то предлагаю завершить этот увлекательны тред :D Если же вы знаете какой формат может лучше моего подойти мне - подскажите какой, буду очень признателен и в статье в UPD укажу на вас, мол, вы предложили вариант получше.

А вот с адресами секций я уже действительно потерял вас

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

И как вам «доказать»

Вы дадите исходную анимацию ?;-)

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

П.с. Перечитал вдоль и поперек …. Не нашел почему так важны размеры секций для лучшего сжатия … :-)

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

П.с. Перечитал вдоль и поперек …. Не нашел почему так важны размеры секций для лучшего сжатия … :-)

Если размер секции будет большой, для ссылки внутри файла нужно будет применять uint32, это 4 байта на адрес. Например, если секция 1000*1000, то максимальный размер файла (если брать 2 байта на пиксель после квантования):

1000 * 1000 * 2 * 60 кадров = 120 000 000

Для того, чтобы ссылаться на любой байт этого файла нужно адрес uint32.

Для хранения адреса в 3 байтах вместо 4 нужен размер файла меньше 2^24 = 16777216.

Если взять мой размер секции, то это

508 * 406 * 2 * 60 = 24749760

Видно, что мой размер секции больше, чем uint24, но не на много. Я взял по максимум, в реальности после RLE упаковки такой размер уже не будет и адреса все будут укладываться в uint24. Тем самым сотни тысяч повторений внутри файла будут ссылаться используя адреса из 3 байт, вместо адресов из 4. Надеюсь доступно рассказал)

Вы дадите исходную анимацию ?;-)

Без проблем. Одна доступна и по сети. Вы займетесь поиском формата, который лучше моего сожмет? Если да, то я расскажу как формировать URL для скачивания 2.2 ГБ png файлов.

А зачем нужна нужна абсолютная адресация ?

Ни один из известных мне форматов так не делает ;-)

Иди естественный порядок и ненужна адресация

Или фиксированные сдвиги

Или относительные и переменная длина кодирования сдвига

Но все это другая темя

Я не собираюсь искать формат

Он и так известен вернее 3

Mpeg4

H264

H265

Я сразу выберу 264 И всего лишь подберу правильные параметры :-)

ваш пример кажется вывески, которая замылилась возьму как точку сравнения

Так что давайте ссылку , можно в личку

А зачем нужна нужна абсолютная адресация ?

Ну так ведь повторяющиеся фрагменты (в который раз уточняю, вы читали статью?). Мы ищем по всем предыдущим кадрам есть ли где-то повторение фрагмента.

Так что давайте ссылку

Загружаем файл https://floor796.com/data/matrix.json

В нем массив объектов матрицы сцен. В каждом объекте есть поле preview. В нем адрес до jpg файла превью сцены. Удаляем из адреса имя файла и получаем что-то типа такого:

scene/t0l2/b1l3/fin/

В этом каталоге лежат 60 png файлов этой секции. Формат имени файла - frame_%02d.png . Например

https://floor796.com/data/scene/t0r0/t4r0/fin/frame_06.png

В итоге если вы пройдете по всем объектам матрицы и скачаете все png файлы, должно быть 2.2 ГБ. Ну а дальше нужно сделать что-то, что будет лучше моего формата работать в баузерах. Буду признателен, если сможете что-то лучше предложить, чем мой велосипед ) Однако очень сомневаюсь, так как текущий формат весит 57МБ и позволяет зумить насколько угодно сохраняя пиксельную четкость.

Он и так известен вернее 3 - H265

На всякий случай проверяйте вот такой сайт перед выбором формата, так как не все поддерживается https://caniuse.com/?search=H265

не знаю как у вас получилось с crf=1 такое качество
но вот что получилось у меня с crf=23

Я думаю оригинал вы без проблем найдете на увеличеной в 5 раз картинке.
сжатие получилось с 144МБ RGB до 1.3-1.4МБ то есть в 100-110 раз
ffmpeg -framerate 12 -i ./b1l1/frame_%02d.png -c:v libx264 -preset veryslow -tune animation -crf 23 -bf 0 -x264-params "no-deblock=1" b1l1.mkv

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


Простите, а что за mkv, я же для браузера делаю. И нет указания каждый 10 кадр ключевой. Да и в целом повторюсь, у видео форматов нет сохранения пиксельной четкости, вы никак ими не добьетесь этого. То, что вы на скриншоте показываете - это условно 100%, покажите как на 200%, будет ли также все четко, как вот на этой картинке:

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


а вот то что я показал на скриншоте это уже увеличеное в 5 раз изображение ...
правый вариант - ваш оригинал, левый и средний после сжатия .
А что вы подрозумеваете "нет пиксельной точности" я не понимаю ... все форматы прекрасно все сохраняют , сжатие произходит за счет удаления высоких частот, а не точности ...
но не суть , вам мой результат нравиться или нет ? ;-)

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

Вы просите оценить ваш результат. Я не могу его оценить, так как результата нет. Вы просто увеличили crf, убрали частоту ключевого кадра. Я с этим баловался ещё пару лет назад, тоже получал размер в 1.2МБ на 1 секцию. Вы думаете вы что-то новое предложили применив такие параметры?)) Качество плохое, а без частых ключевых кадров непонятно как рендерить постоянно растущую анимацию (вы ловко перекинули это на "тема отдельного разговора" :D)

Наверное у вас монитор лучше моего

Но я не вижу разницы между 3мя моими вариантами

Особенно учитывая что это увеличенное в 5 раз изображение В том числе вашего исходного материала

Вы привели пример в статье с параметром crf=23 но почему то у вас он выглядит хуже

Только ли изза количества ключевых кадров ? Я не уверен.

Как минимум в моем примере есть еще пару.

Ваши основополагающие предположения привели вас к решению со многими потоками проигрываемыми параллельно и к проблеме синхронизации.

И вы решили свою проблему прекрасно и изобрели действительно свой велосипед.

Квантовали значения как в rgb15

Использовали пространственные предикции как в png

И временные как у mpeg.

Что в принципе интересно с академической точки зрения.

Но если вы сможете понять и поменять вводные предположения, то решение будет другое

В этом посте много было хороших советов, некоторые я применил. А в диалогах один пользователь предложил вариант с webp и показал какие результаты он получил с ним.

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

В любом случае спасибо за ваше мнение и потраченное время)

Я с удовольствием потратил время :-)

А что на счет «пиксельной» четкости у webp ?

Надеюсь вы вкурсе что это Сжатие вточности как ключевые кадры у vp9 формата ? И с таким же успехом можно использовать 264 :-)

Ну если разобраться в параметрах :-)

там качество сильно плавает в зависимости как сжимать. По сути те же результаты, что и при webm (vp9). Однако идея webp в другом - распаковывать секции на wasm и рендерить каждый кадр каждой секции отдельно. Тем самым можно как минимум убрать синхронизацию видео потоков. Но это по сути разрабатывать многопоточный wasm, а на выходе профит будет не сильно большой по сжатию и опять же теряется пиксельная четкость (которую вы не видите, к сожалению :D ).

Т.е. идея webp в том, что с ним легче работать на ЯП, проще распаковывать.

ЯП … это извините что ?;-)

Но если вы так хотите загрузить пользовательское устройство … почему пост обработка не стала кандидатом ?;-) и умный апскэйл ?

Вы упорно ссылаетесь на пиксельную четкость, даже в вашем исходном материале :-)

Какой материал такая и четкость :-)

ЯП - язык программирования.

Извините … что ? Webp легче запрограммировать? Чем ключевые кадры в мпеге ? Или 264 ?

А это то откуда вы взяли ????

Короче, это мой последний ответ вам, так как ваши вопросы просто отвлекают, особо ничем не помогают...

Webp легче запрограммировать на rust, чтобы потом перекинуть на wasm, который будет загружаться в браузере. Но я не проверял как на rust распаковывать mp4 или другие форматы, поэтому не обладаю полной информацией. Может окажется, что webp сложнее остальных распаковывать. Не знаю, поэтому эту идею не хочу обсуждать пока не попробую ей на деле и не изучу все

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

Если из академического интереса ,ладно, но с практической точки зрения ?

Webp технически является ключевым кадром vp9

Точно так же можно использовать h264

И все на аппаратном уровне :-)

Но …

Можете не отвечать :-)

Значит стандартные видео кодеки смогут еще :-)

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


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


Это как сжатие в /dev/null: Работает сильно круче любого алгоритма, только одно маленькое условие задачи нарушает — распаковка не для всех файлов работает.

Приведенные в статье артефакты не связаны только с меньшим разрешением в цветовой составляющей. Это обычные артефакты квантвания которые проявляються так же и на чернобелой составляющей.
10 бит может быть так же с уменьшеным разрешением по цвету, так как относиться только к динамическому диапозону каждой отдельной компоненты.
У стандартных кодеков есть поддерживаемые профили, и да в начальном (base) ни mpeg4 h.264 не поддерживают ни 10бит ни YUV 4:4:4 - полное разрешение по цвету, но main уже поддерживают ...
Но можно ли этим воспользоваться из браузера , другой вопрос ...

Экраны сильной разные, это могут быть мобильники, планшеты, ПК, поэтому нет какого-то среднего значения.

Справедливости ради - экраны мобилок в основной массе в среднем являются FullHD, по статистике Steam 68% мониторов с разрешением 1920х1080, а 68% двухмониторных систем тоже имеют два устройства FullHD, поэтому, де-факто, средним разрешением монитора будет 1920х1080 от которого и можно плясать.

Кстати вы могли бы собирать свою статистику по разрешениям систем тех, кто открывает ваш сайт и отталкиваться от неё.

Статистика ведь будет сильно разной в зависимости от сайта, аудитория везде разная ведь. Я статистику собираю, в моем случае 59,5% пользователи моб устройств. В среднем ширина экрана где-то 400px у этих пользователей. И 36% пользователей ПК, у них преобладает ширина 1920.

Не подвергаю сомнению ваши данные, только задумываюсь кто те люди, которые в 2022 году пользуются такими древними аппаратами, последние 4 года мейнстрим это 720р и сейчас происходит переход на 1080р. Ладно экран, но андроид на них какой-то жутко старой версии должен быть. Но это так, мысли вслух.

Возможно вы не те цифры смотрите)

У меня, например, Redmi Note 9 Pro, у него написано, что экран 2400×1080. Это количество точек на экране. Но количество точек не равно количеству пикселей. У этого телефона DPR 2.57, т.е. столько точек в одном пикселе. Берем 1080 делим на 2.57 и получаем 393. Именно такое число пикселей будет ширина любого сайта на этом телефоне в вертикальном положении. Это легко увидеть, достаточно распечатать window.innerWidth или screen.availWidth в js на любом сайте, где включена адаптация под мобильники.

Поэтому когда DPR выше 1, то нужно ширину устройства делить на DPR, чтобы узнать реальное количество пикселей )

Смотрели ли вы в сторону сжатия алгоритмом zstd вместо gzip? Так как у него лучше параметры сжатия а самое главное время распаковки намного меньше, мне кажется это идеальный вариант для такой задачи.

Просто я недавно натыкался на проект zstd-wasm.

неа, не смотрел) Спасибо за наводку, попробую его применить вместо gzip.

Он вообще бомба, я везде пытаюсь теперь его юзать вместо gzip.

Консольный zstd работает раз в 20 быстрее gzip, при этом жмёт по дефолту процентов на 15 лучше.

Либа умеет в тот же интерфейс, что и zlib (за исключением деталей). И умеет ещё подготовить словарь, и с ним жать ещё лучше.

Т.е. как general usage она уже прямо сразу выигрывает.

А можно посмотреть специфику и подобрать (возможно) ещё более подходящий под конкретную задачу. Их нынче куча brotli, lz4, lbzip2, pxz и т.д.

Я уже переключил все на Brotli. В итоге вместо 82МБ стало весить 57 :) Правда пришлось расчехлять wasm, так как декодировать Brotli не просто так) На транспортировку HTTP я не могу Brotli подключить, так как у CDN это платная функция, да и я сжимаю максимальным уровнем 11, подозреваю CDN не предлагает такое сжатие (хотя нужно проверять).

А почему тогда сразу не присылать в бразуер чанки GZIP обжатые на стороне сервера с заголовком Compression, чтобы браузер их распаковывал пока они летят?

Тогда можно заюзать brotli, а не gzip, у него больше степень сжатия на это все.

И формально у вас кадр 100mb, так как пожатый brotil - 10mb по сети, и на js все равно прилетает 100mb.

Просто непонятно почему не отдали? Он должен сделать распаковку, и сделает ее всяко эффективнее.

(опять дубляж, но просто непонятно почему не решились)

Этот вариант рассматривал. Особого проигрыша в том, что я распаковываю на стороне клиента, не увидел. Стандартным API браузера или даже pako скорость распаковки gzip очень высокая. Но проблема появляется в хранении, бэкапах и CDN кеше. Если я буду хранить их в распакованном виде, то будет требовать всего больше: диска, кеша CDN, времени копирования бэкапов. Поэтому мне выгодно для роста хранить все уже сразу в сжатом виде.

Тем не менее я ещё не до конца уверен, что в будущем буду использовать gzip, так как уже даже в этих комментах мне посоветовали zstd попробовать. Возможно ещё замерю Brotli, если выигрыш будет большой, то тогда закрою глаза на проблемы с хранением больших файлов и переключу все на компрессию HTTP трафика)

Зачем?
Настройте nginx static compression и храните их прям пожатые.

https://docs.nginx.com/nginx/admin-guide/web-server/compression/

Те они уже будут пожаты лежать.
Многие публичные сервера автоматически ресурсы типа filename.extension.gzip/br отправляют с заголовком

Интересно, спасибо) Если буду переключать на Brotli, то попробую nginx static compression. Хотя CDN все равно будет пережимать, и на кеш CDN это не повлияет вероятно.

Интересно было бы посмотреть как с этим пиксельартом справился бы QOI. Вроде бы он тоже почти RLE и также дополнительно хорошо сжимается стандартными brotli или gzip.

Учитывая, что у автора ещё и сжатие за счёт повторяющихся последовательностей пикселей, которое добавили из-за недостаточной эффективности RLE — скорее всего, так себе покажет

Все же продолжу про WASM.

С WASM + SIMD вы бы получили более быстрый холодный старт и более мелкий рантайм.

Да, используя SIMD нужно ногу сломать чтобы правильно векторизовать это все (так как не все могут вычислять 4 команды враз), но так как у вас RGBa, то все векторные операции с ним можно сделать в 4 раза быстрее из-за одной операции.
Пока бинариен (это то что есть wasm-opt) не умеет (или уже умеет?) сам векторизовать операции такого типа.

Я вижу у вас есть как <<, так и +*/.

Ну и не хочется Rust/C++, есть всегда:
https://www.assemblyscript.org/

Как раз сравнение генерации фрактала (без SIMD):
https://colineberhardt.github.io/wasm-mandelbrot/#WebAssembly

(но на производительных девайсах разница небольшая)

Спасибо за совет) Как вариант для улучшения попробую рассмотреть. Я изначально думал только на wasm все реализовать и делить потоки на нем, но что-то меня сильно отпугнула сложность реализации многопоточности rust wasm, решил оставить на потом) Т.е. сама многопоточность на rust - не проблема, но, как я понял из статей, реализация её для wasm уже не так просто. Но в rust я новичок, так что многого не знаю.

Как я понимаю у вас нет возможности уменьшить сцену, чтобы видеть больше "комнат" на экране, во всяком случае у меня не получилось на телефоне. Планируете ли вы такую возможность добавить?

в новом движке рендера (про который статья), я сделал 3 уровня масштабирования: 200%, 100%, и 70%. Могу ещё 50% сделать для мобильников (где экран небольшой), но для больших экранов слишком мелкий масштаб сильно повышает расход CPU, поэтому там только до 70% можно будет уменьшать.

Но если вы откроете https://floor796.com без параметра render=2, то там запустится старый движок рендера, он на mp4 и там масштаб можно уменьшать до 50%. Зато нельзя его увеличивать выше 100%, так как будет размытие (то, что я хотел исправить, разрабатывая свой формат).

Нельзя ли заранее склеить уменьшенные до нужного масштаба сцены и отдавать их? То есть не просчитывать масштабирование на клиенте с кучей чанков, а в зависимости от выбранного масштаба загружать сцены, где уже 4 сцены объединены в один чанк и уменьшены в размере в два раза и тд для каждого варианта масштабирования. По тому же принципу, как это сделано на картах (Яндекс карты и тп)

Да-да, думал такое сделать) Возможно в будущем именно так и сделаю уменьшение меньше 70%. Сейчас просто особо смысла нет делать сильное уменьшение, как в Гугл или Яндекс картах, блоков ещё не так много пока что

ну .... на склеивание и уменьшение вы согласны на сервере ? ;-)

конечно нет) Самое нелепое, что может быть, это перекидывать вычисления зума с клиента на сервер. То, что можно вычислять на клиентских машинах выгоднее на них и вычислять.

Однако в данном треде речь про подготовку видео файлов (своего формата) с заранее уменьшенной картинкой. Это как mipmap в рендеринге 3d. Подготовка делается не налету (как вы предлагали в другом треде), а 1 раз после изменения чего-либо на сцене, т.е. всего пару раз в неделю такое происходит.

Ок

Но не важно надету или нет вы идете к решению что ненужна постоянная дележка на области а лучше делить по точке просмотра

И как это сделать для всех вариантов ?

я как мог объяснил вам) Да и в статье все в целом изложено что да как. Извините, как-то лучше не могу изложить.

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

Включите в браузере «версия для пк» или что-то такое. Почему-то мобильные браузеры не дают сделать сайт мелким.

Огромная работа, поразительно. Узнал каждую отсылку.

Обожаю гигантские детализированные пиксельартные работы. А тут еще и с анимацией!
Потрясающий проект!
Желаю успехов и развития!

Успехов ! Лишь бы "Левшой" не оказаться

Отличная работа. Сколько времени разработки занял переход на новый формат?

На удивление 1 неделю всего. Но это, наверное, 3я или 4я попытка уже. В течение последних 2 лет я все время хотел уйти с рендера на mp4 и пробовал разные варианты, все были неудачными. И вот пару недель назад ещё одна попытка перейти на canvas оказалась успешной)

Я думал танцы с костылями надумают разобраться как работают анимации в простых видеоиграх, тем более раз делаете идейного наследника Club Control ("Клубные замарочки") - адаптировать их зацикленную анимацию под WebGL, и сделать первую полноценную браузерную игру, а тут...

но технология видеоигр никак не подходит для такой задачи. Тут нет повторений, каждый объект уникален. По сути если бы это была игра, то пришлось бы каждый объект отдельно хранить в виде спрайтов и рендерить отдельно. У меня в исходниках все так и хранится, каждый объект - отдельный файл. Вся анимация состоит из уже более чем 480 объектов, в сумме они имеют 3900 слоев, и каждый слой имеет от 1 до 60 спрайтов. В исходном виде это весит все 15.1 ГБ (КАРЛ!) чисто в PNG формате (не путать с 2.2ГБ, про которые я в статье писал, они уже после склейки всех слоев). В прошлом я разрабатывал много игр 2d и пока что не понимаю какой именно опыт разработки видеоигр позволит 15ГБ загрузить в браузере и показывать как анимацию быстрее, чем склейка всего этого в видео форматы и просто воспроизводить их. В идеале я бы хотел воспроизводить на фрагментном шейдере, но, увы, ограничения GLSL не позволяют написать программу распаковки сильно сжатого формата, только простое сжатие можно распаковывать на GLSL, а это где-то всего 30% будет сжатие. В этой статье я рассказал как построить сжатие без видимой потери качества на 96% и почти без ощутимых последствий рендерить на CPU.

но технология видеоигр никак не подходит для такой задачи. Тут нет повторений, каждый объект уникален. По сути если бы это была игра, то пришлось бы каждый объект отдельно хранить в виде спрайтов и рендерить отдельно.

Прямая цитата из статьи:

Поделюсь с вами успешным опытом разработки рендера в браузере большой, постоянно расширяющейся анимационной сцены, состоящей из множества мелких двигающихся объектов, зацикленных в 5 секунд.

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

Я правда не могу сказать, трехмерный там полигон или используется polygon в 2D-отрисовке в него картинки, по сути простой текстуры.

То есть вообще смысл такого рендера для браузерной игры, как у вас, если можно сделать одну развернутую текстуру и движущиеся части (ноги, руки), определять отдельными треугольниками, рассчитываемыми на лету. Да, вы получите наложение одной картинки на другую, но именно так оно и работает.

Ещё цитата:

Чтобы браузер мог достаточно быстро синхронизировать видео до нужного нам кадра необходимо иметь частые ключевые кадры (т.н. i-frames). 

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

Обычно в виде одновременного воспроизведения нескольких аудиофайлов формата wav или mp3 на одно аудиоустройство. Честно говоря, я бы изучил этот вопрос по-лучше несколькими простыми экспериментами, нежели заморачивался с костылями, чтобы получить тот же эффект.

что ему обязательно нужна синхронизация кадров со звуком.

не совсем понял откуда вообще звук появился)) Я делаю немой проект, там нет звука. Там нужна синхронизация видео потоков.

В любом случае спасибо за высказанное мнение

Шаг 5 можно делать за линейное время, например, с помощью суффиксного автомата (возможно, есть и более простые способы это сделать).

Упрощённый пример (для каждого суффикса исходной строки найти наибольший префикс, который полностью встречается раньше суффикса): https://godbolt.org/z/eeM4sccdM

Интересно) Я пока что ускорил шаг 5 созданием простого двумерного инвертированного индекса всех пикселей во всех предыдущих кадрах. Т.е. с помощью такого индекса можно для двух подряд идущих пикселей найти все позиции во всех предыдущих кадрах, тем самым поиск очень ускоряется. Но индекс такой требует очень много памяти, правда это не проблема, ведь упаковку сцен запускаю не часто, где-то пару раз в неделю.

Да, тут тоже будет проблема с памятью. В частности, текущая версия использует 1024*вес строки памяти, но можно дооптимизировать константу до ~10.

И конечно, без данных нельзя предсказать будет ли вообще моё решение работать быстрее вашего)

Для mp4 можно было бы использовать h.265 или даже av1, будет меньше весить. Браузеры уже поддерживают av1.

Многие устройства не поддерживают av1 аппаратно, поэтому это будет на процессор давить сильно.

Например, H.264 при самом лучшем качестве (crf=1) почему-то все равно делает ужасное искажение красных линий на темном фоне:

Это потому что видео чаще всего кодируется в YUV 4:2:0 пиксельном формате: яркость (luma) закодирована в оригинальном разрешении, а вот цвет (chroma) — с половинным разрешением. Это как в mp3 выбрасываются неразличимые ухом звуки, так и во всех современных видео кодеках выбрасываются невидимые (по идее) глазу детали. Если пошаманить с пиксельным форматом, то искажение исчезнет. Всякие High444p h264 профили не уменьшают нигде разрешение. Но вот эти профили не во всех браузерах поддерживаются, насколько я знаю.

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

Визуально было чем то похоже на вашу текущую анимацию.
Как это выглядело для пользователя, ну например можно тут посмотреть
https://youtu.be/Tl9jS8r5mjQ?t=617

Как это работало, есть небольшая статья (и возможно где то можно ещё найти)
https://www.redditinc.com/blog/how-we-built-rplace/

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

если не ошибаюсь, то там была очень ограниченная палитра, 8 или 10 цветов кажется

Поправка: 16 цветов.

Палитра изначально имела меньше цветов и канвас был 1000х1000. Потом кол-во цветов и размер поля были увеличены.

Значок DVD никогда не попадает в угол :)

А дети в самом справа ))) там еще контейнер - это на группу Pink Floyd Another Brick In The Wall ??? И там же часы из назад в будущее )))

Да, это Pink Floyd) Часы тоже оттуда, они были в клипе, вот тут можно их увидеть https://youtu.be/YR5ApYxkU-U?t=177 .

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

Иногда при подсветке персонажа (не обязательно именно этого) внизу появляется линия (скорее всего, когда это дело находится на стыке анимаций, но это не точно). Если что, 10 винда и хром 102.0.5005.115

А так работа проделана, конечно, колоссальная

Спасибо) Это не совсем связано со стыками анимации. Это больше связано с самим способом подсветки. Для подсветки используется 4 элемента <div> полупрозрачных, их я выстраиваю так, чтобы они закрывалю всю сцену, кроме нужного фрагмента. И вот иногда они стыкуются плохо. Я попробую найти замену такой подсветке. Если полностью перейду на рендер через канвас, то подсветку переделаю на него и тогда все будет четко ;)

Кайфанул, очень клево!

Да, здорово.

Я сам уже давно не в IT (с 2017 года), поэтому вопрос: в JS напряжно с битовыми операциями? Я к тому, что чем не угодила связка: базовый, он же первый, он же единственный полноценный кадр (субкадр, секция, ячейка) + битовая карта измененных пикселей + список замен? Все части весьма неплохо ужимаются. И рендерить на канвас можно, как минимум, двумя способами. Попиксельно, со сверкой с картой и подстановкой замен в позиция, отличных от ноля в карте, и "пакетно", пробегаясь по карте в поисках следующей единицы в карте и копируя оригинал пачками пикселей с подстановкой замен в разрывах цепочки нолей.

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

Я пробовал похожий вариант с упаковкой каждого пикселя и его изменения. Т.е. грубо говоря у нас есть пиксель первого кадра, мы смотрим все изменения этого пикселя в других кадрах, упаковываем это. И полученный "код" заносим в словарь, чтобы реиспользовать. Все это реализовал, но по итогу получился очень большой файл, в несколько раз превышающий mp4. В итоге как я не пробовал этот способ оптимизировать всегда выходило так, что таком способом упаковка анимации дает плохой результат как минимум в моей анимации)

Прямо сцена из Ready Player One. Прекрасная работа!

НЛО прилетело и опубликовало эту надпись здесь

Очень интересно и прекрасно написано !!!

Здравствуйте, мне очень понравился ваш проект, если вы не против, я сделаю обзор на него в своих "шортс" и полном обзоре на другие новости графики на своём YouTube канале? Так же я хотел бы опубликовать вашу работу в своей группе вк посвящённой 2д и 3д графике и дизайну

Здравствуйте) Извините, не увидел сообщение ранее. Я, конечно, не против)

супер-классная вещь, очень залипательно)

у меня важный вопрос - где Аянами Рэй? =)

С удовольствием перечитал еще раз вашу статью, спасибо, очаровательно!!!

Зарегистрируйтесь на Хабре, чтобы оставить комментарий