Canvas
17 May 2011

[Графический редактор на Canvas] Кисть для скетчей

Продолжаем неделю canvas на хабре.
Графические редакторы на флеше/сильверлайте в интернете не редкость. На canvas их намного меньше, но есть и довольно интересные. Вот пара примеров:В этом цикле статей я планирую рассказать о различных моментах, с которыми можно столкнуться при создании графического редактора на canvas. Тривиальные части затрагивать не буду, постараюсь описывать только самое интересное. В этой статье опишу примерный алгоритм создания кисти для скетчей

Итак, с чего начнем? Начнем с создания простого файлика с canvas внутри. Я постараюсь писать как можно компактнее без лишних фреймворков и прочего, чтобы сам алгоритм был как можно прозрачнее. Вот чистый файл в котором мы будем создавать кисть для скетчей:
<!DOCTYPE html>
<html>
 <head>
  <title></title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <style type="text/css">
    body {
      margin: 0;
    }
    #cnvs {
      border: #000000 1px solid;
    }
  </style>
 </head>
 <body>
 <canvas id="cnvs" width="800" height="500"></canvas>
 </body>
</html>


* This source code was highlighted with Source Code Highlighter.

Канва намеренно расположена в левом верхнем углу для того, чтобы координаты курсора на экране совпадали с координатами пикселей на канве (погрешностью в 1 пиксель бордера пренебрегаем для чистоты кода, но знаем и помним о ней).
Для создания механизма рисования нам нужны несколько обработчиков стандартных событий мышки (onmousedown, onmouseup,onmousemove). Создадим для каждого функцию.
function mDown(e){
};
function mUp(e){
};
function mMove(e){
};


* This source code was highlighted with Source Code Highlighter.

Еще нам нужны несколько переменных: в action мы будем хранить текущее состояние левой кнопки мыши (нажата или нет), в ctx — 2d контекст для рисования на канве, в points — массив для хранения точек, в pointer — указатель на один из элементов этого массива.
Итак, в чем же заключается алгоритм? Понаблюдав как рисует скетчи мой приятель, я примерно представил себе механизм работы будущей кисти. Основные моменты которые нужно учитывать для создания такой кисти: скетчи рисуются штрихами, кисть должна быть слабоинтенсивной (мой приятель слабо давил на карандаш, но прорисовывал одно место несколько раз, поэтому линия получается мохнатой с различными хвостиками туда-сюда), на изгибах линия становится шире.
Моя версия алгоритма такова:
  • вслед за мышкой рисуем обычную линию, как если бы это был карандаш
  • кроме того соединяем текущую точку с какой-нибудь давно нарисованной(это придаст толщины на изгибах)
  • все это должно быть с небольшим элементом случайности (мохнатость)
  • кисть будет частично прозрачной (слабая интенсивность)

Первый и второй пункт для наглядности изображены на рисунке:

Итак, в массиве points храним координаты последних 10 точек (я выбрал число 10, вы можете поэкспериментировать с этим). Интенсивность убавляем с помощью прозрачности (выставляем 0.1 — это означает, что наша кисть будет на 90% прозрачна). Вот код объявления переменных и получения доступа к канве:
var action = "up";
var ctx,points,pointer;
function initcnvs(){
  ctx = document.getElementById('cnvs').getContext('2d');
  ctx.globalAlpha = 0.1;
  points = new Array(10);
};


* This source code was highlighted with Source Code Highlighter.

Вызов функции initcnvs() вешаем на onload у body. Остальные функции на соответствующие события:
<body onload="initcnvs()" onmousedown="mDown(event)" onmousemove="mMove(event)" onmouseup="mUp(event)">

* This source code was highlighted with Source Code Highlighter.

Наконец сам алгоритм. При нажатии кнопки меняем action на down, кидаем первую точку в наш массив и выставляем указатель на нее.
function mDown(e){
  action = "down";
  points[0] = [e.pageX, e.pageY];
  pointer = 0;
};


* This source code was highlighted with Source Code Highlighter.

При отпускании кнопки — очищаем массив и меняем action на up.
function mUp(e){
  points = new Array(10);
  action = "up";
};


* This source code was highlighted with Source Code Highlighter.

И наконец при движении (только если зажата мышка) мы определяем новую точку в массиве (если дошли до конца, то начинаем с начала). Далее рисуем обычный отрезок соединяющий предыдущую точку с новыми координатами курсора и, если массив уже достаточно наполнен, соединяем текущее положение курсора с самой старой точкой из массива, при этом добавляем рандомом погрешность в 5 пикселей. В конце сохраняем новые координаты в массив.
function mMove(e){
  if (action == "down") {
    var nextpoint = pointer + 1;
    if (nextpoint > 9) nextpoint = 0;
    ctx.beginPath();
    ctx.moveTo(points[pointer][0],points[pointer][1]);
    ctx.lineTo(e.pageX, e.pageY);
    if (points[nextpoint]) {
      ctx.moveTo(points[nextpoint][0] + Math.round(Math.random()*10-5),points[nextpoint][1] + Math.round(Math.random()*10-5));
      ctx.lineTo(e.pageX, e.pageY);
    }
    ctx.stroke();
    pointer = nextpoint;
    points[pointer] = [e.pageX, e.pageY];
  }
};


* This source code was highlighted with Source Code Highlighter.

Вот как выглядит эта кисточка в движении:

Вы можете сами поэкспериментировать с параметрами, случайностью, интенсивностью и посмотреть на результаты. Вот исходный код всего примера, достаточно компактно вышло:
<!DOCTYPE html>
<html>
 <head>
  <title></title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <style type="text/css">
    body {
      margin: 0;
    }
    #cnvs {
      border: #000000 1px solid;
    }
  </style>
  <script type="text/javascript">
    var action = "up";
    var ctx,points,pointer;
    function initcnvs(){
      ctx = document.getElementById('cnvs').getContext('2d');
      ctx.globalAlpha = 0.1;
      points = new Array(10);
    };
    function mDown(e){
      action = "down";
      points[0] = [e.pageX, e.pageY];
      pointer = 0;
    };
    function mUp(e){
      points = new Array(10);
      action = "up";
    };
    function mMove(e){
      if (action == "down") {
        var nextpoint = pointer + 1;
        if (nextpoint > 9) nextpoint = 0;
        ctx.beginPath();
        ctx.moveTo(points[pointer][0],points[pointer][1]);
        ctx.lineTo(e.pageX, e.pageY);
        if (points[nextpoint]) {
          ctx.moveTo(points[nextpoint][0] + Math.round(Math.random()*10-5),points[nextpoint][1] + Math.round(Math.random()*10-5));
          ctx.lineTo(e.pageX, e.pageY);
        }
        ctx.stroke();
        pointer = nextpoint;
        points[pointer] = [e.pageX, e.pageY];
      }
    };
  </script>
 </head>
 <body onload="initcnvs()" onmousedown="mDown(event)" onmousemove="mMove(event)" onmouseup="mUp(event)">
 <canvas id="cnvs" width="800" height="500"></canvas>
 </body>
</html>


* This source code was highlighted with Source Code Highlighter.


Вот и все, надеюсь, статья оказалась полезной для вас. В дальнейшем планирую рассказать об умной штриховке, кнопке «отмена» для канвы, трюке с размытой кисточкой и многом другом. Кроме того, если вас что-то интересует по теме — отпишите в комментариях, расскажу и об этом тоже. Ошибки в тексте — в приват, заранее спасибо.

P.S. Мои безумные умения с этой кисточкой:

+28
7.5k 56
Comments 4