Как стать автором
Обновить

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

Время на прочтение 4 мин
Количество просмотров 8.1K

Если вы создаете графический редактор на canvas, вам наверняка захочется иметь в арсенале мягкую кисть. Так вот, задача эта довольно нетривиальная и я постараюсь осветить основные трудности и подсказать пути решения.
(на картинке пример работы мягкой кисти в GIMP)
Перед прочтением рекомендую ознакомиться с предыдущей статьей.



Прежде всего вы должны понимать аудиторию и назначение вашего редактора. Это должно помочь вам расставить приоритеты, коих для данной проблемы можно выделить два:
  • Скорость работы
  • Качество работы

Решение, которое я предлагаю основано на встроенной возможности в canvas api, подходящей для этой задачи. А именно — тени. Идея такова: когда пользователь рисует по канве, на самом деле рисуется линия где-нибудь за пределами канвы с определенным смещением, а тень настраивается так, чтобы попадать как раз в то место, где рисует пользователь.

Вот примерная реализация:
<!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 {
   outline: #000000 1px solid;
  }
 </style>
 <script type="text/javascript">
  var action = "up";
  //переменные: канва, контекст, смещение для тени, массив точек, буфер для растра
  var canvas,ctx,offset,points,bufer;

  //инициализация канвы
  function initcnvs(){
   canvas = document.getElementById('cnvs');
   ctx = canvas.getContext('2d');
   ctx.lineWidth = 10;
   //смещение (больше чем ширина канвы)
   offset = 1000;
   //параметры тени
   ctx.shadowBlur = 10;
   ctx.shadowColor = "#000000";
   ctx.shadowOffsetX = -offset;
   points = new Array();
   //в буфере будем хранить растр канвы
   bufer = ctx.getImageData(0,0,canvas.width,canvas.height);
  };

  //при нажатии запоминаем первую точку
  function mDown(e){
   action = "down";
   points.push([e.pageX,e.pageY]);
  };

  //при отпускании кнопки - сохраняем канву и обнуляем массив точек
  function mUp(e){
   action = "up";
   points = new Array();
   bufer = ctx.getImageData(0,0,canvas.width,canvas.height);
  };

  //при движении - восстанавливаем растр из буфера и перерисовываем линию
  function mMove(e){
   if (action == "down") {
    ctx.putImageData(bufer,0,0);
    points.push([e.pageX,e.pageY]);
    ctx.beginPath();
    ctx.moveTo(points[0][0]+offset, points[0][1]);
    for (i = 1; i < points.length; i++){
     ctx.lineTo(points[i][0]+offset,points[i][1]);
    }
    ctx.stroke();
   }
  };
 </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.

ctx.shadowBlur — отвечает за размытие тени, ctx.shadowColor — цвет, ctx.shadowOffsetX — смещение по оси X.
В переменной offset хранится безопасное смещение, чтобы исходная линия не могла попасть на канву. В переменную bufer сохраняется растр канвы. Зачем? Тут и выясняется главный минус алгоритма. После добавления каждой новой точки к линии (и к тени соответственно), нам приходится перерисовывать заново всю линию. Если этого не делать, а составлять линию из отрезков соединяющих две соседних точки, то будет заметна ее прерывистость. (в случае с непрозрачной кистью этот метод бы подошел).

Возникает закономерный вопрос: если количество точек будет большим, не скажется ли это на производительности? Ответ: скажется. Заметное замедление начинается уже после 5 секунд рисования, но рисовать без особых трудностей можно еще долгое время.
В ряде случаев этот вариант может быть приемлем.
Например в редакторе на deviantart именно такой или схожий вариант.
Как вариант, можно задать фиксированный размер массива и сохранять канву в буфер после заполнения, а затем продолжать рисовать с этой точки. В месте разрыва будет небольшой артефакт. Кого-то может устроить и такой вариант.

Конечно, есть еще варианты. Вот, скажем так, не самый простой.

На этом рисунке я отметил какие куски линии придется прорисовывать по-отдельности. Полукруги нужно будет заливать радиальным градиентом от непрозрачного к прозрачному. Прочие куски — линейным градиентом.

Сразу скажу — в данном алгоритме очень много не сильно сложной, но занудной математики и приводить я его не стану. Если это действительно нужно — лучше вывести его самому, чтобы полностью понимать все моменты. Вот основные моменты алгоритма:
  • при зажатии кнопки нужно запомнить точку старта
  • необходимо нарисовать полукруг с радиальным градиентом, когда будет известна вторая точка (чтобы повернуть его в сторону этой точки)
  • далее рисуются трапеции с учетом углов между отрезками линии
  • трапеции заливаются градиентом: прозрачный — непрозрачный — прозрачный от одного основания до другого
  • при отпускании — нужно нарисовать завершающий полукруг


Есть еще довольно простой способ реализации — спрайтами.

Вот и все, о чем я хотел сегодня рассказать. В комментариях отвечу на все возникшие вопросы.

PS: спасибо пользователю Serator за то, что подсказал использовать outline вместо border. Это избавило от погрешности в координатах.
спасибо пользователю TheShock за пару дельных замечаний.

Предыдущие статьи из серии:
Кисть для скетчей
Теги:
Хабы:
+20
Комментарии 9
Комментарии Комментарии 9

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн