Здравствуйте, Хабражители!
В этом топике я покажу как создать простой графический редактор для iPhone. Статья написана максимально понятно, поэтому даже новичку не будет сложности разобраться. Более того, я расскажу:
Больше — под катом
Я думаю, что каждый когда-либо интересовался и разработкой под iPhone, и технологией HTML5 Canvas, но по разным причинам забросывал. В этой статье я хочу показать, что сделать такое приложение очень легко. Для прозрачности кода, я не стал использовать сторонние библиотеки и фреймворки.
Итак, начнем.
Сначала думал начать с html кода, но понял, что для полного понимания, что происходит, сперва нужно предоставить стили.
Для сохранения адекватного размера топика, я решил не публиковать весь файл, а только интересные его части.
Тег meta с атрибутом viewport указывает мобильному браузеру, какая ширина будет у страницы (width), высота (height), разрешить ли пользователю менять зум на данной странице (значение user-scalable), с каким начальным зумом загружать страницу (initial-scale), какой может быть минимальный (minimum-scale) и максимальный (maximum-scale) зум. Ширина и высота текущего девайса хранится в значениях device-width/device-height.
Я долго думал над тем, как сделать выбор цвета, и самое простое, что пришло в голову, создать элемент select, с нужными значениями цвета и размеров в тегах option и по нажатию на кнопку «Цвет» или «Размер» просто слать фокус элементу select. Известно, тег select при фокусе на айфоне работает примерно так.
Фокус (метод .focus()), к сожалению, слаться не хотел. Но и я не сдавался! И вот что подумал: а что если сделать прозрачный див, в котором будут находится нужные мне элементы select. А див этот, в свою очередь, наложить на мои кнопки?! И что Вы думаете? Все работает! Для наглядности, показываю как:
Привожу код хака:
(эта штука называется clickjacking, если прозрачный фрейм с другого сайта, например)
Перед тем, как загрузить читателя сотней строк кода, я хочу рассказать о том, как все должно работать и почему.
ontouchstart — срабатывает, когда пользователь только начал движение пальцем по экрану (можно сравнить с onmousedown на компьютере).
ontouchmove — срабатывает, когда пользователь ведет пальцем по экрану (можно сравнить с onmousemove).
ontouchend — срабатывает, когда пользователь отпустил палец от экрана (можно сравнить с onmouseup).
Так-же есть ongesturechange, ongestureend из Gesture API, но они в данной статье рассматриваться не будут.
Каждое событие возвращает массив touches, каждый элемент которого содержит такие свойства как:
pageX, pageY, clientX, clientY. Количество элементов в массиве touches зависит от количества пальцев, которые касаются экрана.
Поэтому отследить касание экрана и получить координаты можно очень просто:
Как все будет работать? По пунктам
Почему всё так сложно?
Дело в том, что в функции отрисовки используются кривые Безье, т.е. кривая(чаще — ей конец) может измениться, если следующая контрольная координата будет в другой стороне. Если же использовать обычные прямые линии то при быстрых круговых движениях можно будет наблюдать не красивые углы.
Как я уже сказал, после каждого «штриха» изображение с основного холста клонируется на холст-помощник. Это делается для того, чтобы не забивать массив координат для основного холста. Так как чем больше контрольных точек в массиве — тем медленнее будет перерисовываться наш рисунок. Из этих соображений я ограничил буфер перерисовки до 1 штриха (если рисовать один большой штрих, то скорость тоже может снизиться).
Демо
Исходный код
P.S. Если у Вас возникли какие-нибудь вопросы — задавайте! Постараюсь ответить.
В этом топике я покажу как создать простой графический редактор для iPhone. Статья написана максимально понятно, поэтому даже новичку не будет сложности разобраться. Более того, я расскажу:
- об особенностях событий touch-устройств;
- об особенностях верстки для мобильных девайсов;
- почему для создания нормальной «рисовалки» нужно использовать несколько холстов;
- что такое clickjacking и зачем я использовал этот хак в своей рисовалке;
- о всех трудностях и некоторых мелочах, с которыми я столкнулся в процессе разработки;
Больше — под катом
Вступление
Я думаю, что каждый когда-либо интересовался и разработкой под iPhone, и технологией HTML5 Canvas, но по разным причинам забросывал. В этой статье я хочу показать, что сделать такое приложение очень легко. Для прозрачности кода, я не стал использовать сторонние библиотеки и фреймворки.
Итак, начнем.
CSS-стили
Сначала думал начать с html кода, но понял, что для полного понимания, что происходит, сперва нужно предоставить стили.
body {
margin: 0;
padding: 0;
background: #fff;
}
/* Верхняя панель, на которой расположены кнопки */
.tb {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 40px;
overflow: hidden;
border-bottom: 1px solid #CCC;
background-color: orange;
}
canvas {
position: absolute;
top: 40px;
left: 0;
}
/* Стили кнопки */
.bt {
overflow: hidden;
float: left;
font-weight: bold;
color: white;
font: 16px Arial;
width: 33%;
padding-top: 10px;
height: 30px;
text-align: center;
}
/* Стили элемента select */
select {
float: left;
width: 33%;
margin-top: 10px;
height: 20px;
}
HTML-файл
Для сохранения адекватного размера топика, я решил не публиковать весь файл, а только интересные его части.
Meta viewport
...
<head>
<meta name="viewport" content="width=device-width,user-scalable=no" />
</head>
...
Тег meta с атрибутом viewport указывает мобильному браузеру, какая ширина будет у страницы (width), высота (height), разрешить ли пользователю менять зум на данной странице (значение user-scalable), с каким начальным зумом загружать страницу (initial-scale), какой может быть минимальный (minimum-scale) и максимальный (maximum-scale) зум. Ширина и высота текущего девайса хранится в значениях device-width/device-height.
Делаем выбор цвета / размера кисти
Я долго думал над тем, как сделать выбор цвета, и самое простое, что пришло в голову, создать элемент select, с нужными значениями цвета и размеров в тегах option и по нажатию на кнопку «Цвет» или «Размер» просто слать фокус элементу select. Известно, тег select при фокусе на айфоне работает примерно так.
Фокус (метод .focus()), к сожалению, слаться не хотел. Но и я не сдавался! И вот что подумал: а что если сделать прозрачный див, в котором будут находится нужные мне элементы select. А див этот, в свою очередь, наложить на мои кнопки?! И что Вы думаете? Все работает! Для наглядности, показываю как:
Привожу код хака:
<!-- то, что видит пользователь -->
<div id="tb" style="z-index: 2">
<div class="bt">Цвет</div>
<div class="bt">Толщина</div>
<a class="bt">Сохранить</a>
</div>
<!-- то, что видит браузер -->
<div id="htb" style="opacity: 0; z-index: 3">
<select id="hcs">
<option>blue</option>
<option>red</option>
<option>green</option>
<!-- Урезано для сокращения размеров файла -->
</select>
<select id="hss">
<option>10</option>
<option>11</option>
<option>12</option>
<!-- Урезано для сокращения размеров файла -->
</select>
<a class="bt" id="savebutton" style="z-index: 20;"></a>
</div>
(эта штука называется clickjacking, если прозрачный фрейм с другого сайта, например)
Самое интересное
Перед тем, как загрузить читателя сотней строк кода, я хочу рассказать о том, как все должно работать и почему.
События touch-устройств
ontouchstart — срабатывает, когда пользователь только начал движение пальцем по экрану (можно сравнить с onmousedown на компьютере).
ontouchmove — срабатывает, когда пользователь ведет пальцем по экрану (можно сравнить с onmousemove).
ontouchend — срабатывает, когда пользователь отпустил палец от экрана (можно сравнить с onmouseup).
Так-же есть ongesturechange, ongestureend из Gesture API, но они в данной статье рассматриваться не будут.
Каждое событие возвращает массив touches, каждый элемент которого содержит такие свойства как:
pageX, pageY, clientX, clientY. Количество элементов в массиве touches зависит от количества пальцев, которые касаются экрана.
Поэтому отследить касание экрана и получить координаты можно очень просто:
someElement.ontouchstart = function(e) {
console.log("X: " + e.touches[0].pageX + ", Y:" + e.touches[0].pageY);
}
Алгоритм
Как все будет работать? По пунктам
- При срабатывании события ontouchmove / ontouchstart холст очищается, в массив, который состоит из координат движений пальцев записывается ещё одно значение и все заного перерисовывается.
- При срабатывании события ontouchend все, что нарисовано на основном холсте сохраняется в изображение, и это изображение копируется на прозрачный холст-помощник, который лежит под основным холстом. После этого, основной холст очищается, и ждет нового ontouchmove/ontouchstart
Почему всё так сложно?
Дело в том, что в функции отрисовки используются кривые Безье, т.е. кривая(чаще — ей конец) может измениться, если следующая контрольная координата будет в другой стороне. Если же использовать обычные прямые линии то при быстрых круговых движениях можно будет наблюдать не красивые углы.
Зачем все перерисовывать на второй канвас
Как я уже сказал, после каждого «штриха» изображение с основного холста клонируется на холст-помощник. Это делается для того, чтобы не забивать массив координат для основного холста. Так как чем больше контрольных точек в массиве — тем медленнее будет перерисовываться наш рисунок. Из этих соображений я ограничил буфер перерисовки до 1 штриха (если рисовать один большой штрих, то скорость тоже может снизиться).
Код
Код создания холстов и получения контекстов
var width = window.innerWidth; //ширина телефона
var height = window.innerHeight; //высота телефона
var hcanv = document.createElement("canvas"); // создаем канвас рас
var mcanv = document.createElement("canvas"); // создаем канвас два
//выставляем канвасам размеры
mcanv.width = width;
mcanv.height = height;
hcanv.width = width;
hcanv.height = height;
//добавляем канвасы в DOM
document.body.appendChild(mcanv);
document.body.appendChild(hcanv);
hcanv.style.zIndex = 10; //делаем один канвас выше другого
//получаем контексты
var mctx = mcanv.getContext("2d");
var hctx = hcanv.getContext("2d");
Обработка выбора цвета и размера кисти
var selects = document.getElementsByTagName("select"); //получаем все элементы select
for(var i=0; i<selects.length; i++) { //пробегаем циклом
selects[i].onchange = handleSelects; // и вешаем события на выбор значения
}
function handleSelects() { //функция обработки выбранного значения
var val = this.options[this.selectedIndex].value;
switch(this.id) { //проверяем что это, выбор цвета или размера
case "hcs":
brush.color = val; // выставляем значение
break;
case "hss":
brush.size = val; // выставляем значение
break;
}
}
Код рисования
var touches = {x:[], y:[]}; // массивы координат
var brush = {color: "blue", size: 10}; //текущие настройки кисти
var snapshot = ""; //тут будет храниться изображение
hcanv.ontouchstart = function(e) { //пользователь коснулся экрана
//добавляем координаты в массивы
touches.x.push(e.touches[0].pageX);
touches.y.push(e.touches[0].pageY-40); //40 пикселей - высота верхней панели
hctx.clearRect(0, 0, width, height); //очищаем канвас
redraw(hctx); // рисуем (точечки)
return false; // отменяем действие браузера по-умолчанию
}
hcanv.ontouchmove = function(e) { //пользователь повел пальцем
//добавляем координаты в массивы
touches.x.push(e.touches[0].pageX);
touches.y.push(e.touches[0].pageY-40);
hctx.clearRect(0, 0, width, height); //очищаем канвас
redraw(hctx); //рисуем (линии)
return false; // отменяем действие браузера по-умолчанию (скроллинг etc)
}
hcanv.ontouchend = function(e) { //пользователь оторвал палец от экрана
snapshot = hcanv.toDataURL(); // получаем URL представление канваса в png
var img = new Image(); // создаем новую картинку
img.src = snapshot; // назначаем ей url
img.onload = function() { //когда она загрузится (а на айфоне это не моментально)
mctx.drawImage(img, 0, 0); //рисуем картинку на втором канвасе
hctx.clearRect(0, 0, width, height); //очищаем первый канвас
}
//очищаем массивы
touches.x = [];
touches.y = [];
}
//функция перерисовки
function redraw(ctx) {
ctx.lineCap = "round"; //вид конца линии
ctx.lineJoin = "round"; //вид излома
ctx.strokeStyle = brush.color; //цвет линии
ctx.lineWidth = brush.size; // размер линии
ctx.beginPath();
if(touches.x.length < 2) { //проверяем, не нарисовал ли пользователь точку
ctx.moveTo(touches.x[0], touches.y[0]);
ctx.lineTo(touches.x[0] + 0.51, touches.y[0]);
ctx.stroke();
ctx.closePath();
return;
}
ctx.moveTo(touches.x[0], touches.y[0]);
ctx.lineTo((touches.x[0] + touches.x[1]) * 0.5, (touches.y[0] + touches.y[1]) * 0.5);
var i = 0;
while(++i < (touches.x.length -1)) {
var abs1 = Math.abs(touches.x[i-1] - touches.x[i]) + Math.abs(touches.y[i-1] - touches.y[i])
+ Math.abs(touches.x[i] - touches.x[i+1]) + Math.abs(touches.y[i] - touches.y[i+1]);
var abs2 = Math.abs(touches.x[i-1] - touches.x[i+1]) + Math.abs(touches.y[i-1] - touches.y[i+1]);
if(abs1 > 10 && abs2 > abs1 * 0.8) { //проверяем, нужно ли рисовать кривую Безье
ctx.quadraticCurveTo(touches.x[i], touches.y[i], (touches.x[i] + touches.x[i+1]) * 0.5, (touches.y[i] + touches.y[i+1]) * 0.5);
continue;
}
ctx.lineTo(touches.x[i], touches.y[i]);
ctx.lineTo((touches.x[i] + touches.x[i+1]) * 0.5, (touches.y[i] + touches.y[i+1]) * 0.5);
}
ctx.lineTo(touches.x[touches.x.length-1], touches.y[touches.y.length-1]);
ctx.moveTo(touches.x[touches.x.length-1], touches.y[touches.y.length-1]);
ctx.stroke();
ctx.closePath();
}
Демо
Исходный код
P.S. Если у Вас возникли какие-нибудь вопросы — задавайте! Постараюсь ответить.