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


    Если вы создаете графический редактор на 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
    • 5,3k
    • 9
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 9

      +2
      На самом деле в Гимпе все делается через спрайт кисти:


      Когда делал графический редактор на LibCanvas, то простую кисть тоже делал через lineTo, но, в итоге, решил, что лучше делать только спрайтами и не заморачиваться.
        +2
        Кстати, на счёт buffer. Почитайте ответ на вопросы 21 и 26 в моём Canvas FAQ

        Между прочим, зачем рисовать за пределами канвы, если можно рисовать прозрачной кистью, на с тенью:
        ctx.strokeStyle = 'rgba(0,0,0,0)';
        
          0
          спасибо, дельные советы
            0
            кстати, попробовал strokeStyle выставить прозрачным — тень в данном случае не рисуется
          0
          А если просто рисовать тонкой линией чтобы ее не было видно на фоне тени? или это не сработает если у кисти малый радиус?
            0
            ширина тени зависит от ширины линии
            0
            Замечательный способ… но перерисовка заметна даже в Chrome(
            Надо будет попробовать сделать частичную перерисовку.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое