Блог от AS3Coder'a о JavaScript, HTML, CSS... и немного о Flash.

пятница, 29 августа 2014 г.

Emoji на вашем сайте

Что такое Emoji?

Emoji - это способ передачи электронных сообщений, состоящих не из букв, а из значков и смайликов. Придумали такой способ общения в Японии и вскоре он получил популярность по всему миру. С подробной историей появления и развития вы можете ознакомиться здесь.

Как это выглядит?

Вот так:



Изображения вставляются прямо в текст. Каждая иконка имеет свой юникод, такой же как и обычный текстовый символ. Все иконки делятся на несколько семантических групп:
  1. Эмоции
  2. Декорации
  3. Транспорт и навигация
  4. Закрытые (внутри геометрической фигуры)
  5. Несортированные
  6. Дополнительные
У каждой группы свой диапазон юникодов. Более подробно можно ознакомиться по ссылке.

В чем заключается трудность

Дело в том, что на сайтах обычно используется кодировка UTF-8. И отображаться на таком сайте наш пример будет так:



А всё потому, что в кодировке UTF-8 нет описания для этих символов. Большинство имеют код из неиспользуемого диапазона начиная с 4 байт.

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

Вот наш пример в InternetExplorer 11 (Windows 8.1):



В FireFox 31:



В Mobile Safari (iOS):



Но в Chrome мы увидим всё тот же набор квадратиков, который называется шумом:



Отсюда выделим следующие проблемы:
  • Отображение шума вместо иконок в некоторых браузерах
  • Не одинаковый вид на различных устройствах

Что делать?

А делать мы будем ровным счетов тоже самое, что делают остальные браузеры: искать символы в тексте и подменять их на свои изображения c помощью JavaScript.

Поиск и замена символов на иконки

Подготовка

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

Я нашел такое изображение в проекте модулей для node.js:



Загрузить файл можно по ссылке: https://raw.githubusercontent.com/node-modules/emoji/master/lib/emoji.png

Там же я нашел и описание для этой сетки иконок в CSS-файле:
... .emoji { background: url("emoji.png") top left no-repeat; width:20px; height: 20px; } .emoji2600 { background-position: -500px -120px; } .emoji2601 { background-position: -500px -140px; } .emoji2614 { background-position: -500px -200px; } .emoji26c4 { background-position: -520px -200px; } .emoji26a1 { background-position: -520px -100px; } .emoji1f300 { background-position: -20px -500px; } .emoji1f301 { background-position: -20px -520px; } .emoji1f302 { background-position: -20px -540px; } .emoji1f303 { background-position: -20px -560px; } .emoji1f304 { background-position: -20px -580px; } ...
Файл доступен по адресу: https://raw.githubusercontent.com/node-modules/emoji/master/lib/emoji.css

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

Алгоритм будет следующий:
  1. Ищем в тексте символы с юникодом в известных нам диапазонах.
  2. Заменяем найденные символы на текстовые элементы span, для которых указываем CSS-класс с HEX-кодом.

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

Анализ

Проанализировав таблицу символов, я поделил их на несколько групп по диапазону кодов:

Группа Размер символа Коды
1. Флаги 8 байт Представлены в виде 2-х суррогатных пар символов в диапазоне: \ud83c\udde8-ud83c\uddfa (1-я пара) + \ud83c\udde7-\ud83c\uddfa (2-я пара)
2. Числа 4 байта Представлены в виде двух 2 байтных символов идущих подряд с диапазоном \u0023-\u0039 (1-й символ) и \u20E3 (2 символ)
3. Несортированные 3 байта Символы с кодами от \u2139 до \u3299
4. Пунктуация 3 байта Два символа с кодами \u203C и \u2049
5. Все остальные 4 байта Представлены в виде суррогатной пары в диапазоне \ud800-\udbff (1-я часть пары) + \udc00-\udfff (2-я часть пары)

Суррогатные пары

Что это такое?

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

В нашем случае большинство emoji-иконок описываются кодами превышающими формат UTF-8. Поэтому мы использовали суррогатные пары в приведенных выше диапазонах. Для этого использовался такой вот метод:
function GetHexFromSurrogatePair (a, b) { return((a - 0xD800) * 0x400 + (b - 0xDC00) + 0x10000); }
Но, так как мы будем искать по юникоду, нам нужен обратный метод:
function GetSurrogatePairFromHex (hex) { var a = Math.floor((hex - 0x10000) / 0x400) + 0xD800, b = (hex - 0x10000) % 0x400 + 0xDC00; return(['\\u' + a.toString(16), '\\u' + b.toString(16)]); }
Код

Теперь попробуем написать код для поиска и замены для каждой из этих групп.

1. Флаги:
string = string.replace(/(\ud83c[\udde8-\uddfa])(\ud83c[\udde7-\uddfa])/g, function (match) { var hex = [ GetHexFromSurrogatePair(match.charCodeAt(0), match.charCodeAt(1)).toString(16), GetHexFromSurrogatePair(match.charCodeAt(2), match.charCodeAt(3)).toString(16) ].join(''); //--- return(['<span class="emoji emoji', hex, '"></span>'].join('')); });
2. Числа:
string = string.replace(/(\ud83c[\udde8-\uddfa])(\ud83c[\udde7-\uddfa])/g, function (match) { var hex = [ GetHexFromSurrogatePair(match.charCodeAt(0), match.charCodeAt(1)).toString(16), GetHexFromSurrogatePair(match.charCodeAt(2), match.charCodeAt(3)).toString(16) ].join(''); //--- return(['<span class="emoji emoji', hex, '"></span>'].join('')); });
3. Несортированые:
string = string.replace(/[\u0023-\u0039]\u20E3/g, function (match) { var hex = [ match.charCodeAt(0).toString(16), match.charCodeAt(1).toString(16) ].join(''); //--- return(['<span class="emoji emoji', hex, '"></span>'].join('')); });
4. Пунктуация:
string = string.replace(/[\u2139-\u3299]/g, function (match) { var hex = match.charCodeAt(0).toString(16); //--- return(['<span class="emoji emoji', hex, '"></span>'].join('')); });
5. Все остальные:
string = string.replace(/[\u203C\u2049]/g, function (match) { var hex = match.charCodeAt(0).toString(16); //--- return(['<span class="emoji emoji', hex, '"></span>'].join('')); });
Теперь осталось собрать и причесать весь код в один итоговый класс, который будет выполнять замену всех групп по очереди:
/** * Emoji class * @description http://as3coder.blogspot.com/2014/08/emoji.html * @author AS3Coder */ (function(){ /** * Define public access * @private */ var emoji = window.emoji = {}; emoji.replace = Replace; /** * Grouping by range * @constant * @private */ var GROUPS = [ [/(\ud83c[\udde8-\uddfa])(\ud83c[\udde7-\uddfa])/g, ReplaceFlags], // Flags [/[\u0023-\u0039]\u20E3/g, ReplaceNumbers], // Numbers [/[\u2139-\u3299]/g, ReplaceStandard], // Unsorted [/[\u203C\u2049]/g, ReplaceStandard], // Punctuation [/([\ud800-\udbff])([\udc00-\udfff])/g, ReplaceSurrogate] // Other (surrogate pairs) ]; /** * Method to replace all emoji characters in the icon * @param {String} Source string * @return {String} * @public */ function Replace (source) { var pattern; //--- for(var i=0, j=GROUPS.length; i<j; i++) { pattern = GROUPS[i]; if(pattern && pattern[0] && pattern[1]) { if(source.match(pattern[0])) { source = source.replace(pattern[0], pattern[1]); } } } //--- return(source); } /** * Method to replace flags * @return {String} * @private */ function ReplaceFlags (match) { return(GetHtmlCodeFromHex( [ GetHexFromSurrogatePair(match.charCodeAt(0), match.charCodeAt(1)).toString(16), GetHexFromSurrogatePair(match.charCodeAt(2), match.charCodeAt(3)).toString(16) ].join(''))); } /** * Method to replace numbers * @return {String} * @private */ function ReplaceNumbers (match) { return(GetHtmlCodeFromHex(match.charCodeAt(0).toString(16) + match.charCodeAt(1).toString(16))); } /** * Method to replace srandard charters * @return {String} * @private */ function ReplaceStandard (match) { return(GetHtmlCodeFromHex(match.charCodeAt(0).toString(16))); } /** * Method to replace surrogate pairs * @return {String} * @private */ function ReplaceSurrogate (match, p1, p2) { return(GetHtmlCodeFromHex(GetHexFromSurrogatePair(p1.charCodeAt(0),p2.charCodeAt(0)).toString(16))); } /** * The method returns the hex code for a surrogate pair * @return {String} * @private */ function GetHexFromSurrogatePair (a, b) { return((a - 0xD800) * 0x400 + (b - 0xDC00) + 0x10000); } /** * The method returns an html code for icon image * @param {String} hex * @return {String} * @private */ function GetHtmlCodeFromHex (hex) { return(['<span class="emojic"><span class="emoji emoji', hex, '"></span><span class="emojit">&#x', hex, ';</span></span>'].join('')); } //--- })();
Пример. Нужно смотреть в разных браузерах

Original text: After replacing:

Исходный код примера (163 кб)

Смотрите также:

четверг, 7 августа 2014 г.

Сохранить изображение с камеры на JavaScript

Я уже писал, о том как сохранить изображение с камеры на ActionScript 3.0. Теперь самое время разобраться как сделать то же самое на JavaScript.

Вывод изображения с камеры

Для вывода изображения с камеры нам понадобится video-элемент. В нем мы будем отображать потоковое видео с веб-камеры. На странице выглядеть это будет так:
<video autoplay id="video"></video>
Также нам понадобиться метод navigator.getUserMedia. С помощью этого метода мы можем попросить браузер вернуть нам медиаданные пользователя, а именно: изображение с камеры и звук с микрофона. Метод довольно новый и реализован еще не во всех обозревателях (по данным caniuse.com в Chrome 21+, FireFox 17+ и Opera 18+), поэтому добавим такую вот обвязку:
navigator.getUserMedia = (navigator.getUserMedia ||
                          navigator.webkitGetUserMedia ||
                          navigator.mozGetUserMedia ||
                          navigator.msGetUserMedia);
Теперь попробуем вызвать метод:
navigator.getUserMedia({ video : true }, function (){}, function(){});
В первом параметре мы указываем тип необходимых нам данных. Допустимо два параметра audio и video. В нашем случае это только video. После вызова пользователь увидит сообщение с запросом на разрешение использовать указанные устройства:

Снимок в Opera 23.0

Для реакции на выбор пользователя имеются второй и третий параметры. В них указываются ссылки на обработчики событий. Второй вызывается, если доступ к камере разрешен. Третий, если доступ запрещен.
navigator.getUserMedia({ video : true }, 
//---
function ()
{
  // Доступ к камере разрешен
}, 
function()
{
  // Доступ к камере запрещен
});
Если доступ к камере имеется, то в соответствующем обработчике будет единственный аргумент - ссылка на потоковое видео с камеры. Чтобы отобразить видео, его нужно задать ранее созданному video-элементу c помощью URL.createObjectURL:
navigator.getUserMedia({ video : true }, 
//---
function ()
{
  var video = document.getElementById('video');
  video.src = window.URL.createObjectURL(stream);
}, 
function()
{
  alert('No access to the camera!')
});
В старой Opera (которая Presto) формат параметров и ссылка на локальный медиапоток немного отличается. В рамках данной статьи, мы этого рассматривать не будем.
После того как страница получает видео с камеры, на вкладке появляется индикатор записи. Например в Chrome это выглядит так:



Снимок с экрана

Для того, чтобы сделать снимок с video-элемента, нам понадобится canvas-элемент.
<canvas id="canvas"></canvas>
Только этот элемент может снимать растровые изображения с любого элемента на странице. Для этого у двухмерного контекста имеется метод .drawImage(); В нашем случае мы будем снимать изображение с video-элемента, в котором отображается видео с камеры.
var context = canvas.getContext('2d');
context.drawImage(video, 0, 0);
Сборка файла изображения

Теперь изображение можно открыть в отдельном окне, используя адрес полученный из метода .toDataURL(); Здесь вы можете получить изображения в необходимом вам формате. Для этого достаточно указать в первом параметре MIME-тип файла. Мы будем использовать PNG-формат.
canvas.toDataURL('image/png');
Открыв файл в новом окне, можно заметить, что изображение описывается прямо в адресной строке закодированное в системе Base64.



Для работы с Base64 в JavaScript имеются методы .atob(); и .btoa(); Для декодирования строки Base64 в бинарные данные представленные в UTF-8 мы будем использовать .atob();
var utf8 = atob(canvas.toDataURL('image/png').split(',')[1]);
Получив бинарные данные файла, мы должны оформить их в файл. Для работы с файлами в JavaScript имеется объект Blob. Чтобы записать данные в файл, по-символьно пройдемся по строке в UTF-8 и соберем в Uint8Array. В итоге получим вот такой метод:
function base64toblob (base64)
{
  var utf8 = atob(base64),
  array = [];
  //---
  for(var i = 0, j = utf8.length; i < j; i++)
    array.push(utf8.charCodeAt(i));
  //---
  return(new Blob([new Uint8Array(array)], {type: 'image/png'}));
}
Сохранение файла

Теперь о том, как сохранить файл на локальный диск пользователя. Мы воспользуемся готовым решением от Eli Grey, за которое ему огромное спасибо. Речь идет о реализации заявленного FileSaver для большинства современных браузеров. Итак, скачиваем FileSaver.js и подключаем к нашей странице. Скрипт распространяется с лицензией X11/MIT.
<script type="text/javascript" src="filesaver.js"></script>
Нам необходимо только вызвать единственный метод saveAs(); в который передаем ссылку на файл и его название:
saveAs(base64toblob(canvas.toDataURL('image/png').split(',')[1]), 'image_from_camera.png');
Готовый пример и исходный код под катом.

Поиск по блогу

Обо мне



Farid Shamsutdinov (AS3Coder)
Russia, Tatarstan, Kazan
as3coder@gmail.com

Подробнее...

Постоянные читатели

© 2014 Farid Shamsutdinov. При копировании материалов, ссылка на источник обязательна. Технологии Blogger.