Здравствуйте, Хабражители!
В этом топике я покажу как создать простой графический редактор для 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. Если у Вас возникли какие-нибудь вопросы — ��адавайте! Постараюсь ответить.