Обработка контуров Adobe Illustrator, но с JavaScript

В этом посте я рассмотрю, как обрабатывать логические операции векторной графики, такие как объединение, разность, пересечение или xor, в холсте HTML5. Логические операции очень распространены и полезны для любого дизайнера. Если вы регулярно используете Adobe Illustrator для создания графических элементов, вы должны быть знакомы с панелью «Обработка контуров», которая позволяет использовать эти операции.

Почему векторные логические операции?

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

Использование g.js

Это будет довольно сложно реализовать самостоятельно, поэтому я решил использовать библиотеку под названием g.js. Это графическая библиотека JavaScript от команды Nodebox, и ее простые методы позволяют очень легко выполнять сложные операции с векторной графикой. Сначала я покажу вам, как использовать g.js для создания путей, а также как объединить библиотеку с другой библиотекой на основе холста p5.js в конце.

Давайте сначала настроим index.html и подключим canvas к нашему скрипту. Затем добавьте библиотеку в head. Вы можете либо скачать библиотеку, либо использовать CDN, как показано ниже:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://cdn.rawgit.com/nodebox/g.js/master/dist/g.min.js"></script>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      const canvas = document.querySelector("#canvas");
      const ctx = canvas.getContext("2d");
      canvas.width = 800
      canvas.height = 500
  
  	  // write drawing codes below

    </script>
  </body>
</html>

g.js имеет множество примитивных форм, таких как прямоугольник, эллипс, многоугольник и т. Д., Но здесь я сосредоточусь только на создании настраиваемого объекта пути. Чтобы создать новую форму или путь, используйте объект g.Path.

const pathA = new g.Path()

Чтобы добавить точки к пути, вы можете использовать методы, очень похожие на методы HTML5 Canvas.

const pathA = new g.Path();
pathA.moveTo(280, 100);
pathA.lineTo(560, 100);
pathA.curveTo(350, 190, 480, 400, 480, 400)
pathA.lineTo(150, 400)
pathA.curveTo(450, 220, 280, 100, 280, 100)
pathA.closePath();
pathA.stroke = "teal";
pathA.strokeWidth = 5
pathA.fill = "black";
pathA.draw(ctx);

Вы можете посмотреть исходный код, чтобы узнать, какие методы доступны для использования. Для рисования пути вы можете использовать moveTo(), lineTo(), curveTo(), quadTo() и closePath().

Давайте нарисуем сверху еще одну фигуру, но на этот раз вместо использования тех же методов я передам массив объектов команд в конструктор объекта Path вместе со стилем. Команда принимает форму объекта, как показано ниже:

{ type: "M", x: 100, y: 200 }

В случае кривой Безье ей необходимы три значения координат - (x,y), определяющие фактическое положение точки, и другие значения для маркеров кривой:

{ type: "C", x1: 50, y1: 50, x2: 100, y2: 100, x: 150, y: 150 }

И вот код для рисования нашего второго пути:

const cmds = [
	{
	  type: "M",
	  x: 250,
	  y: 180,
	},
	{
	  type: "L",
	  x: 680,
	  y: 200,
	},
	{
	  type: "C",
	  x1: 450,
	  y1: 250,
	  x2: 550,
	  y2: 400,
	  x: 550,
	  y: 400,
	},
	{
	  type: "L",
	  x: 250,
	  y: 180,
	},
	{
	  type: "Z",
	},
];

const pathB = new g.Path(cmds, "hotpink", "#ccaaff", 5);
pathB.draw(ctx);

Составной путь

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

g.compound(firstShape, secondShape, method)

Давайте попробуем все четыре составных метода - union, difference, intersection и xor.

// comment out the previous drawings
// pathA.draw(ctx);
// pathB.draw(ctx);

let compoundPath;

ctx.save()
ctx.translate(50, 0);
ctx.scale(0.5, 0.5);
compoundPath = g.compound(pathA, pathB, "union");
compoundPath.stroke = "black";
compoundPath.fill = "yellow";
compoundPath.strokeWidth = 5
compoundPath.draw(ctx);

ctx.translate(600, 0);
compoundPath = g.compound(pathA, pathB, "difference");
compoundPath.stroke = "black";
compoundPath.fill = "green";
compoundPath.strokeWidth = 5
compoundPath.draw(ctx);
ctx.restore()

ctx.save()
ctx.translate(50, 200);
ctx.scale(0.5, 0.5)
compoundPath = g.compound(pathA, pathB, "intersection");
compoundPath.stroke = "black";
compoundPath.fill = "blue";
compoundPath.strokeWidth = 5
compoundPath.draw(ctx);

ctx.translate(600, 0);
compoundPath = g.compound(pathA, pathB, "xor");
compoundPath.stroke = "black";
compoundPath.fill = "brown";
compoundPath.strokeWidth = 5
compoundPath.draw(ctx);

Как вы можете видеть на изображении выше, мы можем очертить фигуру по своему усмотрению, потому что все они являются векторными фигурами. Также следует отметить, что если вы console.log(compoundPath), он добавляет много дополнительных точек вокруг формы, и все они представляют собой простую команду L или lineTo(). Я еще не нашел способа упростить эти итоговые моменты.

В случае составной формы из метода difference в правом верхнем углу вы получите две результирующие фигуры (их может быть больше двух). Есть ли способ разделить их на отдельные фигуры? Да, есть. Я не уверен, предоставляет ли библиотека способ сделать это, но я понял, что могу просто перебрать весь массив команд и найти команду Z или closePath() всякий раз, когда форма заканчивается. Моя функция выглядит примерно так:

// pass a compoundPath(cPath) to the function
// it returns an array of Path objects
function separateCompoundPath(cPath) {
  const result = [];
  let path = new g.Path();
  for (let i = 0; i < cPath.commands.length; i++) {
    path.commands.push(cPath.commands[i]);
    if (cPath.commands[i].type === "Z") {
      result.push(path);
      path = new g.Path();
    }
  }
  return result;
}

Использовать с p5.js

Я лишь прикоснулся к очень мощной библиотеке g.js. Есть много других объектов и функций, которые значительно упростят программирование векторной графики, и я еще не уделил им достаточно времени. Тем не менее, я обычно использую p5.js библиотеку для любых набросков, связанных с Canvas, поэтому в качестве последнего шага я хочу показать вам, как использовать g.js логические операции с p5.js библиотекой.

Единственное, что вам следует знать, это то, что с помощью createCanvas() p5js создает для вас элемент canvas и его контекст. поэтому вместо того, чтобы подключать их вручную, просто используйте drawingContext. В остальном то же самое.

function setup() {
  createCanvas(800, 500);
  background(220);

  const pathA = new g.Path();
  pathA.moveTo(20, 20);
  pathA.lineTo(80, 20);
  pathA.lineTo(100, 80);
  pathA.lineTo(60, 80);
  pathA.lineTo(40, 120);
  pathA.closePath();
  pathA.fill = "yellow";
  pathA.draw(drawingContext);
}

Я завершу этот вопрос, поделившись несколькими ссылками, которые помогли мне изучить тему. Надеюсь, тебе весело!

Ссылки

Первоначально опубликовано на https://erraticgenerator.com.