【Перевод】Волшебник на холсте

Canvas

Супер хорошая статья, которую я нашел недавно! За год до этого я сам разработал веб-доску с ручной росписью, было бы здорово, если бы я ее увидел в то время! Не пропустите два эффекта Super 6 в конце статьи! p.s. Каждый пример в исходном тексте поставляется с codepen.Если вам интересно, вы можете нажать на исходный текст, чтобы протестировать один за другим~

Оригинальный адрес:Exploring canvas drawing techniques

---------- Разделительная линия кузова ----------

В последнее время я экспериментировал с различными стилями рисования в Интернете: плавными мазками, мазками Безье, мазками тушью, мазками карандашом, печатными мазками и т. д. Результат меня очень удивил~ Итак, я решил собрать интерактивный учебник по мазкам на холсте, чтобы насладиться этим опытом. Мы начнем с основ (очень примитивные мазки движениями мыши), перейдем к гармоничным мазкам, похожим на кисти, к другим мазкам со сложными кривыми, странными, но красивыми. Этот урок также отражает мои исследования холста.

Я кратко представлю различные реализации кистей, если вы знаете, что можете сами реализовать свободные мазки, то можете повеселиться.

Прежде чем начать, вы, конечно, хотя бы дляcanvasПонять ой

Основание

Сначала начните с основ.

нормальный ход

var el = document.getElementById('c');
var ctx = el.getContext('2d');
var isDrawing;

el.onmousedown = function(e) {
  isDrawing = true;
  ctx.moveTo(e.clientX, e.clientY);
};
el.onmousemove = function(e) {
  if (isDrawing) {
    ctx.lineTo(e.clientX, e.clientY);
    ctx.stroke();
  }
};
el.onmouseup = function() {
  isDrawing = false;
};

Слушайте события mousedown, mousemove и mouseup на холсте. Когда mousedown, переместите начальную точку на (ctx.moveTo) координаты щелчка мыши. Когда MouseMove, подключите (ctx.lineTo) К новым координатам проведите линию. Наконец, при наведении указателя мыши конец отрисовки иisDrawingФлаг установлен в значение false. Это делается для того, чтобы избежать того, что когда мышь не совершает щелчка, а просто перемещается из фокуса на холсте, линия не будет нарисована. Вы также можете прослушивать событие mousemove по событию mousedown и отменять событие mousemove по событию mouseup, но удобнее установить глобальный флаг.

ctx.lineWidthctx.lineJoinа такжеctx.lineCapрешить для 'круглого' (Некоторые случаи на MDN).

var el = document.getElementById('c');
var ctx = el.getContext('2d');
var isDrawing;

el.onmousedown = function(e) {
  isDrawing = true;
  ctx.lineWidth = 10;
  ctx.lineJoin = ctx.lineCap = 'round';
  ctx.moveTo(e.clientX, e.clientY);
};
el.onmousemove = function(e) {
  if (isDrawing) {
    ctx.lineTo(e.clientX, e.clientY);
    ctx.stroke();
  }
};
el.onmouseup = function() {
  isDrawing = false;
};

Сгладить края заштрихованного

Линии по углам теперь менее неровные. Тем не менее, основная часть линии по-прежнему неровная.Поскольку у холста нет прямого API сглаживания, как нам оптимизировать край?

Один из способов — с тенями.

var el = document.getElementById('c');
var ctx = el.getContext('2d');
var isDrawing;

el.onmousedown = function(e) {
  isDrawing = true;
  ctx.lineWidth = 10;
  ctx.lineJoin = ctx.lineCap = 'round';
  ctx.shadowBlur = 10;
  ctx.shadowColor = 'rgb(0, 0, 0)';
  ctx.moveTo(e.clientX, e.clientY);
};
el.onmousemove = function(e) {
  if (isDrawing) {
    ctx.lineTo(e.clientX, e.clientY);
    ctx.stroke();
  }
};
el.onmouseup = function() {
  isDrawing = false;
};

просто добавьctx.shadowBlurа такжеctx.shadowColor.边缘明显更为顺滑,锯齿边缘都被阴影包裹住了。但是却有个小问题。注意到线条的开头部分通常较淡也较糊,尾部颜色却会变得更深。效果独特,不过并不是我们的本意。这是由什么引起的呢?

Ответ перекрывает тени. Текущая тень тени на инсульте полосового хода, тень, покрывающая более мощный, тем слабее эффект размытия, цвет линии также более темнее. Как я могу решить эту проблему LE?

точечная обработка

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

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 10;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if (!isDrawing) return;

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  points.push({ x: e.clientX, y: e.clientY });

  ctx.beginPath();
  ctx.moveTo(points[0].x, points[0].y);
  for (var i = 1; i < points.length; i++) {
    ctx.lineTo(points[i].x, points[i].y);
  }
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

Как видите, он почти такой же, как и в первом примере, с одинаковой толщиной от начала до конца. Теперь мы можем попробовать добавить к нему тень~

var el = document.getElementById('c');
var ctx = el.getContext('2d');
var isDrawing;

el.onmousedown = function(e) {
  isDrawing = true;
  ctx.moveTo(e.clientX, e.clientY);
};
el.onmousemove = function(e) {
  if (isDrawing) {
    var radgrad = ctx.createRadialGradient(
      e.clientX,e.clientY,10,e.clientX,e.clientY,20);
    
    radgrad.addColorStop(0, '#000');
    radgrad.addColorStop(0.5, 'rgba(0,0,0,0.5)');
    radgrad.addColorStop(1, 'rgba(0,0,0,0)');
    ctx.fillStyle = radgrad;
    
    ctx.fillRect(e.clientX-20, e.clientY-20, 40, 40);
  }
};
el.onmouseup = function() {
  isDrawing = false;
};

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

Решением этой проблемы может быть автоматическое заполнение промежутка дополнительными точками, когда расстояние между двумя точками опускания пера слишком велико.

function distanceBetween(point1, point2) {
  return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2));
}
function angleBetween(point1, point2) {
  return Math.atan2( point2.x - point1.x, point2.y - point1.y );
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, lastPoint;

el.onmousedown = function(e) {
  isDrawing = true;
  lastPoint = { x: e.clientX, y: e.clientY };
};

el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  var currentPoint = { x: e.clientX, y: e.clientY };
  var dist = distanceBetween(lastPoint, currentPoint);
  var angle = angleBetween(lastPoint, currentPoint);
  
  for (var i = 0; i < dist; i+=5) {
    
    x = lastPoint.x + (Math.sin(angle) * i);
    y = lastPoint.y + (Math.cos(angle) * i);
    
    var radgrad = ctx.createRadialGradient(x,y,10,x,y,20);
    
    radgrad.addColorStop(0, '#000');
    radgrad.addColorStop(0.5, 'rgba(0,0,0,0.5)');
    radgrad.addColorStop(1, 'rgba(0,0,0,0)');
    
    ctx.fillStyle = radgrad;
     ctx.fillRect(x-20, y-20, 40, 40);
  }
  
  lastPoint = currentPoint;
};

el.onmouseup = function() {
  isDrawing = false;
};

Наконец-то получился плавный изгиб!

Возможно, вы заметили небольшое изменение в приведенном выше примере. Мы сохраняем только последнюю точку пути, а не все точки всего пути. Каждый раз, когда линия подключается, она будет соединяться от предыдущей точки к текущей последней точке, чтобы получить расстояние между двумя точками. Если интервал слишком велик, заполните его большим количеством точек. Преимущество этого в том, что вам не нужно каждый раз сохранять все массивы точек!

Кривые Безье

Пожалуйста, имейте в виду концепцию между двумя точками, соединенными с ними прямой линией, как с кривой Безье. Это будет выглядеть более естественным путем. Практика заменена прямой линиейquadraticCurveTo, и используйте среднюю точку между двумя точками в качестве контрольной точки:

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  points.push({ x: e.clientX, y: e.clientY });

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);
  console.log(points);

  for (var i = 1, len = points.length; i < len; i++) {
    // we pick the point between pi+1 & pi+2 as the
    // end point and p1 as our control point
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  // Draw last line as a straight line while
  // we wait for the next point to be able to calculate
  // the bezier control point
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
};

Итак, вы владеете основами рисования и умеете рисовать плавные плавные кривые. Далее, давайте сделаем что-нибудь повеселее~

Эффект кисти, эффект заусенца, эффект ручной росписи

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

el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  var currentPoint = { x: e.clientX, y: e.clientY };
  var dist = distanceBetween(lastPoint, currentPoint);
  var angle = angleBetween(lastPoint, currentPoint);
  
  for (var i = 0; i < dist; i++) {
    x = lastPoint.x + (Math.sin(angle) * i) - 25;
    y = lastPoint.y + (Math.cos(angle) * i) - 25;
    ctx.drawImage(img, x, y);
  }
  
  lastPoint = currentPoint;
};

В зависимости от изображения заливки мы можем создавать кисти с разными характеристиками. На картинке выше толстая кисть.

Эффект заусенца (обратные штрихи)

Каждый раз, когда вы заполняете путь изображением, произвольно поворачивая изображение, вы можете получить очень интересный эффект, похожий на эффект заусенцев/гирлянд на изображении ниже:

el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  var currentPoint = { x: e.clientX, y: e.clientY };
  var dist = distanceBetween(lastPoint, currentPoint);
  var angle = angleBetween(lastPoint, currentPoint);
  
  for (var i = 0; i < dist; i++) {
    x = lastPoint.x + (Math.sin(angle) * i);
    y = lastPoint.y + (Math.cos(angle) * i);
    ctx.save();
    ctx.translate(x, y);
    ctx.scale(0.5, 0.5);
    ctx.rotate(Math.PI * 180 / getRandomInt(0, 180));
    ctx.drawImage(img, 0, 0);
    ctx.restore();
  }
  
  lastPoint = currentPoint;
};

Эффект рисования вручную (случайная ширина)

Чтобы моделировать покрашенное вручную эффектом, то ширина неопределенного пути есть. Мы все еще используемmoveTo+lineTo

...
for (var i = 1; i < points.length; i++) {
    ctx.beginPath();
    ctx.moveTo(points[i-1].x, points[i-1].y);
    ctx.lineWidth = points[i].width;
    ctx.lineTo(points[i].x, points[i].y);
    ctx.stroke();
  }
 

Но помните, пользовательская ширина линии не может быть большим зазором О.

Ручной окрашенный эффект # 2 (несколько строк)

Еще одна реализация эффекта нарисованного от руки — имитация нескольких линий. Мы добавим еще две строки (называемые ниже «присоединенными строками») рядом с соединением, но, конечно, позиции будут немного смещены. Метод состоит в том, чтобы выбрать две случайные точки (синие точки) рядом с исходной точкой (зеленые точки) и соединить их так, чтобы вы получили две дополнительные линии рядом с исходной линией. Разве это не идеальная имитация эффекта раздвоения пера!

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 1;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = 'purple';

var isDrawing, lastPoint;

el.onmousedown = function(e) {
  isDrawing = true;
  lastPoint = { x: e.clientX, y: e.clientY };
};

el.onmousemove = function(e) {
  if (!isDrawing) return;

  ctx.beginPath();
  
  ctx.moveTo(lastPoint.x - getRandomInt(0, 2), lastPoint.y - getRandomInt(0, 2));
  ctx.lineTo(e.clientX - getRandomInt(0, 2), e.clientY - getRandomInt(0, 2));
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x, lastPoint.y);
  ctx.lineTo(e.clientX, e.clientY);
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x + getRandomInt(0, 2), lastPoint.y + getRandomInt(0, 2));
  ctx.lineTo(e.clientX + getRandomInt(0, 2), e.clientY + getRandomInt(0, 2));
  ctx.stroke();
    
  lastPoint = { x: e.clientX, y: e.clientY };
};

el.onmouseup = function() {
  isDrawing = false;
};

Эффект кисти поперечного сечения

Если мы используем несколько дополнительных линий и немного смещаем их, мы можем имитировать кисть с поперечным сечением, как маркер. Таким образом, нет необходимости заполнять путь изображением, и обводка, естественно, будет иметь эффект смещения~

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 3;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, lastPoint;

el.onmousedown = function(e) {
  isDrawing = true;
  lastPoint = { x: e.clientX, y: e.clientY };
};

el.onmousemove = function(e) {
  if (!isDrawing) return;

  ctx.beginPath();
  
  ctx.globalAlpha = 1;
  ctx.moveTo(lastPoint.x, lastPoint.y);
  ctx.lineTo(e.clientX, e.clientY);
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x - 4, lastPoint.y - 4);
  ctx.lineTo(e.clientX - 4, e.clientY - 4);
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x - 2, lastPoint.y - 2);
  ctx.lineTo(e.clientX - 2, e.clientY - 2);
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x + 2, lastPoint.y + 2);
  ctx.lineTo(e.clientX + 2, e.clientY + 2);
  ctx.stroke();
  
  ctx.moveTo(lastPoint.x + 4, lastPoint.y + 4);
  ctx.lineTo(e.clientX + 4, e.clientY + 4);
  ctx.stroke();
    
  lastPoint = { x: e.clientX, y: e.clientY };
};

el.onmouseup = function() {
  isDrawing = false;
};

Кисть поперечного сечения с прозрачностью

Если мы будем добавлять все больше и больше прозрачности к каждой дополнительной строке на основе предыдущего эффекта, мы можем получить следующий интересный эффект:

полилиния

直线练习得够多的啦,我们能否将上文介绍的几种技巧应用于贝塞尔曲线上呢? Конечно.同样只需将每条曲线在原线的基础上偏移一点:

function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 1;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  points.push({ x: e.clientX, y: e.clientY });
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
   
  stroke(offsetPoints(-4));
  stroke(offsetPoints(-2));
  stroke(points);
  stroke(offsetPoints(2));
  stroke(offsetPoints(4));
};

function offsetPoints(val) {
  var offsetPoints = [ ];
  for (var i = 0; i < points.length; i++) {
    offsetPoints.push({ 
      x: points[i].x + val,
      y: points[i].y + val
    });
  }
  return offsetPoints;
}

function stroke(points) {
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    // we pick the point between pi+1 & pi+2 as the
    // end point and p1 as our control point
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  // Draw last line as a straight line while
  // we wait for the next point to be able to calculate
  // the bezier control point
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
}

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

С многострочной прозрачностью

Вы также можете добавить прозрачность к каждой строке по очереди, что довольно элегантно.

function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 1;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  points.push({ x: e.clientX, y: e.clientY });
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  ctx.strokeStyle = 'rgba(0,0,0,1)';
  stroke(offsetPoints(-4));
  ctx.strokeStyle = 'rgba(0,0,0,0.8)';
  stroke(offsetPoints(-2));
  ctx.strokeStyle = 'rgba(0,0,0,0.6)';
  stroke(points);
  ctx.strokeStyle = 'rgba(0,0,0,0.4)';
  stroke(offsetPoints(2));
  ctx.strokeStyle = 'rgba(0,0,0,0.2)';
  stroke(offsetPoints(4));
};

function offsetPoints(val) {
  var offsetPoints = [ ];
  for (var i = 0; i < points.length; i++) {
    offsetPoints.push({ 
      x: points[i].x + val,
      y: points[i].y + val
    });
  }
  return offsetPoints;
}

function stroke(points) {
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    // we pick the point between pi+1 & pi+2 as the
    // end point and p1 as our control point
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  // Draw last line as a straight line while
  // we wait for the next point to be able to calculate
  // the bezier control point
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
}

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

Теперь, когда мы научились рисовать линии и кривые, применить кисть-штамп стало еще проще! Нам просто нужно нарисовать какую-то графику по координатам каждой точки на пути мыши, и вот эффект красного круга:

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineJoin = ctx.lineCap = 'round';
ctx.fillStyle = 'red';

var isDrawing, points = [ ], radius = 15;

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};
el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  points.push({ x: e.clientX, y: e.clientY });
  
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  for (var i = 0; i < points.length; i++) {
    ctx.beginPath();
    ctx.arc(points[i].x, points[i].y, radius, false, Math.PI * 2, false);
    ctx.fill();
    ctx.stroke();
  }
};
el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

эффект дорожки

На приведенном выше изображении также есть несколько точек, которые находятся слишком далеко друг от друга, что также можно решить, заполнив средние точки. Следующее производит интересные следы или эффекты трубы. Вы можете контролировать расстояние между точками и, следовательно, плотность дорожек.

See the Pen Ictqs by Juriy Zaytsev (@kangax) on CodePen.

Случайный радиус и прозрачность

Также можно добавлять новые пункты в исходный рецепт, что-то делать с каждой печатаемой случайной модификацией. Например, печать случайного изменения для изменения радиуса и прозрачности.

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineJoin = ctx.lineCap = 'round';
ctx.fillStyle = 'red';

var isDrawing, points = [ ], radius = 15;

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ 
    x: e.clientX, 
    y: e.clientY,
    radius: getRandomInt(10, 30),
    opacity: Math.random()
  });
};
el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  points.push({ 
    x: e.clientX, 
    y: e.clientY,
    radius: getRandomInt(5, 20),
    opacity: Math.random()
  });
  
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  for (var i = 0; i < points.length; i++) {
    ctx.beginPath();
    ctx.globalAlpha = points[i].opacity;
    ctx.arc(
      points[i].x, points[i].y, points[i].radius, 
      false, Math.PI * 2, false);
    ctx.fill();
  }
};
el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

графика

Так как это принт, форма принта также может быть произвольной. На картинке ниже изображен отпечаток, образованный формой пентаграммы:

function drawStar(x, y) {
  var length = 15;
  ctx.save();
  ctx.translate(x, y);
  ctx.beginPath();
  ctx.rotate((Math.PI * 1 / 10));
  for (var i = 5; i--;) {
    ctx.lineTo(0, length);
    ctx.translate(0, length);
    ctx.rotate((Math.PI * 2 / 10));
    ctx.lineTo(0, -length);
    ctx.translate(0, -length);
    ctx.rotate(-(Math.PI * 6 / 10));
  }
  ctx.lineTo(0, length);
  ctx.closePath();
  ctx.stroke();
  ctx.restore();
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineJoin = ctx.lineCap = 'round';
ctx.fillStyle = 'red';

var isDrawing, points = [ ], radius = 15;

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};
el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  points.push({ x: e.clientX, y: e.clientY });
  
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  for (var i = 0; i < points.length; i++) {
    drawStar(points[i].x, points[i].y);
  }
};
el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

Повернуть графику

Те же пятиконечные звезды более естественны, если их вращать хаотично.

See the Pen Cspre by Juriy Zaytsev (@kangax) on CodePen.

Случайный

Если мы рандомизируем... размер, угол, непрозрачность, цвет и даже толщину, результат будет просто великолепен!

function drawStar(options) {
  var length = 15;
  ctx.save();
  ctx.translate(options.x, options.y);
  ctx.beginPath();
  ctx.globalAlpha = options.opacity;
  ctx.rotate(Math.PI / 180 * options.angle);
  ctx.scale(options.scale, options.scale);
  ctx.strokeStyle = options.color;
  ctx.lineWidth = options.width;
  for (var i = 5; i--;) {
    ctx.lineTo(0, length);
    ctx.translate(0, length);
    ctx.rotate((Math.PI * 2 / 10));
    ctx.lineTo(0, -length);
    ctx.translate(0, -length);
    ctx.rotate(-(Math.PI * 6 / 10));
  }
  ctx.lineTo(0, length);
  ctx.closePath();
  ctx.stroke();
  ctx.restore();
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

var isDrawing, points = [ ], radius = 15;

function addRandomPoint(e) {
  points.push({ 
    x: e.clientX, 
    y: e.clientY, 
    angle: getRandomInt(0, 180),
    width: getRandomInt(1,10),
    opacity: Math.random(),
    scale: getRandomInt(1, 20) / 10,
    color: ('rgb('+getRandomInt(0,255)+','+getRandomInt(0,255)+','+getRandomInt(0,255)+')')
  });
}

el.onmousedown = function(e) {
  isDrawing = true;
  addRandomPoint(e);
};
el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  addRandomPoint(e);
  
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  for (var i = 0; i < points.length; i++) {
    drawStar(points[i]);
  }
};
el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

цветной пиксель

Не зацикливайтесь на форме. Случайно разбросанные цветные пиксели вокруг движущихся мазков тоже милые! Цвета и расположение могут быть случайными!

function drawPixels(x, y) {
  for (var i = -10; i < 10; i+= 4) {
    for (var j = -10; j < 10; j+= 4) {
      if (Math.random() > 0.5) {
        ctx.fillStyle = ['red', 'orange', 'yellow', 'green', 
                         'light-blue', 'blue', 'purple'][getRandomInt(0,6)];
        ctx.fillRect(x+i, y+j, 4, 4);
      }
    }
  }
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineJoin = ctx.lineCap = 'round';
var isDrawing, lastPoint;

el.onmousedown = function(e) {
  isDrawing = true;
  lastPoint = { x: e.clientX, y: e.clientY };
};
el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  drawPixels(e.clientX, e.clientY);
  
  lastPoint = { x: e.clientX, y: e.clientY };
};
el.onmouseup = function() {
  isDrawing = false;
};

Узорчатые кисти

Мы поэкспериментировали с эффектом штампа, а теперь давайте рассмотрим другую, но тоже интересную технику — узорчатую кисть. Мы можем использовать холстcreatePatternapi для заполнения пути. Ниже показана простая кисть с точечным рисунком.

точка
function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}
function getPattern() {
  var patternCanvas = document.createElement('canvas'),
      dotWidth = 20,
      dotDistance = 5,
      patternCtx = patternCanvas.getContext('2d');

  patternCanvas.width = patternCanvas.height = dotWidth + dotDistance;

  patternCtx.fillStyle = 'red';
  patternCtx.beginPath();
  patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false);
  patternCtx.closePath();
  patternCtx.fill();
  return ctx.createPattern(patternCanvas, 'repeat');
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 25;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = getPattern();

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  points.push({ x: e.clientX, y: e.clientY });

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

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

полоса

На основе приведенных выше примеров вы также можете создавать свои собственные узоры, например, горизонтальные полосы.

function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}
function getPattern() {
  var patternCanvas = document.createElement('canvas'),
      dotWidth = 20,
      dotDistance = 5,
      ctx = patternCanvas.getContext('2d');

  patternCanvas.width = patternCanvas.height = 10;
  ctx.strokeStyle = 'green';
  ctx.lineWidth = 5;
  ctx.beginPath();
  ctx.moveTo(0, 5);
  ctx.lineTo(10, 5);
  ctx.closePath();
  ctx.stroke();
  return ctx.createPattern(patternCanvas, 'repeat');
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 25;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = getPattern();

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  points.push({ x: e.clientX, y: e.clientY });

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

##### двойные цветные полосы

... или вертикальные цветные полосы.

function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}
function getPattern() {
  var patternCanvas = document.createElement('canvas'),
      dotWidth = 20,
      dotDistance = 5,
      ctx = patternCanvas.getContext('2d');

  patternCanvas.width = 10; patternCanvas.height = 20;
  ctx.fillStyle = 'black';
  ctx.fillRect(0, 0, 5, 20);
  ctx.fillStyle = 'gold';
  ctx.fillRect(5, 0, 10, 20);
  return ctx.createPattern(patternCanvas, 'repeat');
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 25;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = getPattern();

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  points.push({ x: e.clientX, y: e.clientY });

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

Радуга

…或者是有不同颜色的多重线(我喜欢这个图案!)。 Все возможно!

function midPointBtw(p1, p2) {
  return {
    x: p1.x + (p2.x - p1.x) / 2,
    y: p1.y + (p2.y - p1.y) / 2
  };
}
function getPattern() {
  var patternCanvas = document.createElement('canvas'),
      dotWidth = 20,
      dotDistance = 5,
      ctx = patternCanvas.getContext('2d');

  patternCanvas.width = 35; patternCanvas.height = 20;
  ctx.fillStyle = 'red';
  ctx.fillRect(0, 0, 5, 20);
  ctx.fillStyle = 'orange';
  ctx.fillRect(5, 0, 10, 20);
  ctx.fillStyle = 'yellow';
  ctx.fillRect(10, 0, 15, 20);
  ctx.fillStyle = 'green';
  ctx.fillRect(15, 0, 20, 20);
  ctx.fillStyle = 'lightblue';
  ctx.fillRect(20, 0, 25, 20);
  ctx.fillStyle = 'blue';
  ctx.fillRect(25, 0, 30, 20);
  ctx.fillStyle = 'purple';
  ctx.fillRect(30, 0, 35, 20);
  return ctx.createPattern(patternCanvas, 'repeat');
}

var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 25;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = getPattern();

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if (!isDrawing) return;
  
  points.push({ x: e.clientX, y: e.clientY });

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
  var p1 = points[0];
  var p2 = points[1];
  
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);

  for (var i = 1, len = points.length; i < len; i++) {
    var midPoint = midPointBtw(p1, p2);
    ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
    p1 = points[i];
    p2 = points[i+1];
  }
  ctx.lineTo(p1.x, p1.y);
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

картина

createPatternЭто картинки.

пистолет-распылитель

Как я могу пропустить эффект оружия? Также есть несколько способов его реализации. Например, заполните точку пикселя рядом с контактами пера. Чем больше радиус заполнения, тем толще эффект. Чем больше заполняется точка пикселя, тем плотнее.

var el = document.getElementById('c');
var ctx = el.getContext('2d');
var isDrawing;
var density = 50;

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

el.onmousedown = function(e) {
  isDrawing = true;
  ctx.lineWidth = 10;
  ctx.lineJoin = ctx.lineCap = 'round';
  ctx.moveTo(e.clientX, e.clientY);
};
el.onmousemove = function(e) {
  if (isDrawing) {
    for (var i = density; i--; ) {
      var radius = 20;
      var offsetX = getRandomInt(-radius, radius);
      var offsetY = getRandomInt(-radius, radius);
      ctx.fillRect(e.clientX + offsetX, e.clientY + offsetY, 1, 1);
    }
  }
};
el.onmouseup = function() {
  isDrawing = false;
};

Пистолет непрерывного действия

Вы можете заметить, что между описанным выше методом и реальным эффектом аэрографа все еще есть небольшой разрыв. Настоящий аэрограф распыляет непрерывно, а не только при движении мыши/кисти. Когда мышь нажимает на определенную область, мы можем струйно рисовать эту область с определенным интервалом. Таким образом, «пистолет» дольше остается на участке, а результирующая струя становится тяжелее.

See the Pen Craxn by Juriy Zaytsev (@kangax) on CodePen.

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

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

Соседи связаны

Понятие соединения соседних точек было введено Зефранком.Scribbleс мистером дубомHarmony(Примечание: эти два звена почти затерялись в длинной реке истории...) Популяризировано. Идея состоит в том, чтобы соединить похожие точки на нарисованном пути. Это создаст эффект размытия эскиза или сгиба сетки (примечание: я думаю, что это также лучший эффект!).

все точки связаны

Первоначальный подход может заключаться в добавлении дополнительных штрихов к первому нормальному примеру соединения. Для каждой точки пути соедините ее с предыдущей точкой:

el.onmousemove = function(e) {
  if (!isDrawing) return;

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  points.push({ x: e.clientX, y: e.clientY });

  ctx.beginPath();
  ctx.moveTo(points[0].x, points[0].y);
  for (var i = 1; i < points.length; i++) {
    ctx.lineTo(points[i].x, points[i].y);
    var nearPoint = points[i-5];
    if (nearPoint) {
      ctx.moveTo(nearPoint.x, nearPoint.y);
      ctx.lineTo(points[i].x, points[i].y);
    }
  }
  ctx.stroke();
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

Даже до дополнительной прозрачности или тени точки плюс, так что они могут стать более реалистичным стилем.

Соседняя точка

See the Pen EjivI by Juriy Zaytsev (@kangax) on CodePen.
var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 1;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  points = [ ];
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if (!isDrawing) return;

  //ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  points.push({ x: e.clientX, y: e.clientY });

  ctx.beginPath();
  ctx.moveTo(points[points.length - 2].x, points[points.length - 2].y);
  ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y);
  ctx.stroke();
  
  for (var i = 0, len = points.length; i < len; i++) {
    dx = points[i].x - points[points.length-1].x;
    dy = points[i].y - points[points.length-1].y;
    d = dx * dx + dy * dy;

    if (d < 1000) {
      ctx.beginPath();
      ctx.strokeStyle = 'rgba(0,0,0,0.3)';
      ctx.moveTo( points[points.length-1].x + (dx * 0.2), points[points.length-1].y + (dy * 0.2));
      ctx.lineTo( points[i].x - (dx * 0.2), points[i].y - (dy * 0.2));
      ctx.stroke();
    }
  }
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

Ключевой код для этой части:

var lastPoint = points[points.length-1];

  for (var i = 0, len = points.length; i < len; i++) {
    dx = points[i].x - lastPoint.x;
    dy = points[i].y - lastPoint.y;
    d = dx * dx + dy * dy;

    if (d < 1000) {
      ctx.beginPath();
      ctx.strokeStyle = 'rgba(0,0,0,0.3)';
      ctx.moveTo(lastPoint.x + (dx * 0.2), lastPoint.y + (dy * 0.2));
      ctx.lineTo(points[i].x - (dx * 0.2), points[i].y - (dy * 0.2));
      ctx.stroke();
    }
  }

Что тут происходит! Это выглядит сложно, но правда очень проста~

При рисовании линии мы сравниваем расстояние от текущей точки до всех точек. Если расстояние меньше определенного значения (например, 1000 в примере), которое является соседней точкой, то мы соединим текущую точку с этой соседней точкой. пройти черезdx*0.2а такжеdy*0.2Соединение добавить немного смещения.

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

эффект края заусенца

Внесите небольшую модификацию в приведенную выше формулу, чтобы установить связьобратный(т.е. от текущей точки до соседней точки относительно точки, противоположной текущей точке, есть бит!). Добавьте немного смещения, вы можете сделать эффект границы заусенца ~

See the Pen tmIuD by Juriy Zaytsev (@kangax) on CodePen.
var el = document.getElementById('c');
var ctx = el.getContext('2d');

ctx.lineWidth = 1;
ctx.lineJoin = ctx.lineCap = 'round';

var isDrawing, points = [ ];

el.onmousedown = function(e) {
  points = [ ];
  isDrawing = true;
  points.push({ x: e.clientX, y: e.clientY });
};

el.onmousemove = function(e) {
  if (!isDrawing) return;

  //ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  points.push({ x: e.clientX, y: e.clientY });

  ctx.beginPath();
  ctx.moveTo(points[points.length - 2].x, points[points.length - 2].y);
  ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y);
  ctx.stroke();
  
  for (var i = 0, len = points.length; i < len; i++) {
    dx = points[i].x - points[points.length-1].x;
    dy = points[i].y - points[points.length-1].y;
    d = dx * dx + dy * dy;

    if (d < 2000 && Math.random() > d / 2000) {
      ctx.beginPath();
      ctx.strokeStyle = 'rgba(0,0,0,0.3)';
      ctx.moveTo( points[points.length-1].x + (dx * 0.5), points[points.length-1].y + (dy * 0.5));
      ctx.lineTo( points[points.length-1].x - (dx * 0.5), points[points.length-1].y - (dy * 0.5));
      ctx.stroke();
    }
  }
};

el.onmouseup = function() {
  isDrawing = false;
  points.length = 0;
};

У Лукаса естьстатья

Итак, теперь вы овладели искусством рисования базовой и высококачественной графики. Однако в этой статье мы представили только мех.Возможности рисования холстом безграничны.Изменение цвета и прозрачности – это совершенно другой стиль. Приглашаем всех попрактиковаться и создать еще больше крутых эффектов!