
Если вы создаете графический редактор на 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 за пару дельных замечаний.
Предыдущие статьи из серии:
Кисть для скетчей