Итак, вы техник и художник, тартист, ну, эта статья для вас, здесь мы исследуем, как переносить массивные рисунки 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 лучше всего ограничить минимальным набором элементов.

Утилиты, которые помогли составить эту статью:

Если вы похожи на меня, то вам нравится проверять «другие» вещи… вещи в фоновом режиме, которые используют другие разработчики; плагин, инструмент или оконный менеджер, так что вот некоторые вещи, которые можно взять с собой.

Запрос от автора - привет, это моя самая первая статья на Medium, поэтому я буду очень признателен за ваш отзыв. Эта статья слишком длинная, расплывчатая или скучная? Что бы это ни было, дайте мне знать, я очень хочу улучшить свою игру.