Pull to refresh

Электронное табло 2 или с пользой для общества

Reading time12 min
Views2.5K
Последнее время на Хабре появляется не так много хороших статей о веб-разработке. Но сейчас не об этом.
Иногда хабралюди делятся интересными идеями, но не раскрывают их сути. И может зря. Потому как в ходе написания статьи (описания) замечаешь то, чего не видел раньше, другие ходы и решения, а читающим проще понять идею и дать дельный совет.
Так несколько дней назад на хабре появилась статья «Электронное табло», в которой автор поделился ссылкой на свою поделку, но из-за недостаточного описания статья получилась из разряда — «посмотри, что я сделал».
Насколько инетересней могла быть статья, если бы автор добавил побольше описания. Потому захотелось показать на примере этой статьи, как можно было бы сделать немного лучше, а заодно и поделиться своей реализацией его задачи.

Хочется заметить, что ничего плохо в адрес автора сказать не хочу (разьве только, что не добавил достаточно сопроводительного текста). Видно, что эта поделка была «выполнена с любовью»: потрачено немало часов на придумывание, реализацию и отладку. Получилось то, что получилось. Не очень просто, не очень быстро, но главное — работает.

Что мы можем улучшить?
Первое, что бросается в глаза, так это изображение для «лампочек». Я не имею ничего против — лампочки симпатичные. Но если картинка не подгрузилась (в какой-то момент или вообще), или изображения отключены (да, такое случается), то пользователь ничего не увидит. Не очень приятно. Поэтому всегда стоит подумать о том, как мы сможем выйти из ситуации в случае отсутствия изображений, должен быть некоторый запасной вариант.
К счастью, в данном случае такое решение просится само и заключается в изменении изображения:

(изображение увеличено в 16 раз, шашечками отмечены прозрачные и полупрозрачные области)
Пара минут в Photoshop и у нас новое изображение, полученное из исходного. Если до этого у нас был белый шарик на прозрачной подложке, то теперь квадрат заполненный фоновым цветом (цвет оставлен авторский, то есть #3d3d3d) с отверстием (с дыркой) в нем. До этого мы были привязаны к цвету лампочки (белый) и фон мог быть любым — теперь наоборот. Таким образом, мы можем использовать для лампочки любой цвет, но это не главное. Главный выигрыш в том, что при отсутствии изображения мы увидим белый (или другого цвета) квадрат, который будет сигнализировать, что лампочка включена. Когда станут доступны изображения, лампочка станет идеально круглой, как и было раньше. С помощью CSS прописываем:
background: white url(dot.png);

И получаем следующее:

(сверху то, что будет без картинок, снизу — с картинками)
Здесь еще пришлось добавить отступ в 1 пиксель между блоками (лампочками), чтобы они не сливались. Но можно и без этого.

Следующее улучшение, это корректировка статичного HTML. Видимо автор не очень еще понимает, что и когда лучше использовать: классы или ID, какие теги в какой ситуации. Хотя я и сам до сих пор не знаю :) Так же допускаю, что этот код откуда-то был «выдран», и у него просто тяжелая наследственность. Так или иначе, мне показалось что HTML можно немного упростить — выкинуть все лишнее, тем более он будет использоваться для демонстрации, то есть как пример.
Несколько мыслей. Использовать список для перечисления полей формы внутри <form> — несколько странно, куда уместнее использовать <div class=«field»>..</div>, например. Если вы используете <label>, то лучше поместить <input> в его содержимое, или использовать атрибут for. То есть вместо:
<label>E-TABLO HORIZ. POINTS:</label>
<input type="text" name="w" value="55" />


* This source code was highlighted with Source Code Highlighter.

куда лучше:
<label for="w">E-TABLO HORIZ. POINTS:</label>
<input type="text" name="w" value="55" />


* This source code was highlighted with Source Code Highlighter.

или, что я предпочитаю больше:
<label>
 <span class="title">E-TABLO HORIZ. POINTS:</span>
 <input type="text" name="w" value="55" />
</label>


* This source code was highlighted with Source Code Highlighter.

Тем более это актуально в той стратегии, по которой автор делал верстку: задавал для <label> display: block & float: left. Чтобы было проще манипулировать (добиваться визуальных эффектов, позиционирования) последний вариант подходит лучше всего.
Еще один практический совет не давайте короткие ID, классы, имена полям типа «w», а так же переменным с большой областью видимости, особенно если хотите дать разбираться в коде кому то еще (или тому, кто будет его дальше поддерживать). Это может плохо сказаться в будущем или на большом проекте (пусть даже он никогда не родится). Было бы намного понятнее, если вместо «w» было бы написано «tabloWidth», «tablo_width» или на худой конец просто «width».
Но это все не больше, чем еще одно мнение. В данном случае моё.

Отдельно хочется отметить стили * { outline: none; } и div { display: none !important; } — просто отличный «подарочек» для тех, кто захочет разобраться с тем как это все устроено в firebug. Мне сначала показалось, что у меня что-то не так с firebug — оказалось все намного проще. Если кто не понял о чем я, данные стили «отключают» подсветку блоков при наведении на них, когда вы хотите выбрать элемент на странице. Мне кажется, если вы выкладываете пример на хабр с такими стилями, то это просто издевательство над простыми смертными. Удивительно, что javascript без обфускации...

Ну а теперь непосредственно сам javascript и алгоритм.
Изучая код, я, признаться, запутался. Как-то все сложно, и реально слишком много кода. Есть догадки как это работает, но я могу ошибаться, так что не буду гадать и расскажу как бы это делал я (а вернее сделал).
Первым, что захотелось изменить, это таблицу символов.
Конечно, в обсуждении статьи всплывали альтернативные пути решения, когда можно обойтись без таблицы символов (в которой, по сути, закодирован шрифт). Суть метода использовать некоторую сетку (то есть изображение с дырочками), под которой «выводить» обычный текст обычным шрифтом, и затем двигать его. Так иногда делают в различных графических средах и играх. Но в этом случае зерно (размер дырочек) должен быть кране меленьким, в идеале 1 пиксель, в противном случае (как в нашем, с размером дырочки ~10 пикселей) легко получить ситуацию «непопадание в пиксели», то есть когда в нашей дырочке будет видна только часть буквы. И если даже получится на определенном браузере добиться четкого попадания в пиксели каким-то шрифтом, то добиться этого везде и всегда, имхо, невозможно.
Но вернемся к таблице. В ней для каждого символа с помощью массива из нулей и единиц закодировано изображение. На примере буквы «Т»:

Думаю все можно понять из картинки. Способ кодирования в принципе нормальный, так как удобно «рисовать». Но слишком избыточный и неудобно использовать.
Перекодируем символы таким образом, чтобы символ был закодирован не построчно, а вертикальными линиями. А нули и единицы уложим битами в число. Для это воспользуемся небольшим конвертором:
var letters = {
 // оригинальная таблица символов
};
// новая таблица
var letters2 = {};
// перебираем все символы
for (var letter in letters)
{
 // вычисляем количество линий-столбцов (в столбце 8 строк)
 var lineCount = letters[letter].length / 8;
 // массив содержащий столбцы (по сути перекодированный символ)
 var converted = [];
 // перебираем линии
 for (var i = 0; i < lineCount; i++)
 {
  var line = 0;
  // собираем "линию" в число
  for (var j = 0; j < 8; j++)
   // это проще записать чем объяснить... кто не поймет, считайте это магией :)
   // копать в сторону битовых операций: здесь побитовое "или" и битовый сдвиг
   line = line | letters[letter][j * lineCount + i] << j;

  converted.push(line);
 }
 // сохраняем результат
 letters2[letter] = converted;
}
// так можно вывести результат "в браузер"
for (var letter in letters2)
{
 document.write("<br>'" + letter + "': [" + letters2[letter] + "],");
}

* This source code was highlighted with Source Code Highlighter.


После такой конвертации наша буква «Т» в коде будет выглядеть вот так:
'T': [3, 3, 255, 255, 3, 3]

Что значительно компактнее (сама таблица уменьшилась в 3 раза), но это не самоцель. При таком раскладе стало сложнее кодировать новые символы (с другой стороны, можно кодировать как раньше, а потом пользоваться конвертером), но значительно удобнее использовать в коде (сейчас вы это увидите).

Теперь, когда мы сконвертировали таблицу символов, самое время перейти к основной части. Сначала надо определится с полотном (дальше я буду называть его конвейером, так как это название больше подходит по логике работы). Кажется, логично сразу сформировать из текста всю полосу, а не делать это на каждом шаге анимации — при анимации мы будем просто смещаться. При текущем положении дел сформировать такую полосу весьма просто и она будет представлять из себя линейный (одно измерение) массив, содержащий числа. Делается это следующей функцией:
   // конвейер полос
   var pipeline = [];

   // заполнение текстом
   function fill(str){
    // очищаем
    pipeline.length = 0;

    // приводим к верхнему регистру и добавляем в конце несколько пробелов,
    // чтобы при циклическом сдвиге длинный текст не склеивался
    str = (str + '     ').toUpperCase();

    // заполняем
    for (var i = 0; i < str.length; i++)
     // проверка, умеем ли мы выводить такую букву
     if (letters[str.charAt(i)])
     {
      // если да, то добавляем в конвейер столбцы этой буквы
      // можно и циклом, но так и быстрее и изящнее
      pipeline.push.apply(pipeline, letters[str.charAt(i)]);
      pipeline.push(0);
     }

    // добиваем пустые столбцы, чтобы вписаться в ширину табло (если текст короткий)
    while (pipeline.length <= tabloWidth)
     pipeline.push(0);
   }

* This source code was highlighted with Source Code Highlighter.


Вроде пока все просто. При вызове, скажем, fill('habrahabr') массив pipeline будет содержать:
[255, 255, 24, 24, 255, 255, 0, 252, 254, 51, 51, 254, 252, 0, 255, 255, 153, 153, 255, 118, 0, 255, 255, 27, 59, 255, 206, 0, 252, 254, 51, 51, 254, 252, 0, 255, 255, 24, 24, 255, 255, 0, 252, 254, 51, 51, 254, 252, 0, 255, 255, 153, 153, 255, 118, 0, 255, 255, 27, 59, 255, 206, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Прежде чем перейти к интерпретации этого массива, нам нужно сформировать HTML структуру табло. Так как оно может иметь разную ширину, для этой цели напишем функцию. Размеры табло задаются двумя переменными — tabloWidth и tabloHeight соответственно. В нашем случае tableHeight не меняется и всегда равно 8.
Автор статьи, по мотивам которой написана данная, использовал DOM методы для генерации структуры, что есть хорошо и за что ему можно пожать лапу. Однако он просто добавлял tabloWidth x tabloHeight элементов в контейнер, а на строки они разбивались за счет того, что контейнер ограничен по ширине. Для этого он высчитывал и выставлял контейнеру новую ширину (в пикселях) при изменении tabloWidth. Это не очень удобно, так как размер лампочки получается «зашит» в коде, и при изменении CSS (размеров лампочки) нам придется править код.
Я поступил иначе. В контейнер я помещаю <div> элементы, количество которых равно числу строк, а в каждый из них уже необходимое количество <span> (лампочки). Это удобнее как для верстки, так и для дальнейших манипуляций (анимации). Так же, в данном случае, мы можем создать всего одну строку и потом ее клонировать tableHeight — 1 раз, что получится значительно быстрее (хотя выигрыш, конечно, тут сомнительный). Еще по умолчанию не ставим <span> элементам никаких классов (это соответствует тому, что лампочка включена), а позже будем добавлять класс off для выключенных лампочек. Итак, текст функции:
   // создание HTML структуры
   function createTablo(){
    // чистим контейнер
    while (htmlTablo.lastChild)
     htmlTablo.removeChild(htmlTablo.lastChild);

    // создаем прототип строки
    var lineProto = document.createElement('DIV');
    for (var i = 0; i < tabloWidth; i++)
     lineProto.appendChild(document.createElement('SPAN'));

    // заполняем табло строками
    htmlTablo.appendChild(lineProto); // вставляем оригинал
    for (var i = 0; i < tabloHeight - 1; i++)
     // добавляем клонированные строки
     htmlTablo.appendChild(lineProto.cloneNode(true));
   }

* This source code was highlighted with Source Code Highlighter.

На выходе в контейнере, на который ссылается htmlTablo, будет примерно такой HTML:
<div><span></span><span></span><span></span>...</div>
<div><span></span><span></span><span></span>...</div>
...
<div><span></span><span></span><span></span>...</div>


* This source code was highlighted with Source Code Highlighter.


Теперь все готово для того, чтобы, наконец, заняться анимацией, а вернее шагом анимации, то есть сдвигом. Следующая функция как раз и выполняет эту задачу, а именно зажигает и гасит лампочки, и сдвигает все на одну колонку влево. Переменная cursor указывает на ту линию (колонку), которая будет в данный момент выводится справа. После того как линия «выведена», курсор увеличивается на единицу (смещается к следующему элементу-линии), а по достижении конца массива (pipeline, где хранятся все линии) переходит снова в начало (становится равным нулю). То есть анимация получается зацикленной. Сам код:
     // сдвиг
     function step(){
        // перебираем все строки
        for (var i = 0, line = htmlTablo.firstChild; line; i++, line = line.nextSibling)
        {
         // берем первую точку
         var dot = line.firstChild;

         // переносим в конец
         line.appendChild(dot);

         // задаем новое состояние
         // это снова магия :) сдвиг вправо и побитовое "и"
         // если единица убираем класс (делаем пустым), если ноль - задаем класс off
         dot.className = (pipeline[cursor] >> i) & 1 ? '' : 'off';

         // если хотим в обратную сторону:
         // var dot = line.lastChild;
         // line.insertBefore(dot, line.firstChild);
         // dot.className = (pipeline[pipeline.length - cursor - 1] >> i) & 1 ? '' : 'off';
        }
        // меняем позицию "курсора"
        cursor = (cursor + 1) % pipeline.length;
     }

* This source code was highlighted with Source Code Highlighter.

Немного о самом методе смещения. Мы не перекрашиваем все лампочки, а просто берем первую колонку и ставим ее в конец. Для этого используем DOM. Затем перекрашиваем лампочки (меняем класс у <span>) последней колонки (которая до этого была первой) в соответствии с очередным значением в массиве pipeline. Это значительно дешевле, чем поменять класс у всех <span>: вместо изменения класса для tabloWidth x tabloHeight элементов, мы меняем только для tabloHeight.
В комментариях так же приведен код для сдвига вправо (только этого может оказаться недостаточно, может потребоваться поменять что-то и в других местах).

Вот и все. Осталась только инициализация: нам нужно создать табло (функция createTablo), заполнить конвейер текстом (функция fill), выполнить tableWidth раз шаг-сдвиг (то есть функцию step) для того, чтобы очистить табло и заполнить первоначальным состоянием, и задать с помощью setInterval вызов функции step с требуемым интервалом. Текст функции, которая все это проделывает можно посмотреть в рабочем примере, название оставлено авторское — ENJOY, дабы не портить праздник жизни :)
Посмотреть результат

Чего мы добились:
* Более простой и короткий код;
* Вроде работает быстрее;
* Полный контроль на стилем через CSS, то есть никакие размеры не «зашиты» в коде; к тому же можно менять цвет лампочек, например, для потушенных лампочек я задал цвет немного темнее фона, что добавило большей реалистичности, и погашенные лампочки так же выделяются на фоне; вы можете поэкспериментировать и сделать, например, радугу на табло;
* Не привязаны к изображениям, пользователь сможет что-то увидеть и без них;
* Показали, объяснили как это работает :)
Да и код получился в три раза меньше оригинального (если пропустить оба исходника, например, через packer, который уберет пробелы/комментарии, и сравнить). Насколько проще — судить вам.
Конечно, сценарий анимации немного отличается от оригинального, но, полагаю, при желании вы сами сможете добиться требуемого результата — основа у вас есть.

PS
Хотелось бы видеть статьи примерно такой формы. Решил не просто сказать, но и сделать. Может мой стиль изложения не самый лучший и кое где слишком подробно. Старался, чтобы данная статья оказалась полезна, а начинающим было проще разобраться. Написание рабочего примера потребовало гораздо меньше времени, чем упрощение кода, написание комментариев и данной статьи (которая мучалась три дня из-за нехватки времени). Надеюсь кому то пригодится.
Ждем хороших статей на хабре.

ENJOY!

UPDATE: Некоторые, дочитав до конца, забывают (или не обращают внимания), что я не являюсь автором оригинальной идеи, автор которой regeda, о чем он и написал в своей статье Электронное табло. Я всего лишь автор еще одной реализации и данной статьи.
Так же эта статья предназначена лишь для демонстрации подходов и идей, а само табло лишь пример. Помните, как в Матрице: Я могу лишь показать дверь, но ты должен сам войти в нее"…
Tags:
Hubs:
Total votes 84: ↑70 and ↓14+56
Comments58

Articles