Итак, вы техник и художник, тартист, ну, эта статья для вас, здесь мы исследуем, как переносить массивные рисунки SVG через WebSockets с помощью сжатия строк LZW и распаковывать их в пользовательский интерфейс.
Под массивными рисунками я подразумеваю файл размером от 2 МБ и выше. Иллюстрация выше весит чуть более 6 МБ в исходном формате. Что по веб-стандартам огромно. Тем не менее, если вы доставляете его крошечными частями, интерфейс может сразу начать отвечать, и пользователь не ждет, когда что-то произойдет. По сути, это накопительный опыт с нулевым ожиданием. Побочным продуктом потоковой передачи эскиза, строка за строкой, является то, что пользователь получает мгновенный ответ и получает «опыт» процесса рисования, как это происходило изначально.
Убедитесь в этом сами. Http://lifedrawing.fliptopbox.com/#5
Эта статья - НЕ пошаговое руководство, а скорее комментарий к различным идеям, которые использовались для достижения результата. Это также самокритика, оглядывающаяся на мой код почти шесть лет спустя, признание и признание того, как эволюционировал JavaScript и меня вместе с ним.
Вы узнаете больше из своего собственного любовного труда, чем из ответа поисковой системы. Один учит вас «учиться», другой - копировать. Делайте и то, и другое в равной мере.
Как вы делали скетчи SVG?
Вы записываетесь в класс рисования, и вместо карандаша и бумаги вы используете цифровую ручку, которая записывает ваши штрихи. Вы когда-нибудь слышали о Wacom Inkling? Это (к сожалению) мертвый продукт. Огромная грусть, потому что это то, что я использую. Это чернильное перо с прикрепляемым приемопередатчиком, которое вы прикрепляете к странице, и оно записывает штрихи пера. Когда вы закончите, вы используете программное обеспечение Wacom для преобразования необработанных данных в иллюстрации SVG, которыми вы можете управлять с помощью программы векторного рисования, такой как Inkscape или Illustrator.
Преемником Inkling, похоже, станет Neo Smartpen N2, независимо от того, какой продукт вы используете, важно то, что он экспортирует векторные иллюстрации, и если он изначально поддерживает SVG, это здорово.
После многих неловких месяцев, когда вы смотрели на голых людей и отчаянно пытались улучшить координацию глаз и рук, у вас должен быть объем работы, чтобы использовать это для доказательства концепции. В качестве альтернативы у меня есть SVG, который вы можете позаимствовать. Я настоятельно рекомендую в первую очередь маршрут «голые люди».
В принципе, этот метод будет работать для любого SVG при условии, что XML-DOM соответствует определенной структуре, и мы можем определить эту схему. Подробнее об этом позже.
Прежде чем перейти к техническим аспектам, нужно сказать одно о рисовании шариковой ручкой. Не многие люди делают это по одной простой причине. Это неумолимо. Карандаш, который вы можете размазать или стереть, а не перо, поможет скрыть ваши ошибки. О нет, привет! Вы все видите.
Мне как художнику нравится эта дисциплина; это заставляет вас совершить инсульт. А когда ошибки накапливаются, это также показывает, как вы искали линию формы, и подсказывает, как ваш глаз исследовал формы и волнистости, пытаясь перенести их на страницу. Почти как будто муха, обмакнутая в чернила, ходила вокруг.
Самое замечательное в «ручке SVG» - это то, что вы можете потом редактировать свой рисунок. И я сделал именно это, я не мог удалить отметку от ручки с бумаги, но я мог удалить путь с холста. И изменить заливку, обводку, композицию и т. Д.
Итак, вот полный обзор поездки туда и обратно TL; DR
С сервера мы хотим взять документ SVG…
<svg width="598px" height="697px" viewBox="0 0 598 697" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"> <title>portrait in 60 seconds</title> <desc>(c) 2014 Bruce Thomas - Life drawing</desc> <defs></defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> <g id="Sketch166" sketch:type="MSLayerGroup" stroke="#9B9B9B" stroke-width="0.0966" fill="#4A4A4A"> <path id="Shape" sketch:type="MSShapeGroup" d="M328.624,374.943 L329.252,373.2 L329.881,371.648 L330.564,370.32 L331.039,368.955 L331.477,367.821 L331.575,364.073 .....(very, very long).... L330.303,365.281 L330.249,365.506"></path> /> </g> </g> </svg>
Превратите его в литералы объекта Javascript для JSON.stringify ()…
{ type: "svg", attributes: {width: "598px", height: "697px"} } { type: "title", value: "portrait in 60 seconds"} { type: "desc", value: "(c) 2014 Bruce Thomas - Life drawing"} { type: "defs", value: ""} { type: "g", attributes{id: "Page-1" stroke: "none", "stroke-width": "1" "fill": "none" "fill-rule" "evenodd" }} { type: "g", attributes: {id: "Sketch166"} } { type: "path", attributes: {id: "Shape", "d": "M328.624,374.943 L329.252,373.2 L329.881,371.648 L330.564,370.32 L331.039,368.955 L331.477,367.821 L331.762,366.771 ........ L330.346,364.787 L330.249,365.506" }}
И, наконец, отправьте сильно сжатую строку UTF8, например, эту…
M328.624,374.943 LĂ9.252ĉ73ĕđē.881ę1ą48ĝ30.56ĈĊĪĂ Ē3Ĥ039ĉ6Ą955ĨĤ477Ĺ7ğ21Ŀ.7ĆĹ6Ŋ7ňijĤ8ĬĹ5ņIJ3ĴŊ98Ŗ.06ʼn753ĹČ514ʼn5ŤŧŠĚʼnķŞ36ěĠ3řśŇĹŷĐŒŠ0ĮŶğŚĨĪġŦŵŷďƇą9ƊƄ99ź......ɌȺțƝȯǜĠɧʮ͜ȱ
… В браузер, который «распакует» сжатую строку, JSON анализирует ее обратно в объект, чтобы оператор switch мог определить тип элемента (пространство имен) и создать соответствующий элемент DOM, вложенный каждая из входящих частей в соответствующие дочерние элементы. А затем повторите все заново для следующего наброска. Конец.
Сжатие струны с LZW
Ладно, что это за черная магия? И зачем это делать? Посмотрите на этот аргумент. Вот последовательность чисел: 65,66,67 с длиной байта 8 (включая запятые). Однако, если мы использовали символ ASCII вместо чисел, мы получили бы этот ABC с длиной 3. LZW (Lempel Ziv Welch) использовали эту идею в своем алгоритме, который фактически стал сжатием без потерь для GIF и TIF. За исключением того, что они пошли еще дальше, они сложили их так: 65 + 66 + 67 = 198 (или символ: Æ) с длина… 1. Очень умная штука.
Если вы хотите изучить это самостоятельно, вот суть прототипа String.
Как мы транслируем XML?
Осторожно, спойлер, нет. Когда я начал этот проект (очень давно), я совершил первоначальную ошибку, выбрав XPATH - традиционный способ работы с XML. НО я отказался от этого, потому что, по сути, он должен открыть весь файл, прежде чем он сможет выполнить его синтаксический анализ. В этом случае вы также можете использовать GET для получения SVG и дождаться загрузки. Вы видите, что XML, HTML и SVG - это модели DOM. Некоторые более строгие, чем другие. К счастью, XML / SVG очень строгий.
Я отказался от подхода XPATH и решил поверить в строгость SVG; это позволяет нам делать некоторые предположения. Все теги сбалансированы; для каждого открывающего тега есть закрывающий тег. (В отличие от HTML, который очень терпим, например ‹img src =” thing.jpg ”› - незакрытый тег). Другое предположение состоит в том, что все вложено. Это означает, что мы можем использовать текстовый поток для определения родительского элемента, к которому будут добавлены остальные.
Давайте проиллюстрируем это на примере.
На сервере мы запускаем узел с двумя пакетами NPM, «websocket» и «express», остальное - узел по умолчанию. Сервер размещен на Heroku, которая дает вам «Dyno» бесплатно, навсегда, для личных проектов.
const fs = require('fs'); const readline = require('readline'); ... const rl = readline.createInterface({ input: fs.createReadStream(svgFileName), output: process.stdout, terminal: false }); rl.on('line', (text) => { //// gimme everything up to the \n });
readLine предоставит нам эту первую строку…
<svg width="598px" height="697px" viewBox="0 0 598 697" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
... когда он начинает потоковую передачу текстового файла построчно, он не закрывается, но ... это все, что нам нужно, потому что клиентский JavaScript создаст закрытый элемент, все, что ему нужно, - это соответствующие атрибуты. Материал, выделенный полужирным шрифтом. Все остальные атрибуты являются частью пространства имен элемента, которое создается автоматически.
Клиентский код будет представлять собой цикл, который делает что-то вроде этого:
const ns = "http://www.w3.org/2000/svg"; const type = "svg"; const el = document.createElementNS(ns, type); el.setAttribute("width", "598px"); el.setAttribute("height", "697px"); el.setAttribute("viewBox", "0 0 598 697");
Это означает, что наша схема JSON для сообщения Socket пока проста и учитывает 4 из 7 тегов, которые нам нужно использовать.
{ type: "svg", attributes: {width, height, viewBox}}
Следующий тип тега, который нам нужно разместить, позаботится об оставшихся трех. Этот тип не имеет атрибутов НЕТ и одного текстового элемента, поскольку он дочерний.
<title>portrait in 60 seconds</title> <desc>(c) 2014 Bruce Thomas - Life drawing</desc> <defs></defs>
Для этого мы доводим схему до окончательной формы.
{ type: "String", attributes: {Object} -OR- null, text: "String" -OR- null }
Итак, когда схема для сообщений WebSocket готова, нам нужно выяснить способ извлечь тип и атрибуты (или значение) из входящей строки текста и убедиться, что она игнорирует мусор, то есть комментарии HTML и теперь ненужные закрывающий тег. Это идеальный кандидат на…
Обычные выражения
Регулярные выражения ниже - это все, что нам нужно. По общему признанию, мы будем использовать их в двух разных частях, но в первую очередь он идентифицирует элемент тега и пары ключ / значение атрибута, игнорируя при этом мусор.
/<(\w+)\s(([\w]+)="([^"]+)"|[^<]+)>/gi ///// tag with attributes /<(\w+)>([^<]{0,})/gi ///// tag with innerText only
Возможно, вы уже знакомы с регулярными выражениями, а может, и нет, посетите этот феноменальный веб-сайт regex101.com. Я подготовил для вас тематическое исследование, связанное с этой статьей. Я часто использую этот инструмент. Так полезно для отладки! Так хорошо! Наслаждаться.
- атрибуты - https://regex101.com/r/AHAspm/1/
- innerText - https://regex101.com/r/AHAspm/3
- группа захвата ключей / значений - https://regex101.com/r/AHAspm/4 (снимок экрана) обратите внимание, что это регулярное выражение на самом деле является последней группой захвата нашего первый RE, часть, выделенная полужирным
Что мне нравится в использовании RegEx, так это то, что вы пишете одно выражение и используете его как для проверки целостности - с помощью test () -, так и для извлечения содержимого с помощью match (). В match замечательно то, что он помещает вас в область массивов и всех прекрасных методов, которые поставляются с ES6 Array.prototype.
Вот сравнение того, как это было изначально и как новый рефакторинг достигает той же цели: первый цикл проходит по массиву, разбивает пары на «=», удаляет ненужные кавычки и пробелы, а затем добавляет ключ и значение в глобальный Объект. Второй не выполняет итерацию в цикле, он использует методы и возвращает полный объект. Нет глобальной переменной. Оба используют в основном один и тот же RegEx, который не изменился, но код ... существенно.
Исходный
var pairs = text.match(/\s([^=]+="[^""]+")/g); var atts = {}; // Parse the correctly formatted XML text line into // attribute pairs, and store them in a dictionary pairs.forEach(function(pair, n) { var array = pair.split('='), key = String(array[0]).trim(), value = String(array[1]) .trim() .replace(/^"|"$/g, ''); // add the pair to the dictionary atts[key] = value; });
Рефакторинг (одна строка кода, разорвана для удобства чтения)
const pairs = /\s?(([\w:]+)="([^"]+)")/g; // Create an Object by concatenation of mapped results const attributes = Object.assign.apply({}, attributes.match(pairs) .map(string.match(/(\w[^=]+)="([^"]+)"/).slice(1,3)) .map(([key, value]) => ({[key]: value})) );
Чтобы превратить отличный код в дерьмовый код ... просто подождите пять лет, иногда всего за 3 месяца можно сделать то же самое: D
Было сложно выпустить код этого проекта, потому что он очень устарел. Я собирался преобразовать его в ES6 и сделать вид, что он лучше, чем был на самом деле. И я делаю это, но я хотел, чтобы вы знали, что сам код не важен. Проблема важна, потому что проблема остается прежней.
Интерфейс - это то место, где все происходит
Есть ошибка во внешнем интерфейсе, и она относится к группе SVG («g»). Группа похожа на папку в вашей файловой системе; он может содержать файлы или другие папки. Итак, знание того, в какую папку поместить файл, - это ошибка.
Создать элемент SVG просто, все они используют один и тот же шаблон, за одним исключением, и для нашей схемы одна функция позаботится обо всех вариантах использования.
function getSvgElement( type, attributes, classnames = null, ns = 'http://www.w3.org/2000/svg' ) { const el = document.createElementNS(ns, type); // handle inner text if (typeof attributes === "string") { el.innerHTML = attributes; } // handle attribute Object if (attributes && attributes.constructor === Object) { Object.entries(attributes).forEach(array => { const [key, value] = array; el.setAttribute(key, value); }); } // add CSS classnames (if they exist) if (classnames && classnames.constructor === Array) { el.classList.add(...classnames); } return el; }
Поскольку мы не просматриваем «иерархию папок», а просто помещаем материал в последнюю группу, мы можем использовать метод array slice (), чтобы гарантировать, что входящие пути попадают в правильную группу. Небольшое предостережение ... любой ценой избегайте работы непосредственно с SVG DOM.
С точки зрения производительности дешевле сохранить ссылку на элемент DOM, чем всегда получать его с помощью чего-то вроде document.getElementById (). В итоге я добавил элемент группы SVG к его родительскому элементу в DOM и сохранил тот же элемент в массиве. Все последующие добавления в группу выполняются путем вырезания последнего элемента из ссылочного массива.
// a snippet of the above explanation // it uses the function getSvgElement() defined above this.groups = []; this.svg = {the root SVG Document}; select (type) { ... case 'g': const last = this.groups.length; const classnames = ['svg-group', `group-${last}`]; const g = getSvgElement(type, attributes, classnames); //// if the group array exists use the last element const group = last === 0 ? this.svg : this.groups[last - 1]; group.appendChild(g); //// keep a reference (used by 'path' below) this.groups.push(g); break; case 'path': // a path is always nested within a group // so grab the DOM reference for last added group let parent = this.groups[this.groups.length - 1]; const path = getSvgElement(type, attributes, ['svg-path']); parent.appendChild(path); break; ... }
Последние штрихи
Теперь, когда у нас есть все линии в DOM, мы можем улучшить эскиз с помощью некоторых случайных стилей CSS. На гифке ниже вы заметите всплески очень толстых белых линий. И если вы посмотрите внимательно, вы также увидите, что некоторые линии становятся тоньше и намного темнее (на плече и на левых ресницах). Они добавляют немного дополнительного контраста в изображение, все это подвергается постобработке и добавляет приятные эстетические детали, что делает каждый эскиз поистине уникальным с точки зрения энтропии.
Постобработка выделения применяется только к путям, превышающим определенную длину, и эти пути были случайным образом отфильтрованы для еще меньшего подмножества. Имейте в виду, что некоторые из этих набросков содержат более 1800 путей, а один путь может легко превышать 25000 символов. Поэтому использование в DOM последних обновлений CSS лучше всего ограничить минимальным набором элементов.
Утилиты, которые помогли составить эту статью:
Если вы похожи на меня, то вам нравится проверять «другие» вещи… вещи в фоновом режиме, которые используют другие разработчики; плагин, инструмент или оконный менеджер, так что вот некоторые вещи, которые можно взять с собой.
- Анимированный захват экрана в формате GIF: Peek - https://github.com/phw/peek
- Прототипирование JavaScript в реальном времени: Quokka - https://quokkajs.com/
- Регулярные выражения: RegEx101 - https://regex101.com/
- Редактор Markdown (встроенная, а не двойная панель) Typora - https://typora.io/
- Фрагменты кода: Суть - https://gist.github.com/
- Веб-браузер: Brave - https://brave.com/
- Корректор (я дислексик): Грамматика - https://www.grammarly.com/
- Приложение для графического дизайна - Figma - https://www.figma.com/
Запрос от автора - привет, это моя самая первая статья на Medium, поэтому я буду очень признателен за ваш отзыв. Эта статья слишком длинная, расплывчатая или скучная? Что бы это ни было, дайте мне знать, я очень хочу улучшить свою игру.