29 января 2020

Rough.js: как заставить компьютер рисовать «от руки»

Пишу, перевожу и иллюстрирую IT-статьи. На proglib написал 140 материалов. Увлекаюсь Python, вебом и Data Science. Открыт к диалогу – ссылки на соцсети и мессенджеры: https://matyushkin.github.io/links/ Если понравился стиль изложения, упорядоченный список публикаций — https://github.com/matyushkin/lessons
Слишком минималистичная, ровная графика может приедасться. Простой способ внести приятную шероховатость – библиотека Rough.js. Показываем примеры кода и применения.
Rough.js: как заставить компьютер рисовать «от руки»

Rough.js – небольшая графическая библиотека (< 9 кБ в gzip), позволяющая рисовать на JavaScript в скетчевом стиле, то есть получать графику, как бы сделанную от руки. Пакет определяет примитивы для рисования линий, кривых, дуг, многоугольников, кругов и эллипсов.

В середине января вышла версия 4.0. В этой публикации мы постараемся обединить известные данные и нововведения.

Rough.js работает как с <a href="https://developer.mozilla.org/en-US/docs/Web/SVG" target="_blank" rel="noopener noreferrer nofollow">Canvas</a>, так и с <a href="https://developer.mozilla.org/en-US/docs/Web/SVG" target="_blank" rel="noopener noreferrer nofollow">SVG</a>.
Rough.js работает как с Canvas, так и с SVG.

Установка

Устанавливаем Rough.js из менеджера пакетов:

        npm install --save roughjs
    

Используем в коде:

        import rough from 'roughjs';
    

Примеры использования

Полная документация API Rough.js доступна на Github. Приведём сначала несколько простых примеров графических элементов и кода, с помощью которого они были получены.

Прямоугольник

Rough.js: как заставить компьютер рисовать «от руки»
        const rc = rough.canvas(document.getElementById('canvas'));
rc.rectangle(10, 10, 200, 200); // x, y, ширина, высота
    

Для SVG-варианта:

        const rc = rough.svg(svg);
let node = rc.rectangle(10, 10, 200, 200); // x, y, ширина, высота
svg.appendChild(node);
    

Линии и эллипсы

Rough.js: как заставить компьютер рисовать «от руки»
        rc.circle(80, 120, 50); // centerX, centerY, диаметр
rc.ellipse(300, 100, 150, 80); // centerX, centerY, ширина, высота
rc.line(80, 120, 300, 100); // x1, y1, x2, y2
    

Заполнение

Rough.js: как заставить компьютер рисовать «от руки»
        rc.circle(50, 50, 80, { fill: 'red' }); // fill with red hachure
rc.rectangle(120, 15, 80, 80, { fill: 'red' });
rc.circle(50, 150, 80, {
  fill: "rgb(10,150,10)",
  fillWeight: 3 // thicker lines for hachure
});
rc.rectangle(220, 15, 80, 80, {
  fill: 'red',
  hachureAngle: 60, // angle of hachure,
  hachureGap: 8
});
rc.rectangle(120, 105, 80, 80, {
  fill: 'rgba(255,0,200,0.2)',
  fillStyle: 'solid' // solid fill
});
    

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

Rough.js: как заставить компьютер рисовать «от руки»

Стиль зарисовки

Rough.js: как заставить компьютер рисовать «от руки»
        rc.rectangle(15, 15, 80, 80, { roughness: 0.5, fill: 'red' });
rc.rectangle(120, 15, 80, 80, { roughness: 2.8, fill: 'blue' });
rc.rectangle(220, 15, 80, 80, { bowing: 6, stroke: 'green', strokeWidth: 3 });
    

Рисование в формате SVG-path

Rough.js: как заставить компьютер рисовать «от руки»
        rc.path('M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z', { fill: 'green' });
rc.path('M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z', { fill: 'purple' });
rc.path('M80 230 A 45 45, 0, 0, 1, 125 275 L 125 230 Z', { fill: 'red' });
rc.path('M230 230 A 45 45, 0, 1, 1, 275 275 L 275 230 Z', { fill: 'blue' });
    

Примеры посложнее

Rough.js: как заставить компьютер рисовать «от руки»
        const rc = rough.canvas(document.getElementById('canvas'));

//line and rectangle
rc.rectangle(10, 10, 100, 100);
rc.rectangle(140, 10, 100, 100, {
  fill: 'rgba(255,0,0,0.2)',
  fillStyle: 'solid',
  roughness: 2
});
rc.rectangle(10, 130, 100, 100, {
  fill: 'red',
  stroke: 'blue',
  hachureAngle: 60,
  hachureGap: 10,
  fillWeight: 5,
  strokeWidth: 5
});

// ellipse and circle
rc.ellipse(350, 50, 150, 80);
rc.ellipse(610, 50, 150, 80, {
  fill: 'blue'
});
rc.circle(480, 50, 80, {
  stroke: 'red', strokeWidth: 2,
  fill: 'rgba(0,255,0,0.3)', fillStyle: 'solid'
});

//overlapping circles
rc.circle(480, 150, 80, {
  stroke: 'red', strokeWidth: 4,
  fill: 'rgba(0,255,0,1)', fillWeight: 4, hachureGap: 6
});
rc.circle(530, 150, 80, {
  stroke: 'blue', strokeWidth: 4,
  fill: 'rgba(255,255,0,1)', fillWeight: 4, hachureGap: 6
});

// linearPath and polygon
rc.linearPath([[690, 10], [790, 20], [750, 120], [690, 100]], {
  roughness: 0.7,
  stroke: 'red', strokeWidth: 4
});
rc.polygon([[690, 130], [790, 140], [750, 240], [690, 220]]);
rc.polygon([[690, 250], [790, 260], [750, 360], [690, 340]], {
  stroke: 'red', strokeWidth: 4,
  fill: 'rgba(0,0,255,0.2)', fillStyle: 'solid'
});
rc.polygon([[690, 370], [790, 385], [750, 480], [690, 460]], {
  stroke: 'red',
  hachureAngle: 65,
  fill: 'rgba(0,0,255,0.6)'
});

// arcs
rc.arc(350, 200, 200, 180, Math.PI, Math.PI * 1.6);
rc.arc(350, 300, 200, 180, Math.PI, Math.PI * 1.6, true);
rc.arc(350, 300, 200, 180, 0, Math.PI / 2, true, {
  stroke: 'red', strokeWidth: 4,
  fill: 'rgba(255,255,0,0.4)', fillStyle: 'solid'
});
rc.arc(350, 300, 200, 180, Math.PI / 2, Math.PI, true, {
  stroke: 'blue', strokeWidth: 2,
  fill: 'rgba(255,0,255,0.4)'
});

// draw sine curve
let points = [];
for (let i = 0; i < 20; i++) {
  // 4pi - 400px
  let x = (400 / 20) * i + 10;
  let xdeg = (Math.PI / 100) * x;
  let y = Math.round(Math.sin(xdeg) * 90) + 500;
  points.push([x, y]);
}
rc.curve(points, {
  roughness: 1.2, stroke: 'red', strokeWidth: 3
});
    
Rough.js: как заставить компьютер рисовать «от руки»
        const rc = rough.canvas(document.getElementById('canvas'), {
  async: true,
  options: {
	simplification: 0.2, roughness: 0.65
  }
});
const width = 960, height = 500;
const projection = d3.geo.albersUsa().scale(1070).translate([width / 2, height / 2]);
const path = d3.geo.path().projection(projection);
const randomColor = () => {
  let r = `rgb(${Math.round(Math.random() * 255)}, ${Math.round(Math.random() * 255)}, ${Math.round(Math.random() * 255)})`;
  return r;
}
const randomAngle = () => {
  return (Math.random() > 0.5 ? -1 : 1) * (1 + Math.random() * 88);
}
const randomStyle = () => {
  return (Math.random() > 0.5 ? 'solid' : '');
}
d3.json("./us.json", async (error, us) => {
  if (error) throw error;
  let topo = topojson.feature(us, us.objects.states).features;
  for (let feature of topo) {
	rc.path(path(feature), {
	  fill: randomColor(),
	  fillStyle: randomStyle(),
	  hachureAngle: randomAngle()
	});
  }
});
    
Rough.js: как заставить компьютер рисовать «от руки»
        var canvas = document.getElementById('canvas');
const rc = rough.canvas(canvas, {
  options: {
	fill: "blue",
	roughness: 0.8,
	bowing: 0.7
  }
});

var context = canvas.getContext("2d");
var margin = { top: 20, right: 20, bottom: 30, left: 40 },
  width = canvas.width - margin.left - margin.right,
  height = canvas.height - margin.top - margin.bottom;
var x = d3.scaleBand()
  .rangeRound([0, width])
  .padding(0.1);
var y = d3.scaleLinear()
  .rangeRound([height, 0]);
context.translate(margin.left, margin.top);

d3.tsv("data.tsv", function (d) {
  d.frequency = +d.frequency;
  return d;
}, function (error, data) {
  if (error) throw error;

  x.domain(data.map(function (d) { return d.letter; }));
  y.domain([0, d3.max(data, function (d) { return d.frequency; })]);

  var yTickCount = 10,
	yTicks = y.ticks(yTickCount),
	yTickFormat = y.tickFormat(yTickCount, "%");

  data.forEach(function (d) {
	rc.rectangle(x(d.letter), y(d.frequency), x.bandwidth(), height - y(d.frequency));
  });


  context.beginPath();
  x.domain().forEach(function (d) {
	context.moveTo(x(d) + x.bandwidth() / 2, height);
	context.lineTo(x(d) + x.bandwidth() / 2, height + 6);
  });
  context.strokeStyle = "black";
  context.stroke();

  context.textAlign = "center";
  context.textBaseline = "top";
  x.domain().forEach(function (d) {
	context.fillText(d, x(d) + x.bandwidth() / 2, height + 6);
  });

  context.beginPath();
  yTicks.forEach(function (d) {
	context.moveTo(0, y(d) + 0.5);
	context.lineTo(-6, y(d) + 0.5);
  });
  context.strokeStyle = "black";
  context.stroke();

  context.textAlign = "right";
  context.textBaseline = "middle";
  yTicks.forEach(function (d) {
	context.fillText(yTickFormat(d), -9, y(d));
  });

  context.beginPath();
  context.moveTo(-6.5, 0 + 0.5);
  context.lineTo(0.5, 0 + 0.5);
  context.lineTo(0.5, height + 0.5);
  context.lineTo(-6.5, height + 0.5);
  context.strokeStyle = "black";
  context.stroke();

  context.save();
  context.rotate(-Math.PI / 2);
  context.textAlign = "right";
  context.textBaseline = "top";
  context.font = "bold 10px sans-serif";
  context.fillText("Frequency", -10, 10);
  context.restore();
});
    

Что нового в версии 4.0?

Задание поля случайных чисел. Rough.js вычисляет фигуры, генерируя много случайных смещений. В результате каждый рендер создаёт уникальную нарисованную от руки форму. Однако в некоторых случаях такое поведение нежелательно. Бывает необходимо, например, сохранить форму штриховки при разных размерах фигуры. API теперь позволяет пользователю передавать начальное значение seed, которое используется для генерации предсказуемой последовательности случайных чисел. Параметр является опциональным:

Rough.js: как заставить компьютер рисовать «от руки»
        const seed = rough.newSeed();
rc.rectangle(10, 10, 200, 200, { fill: 'red', seed });
rc.rectangle(240, 10, 250, 250, { fill: 'blue', seed });
    

Новый алгоритм штриховки. Как описано выше, Rough.js может заполнить фигуру штриховыми линиями. Старый алгоритм был адаптацией кода Handy с небольшими изменениями. Код не очень хорошо обрабатывал вогнутые формы под некоторыми углами штриховки. Могли создаваться такие некорректные ситуации:

Rough.js: как заставить компьютер рисовать «от руки»

Новый алгоритм основан на алгоритме растеризации Scan-line. При создании линий штриховки строки обрабатываются пошагово с указанным межстроковым расстоянием hachureGap. Этот алгоритм, однако, предназначен для горизонтальных линий сканирования. Чтобы работать с различными углами штриховки, сначала производится поворот фигуры на угол hachureAngle и заполнение горизонтальными линиями. Потом найденные горизонтальные линии поворачиваются обратно на угол -hachureAngle, как показано на рисунке ниже.

Rough.js: как заставить компьютер рисовать «от руки»

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

Rough.js: как заставить компьютер рисовать «от руки»

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

Rough.js: как заставить компьютер рисовать «от руки»

Фигура без контура

Другой часто запрашиваемой функцией было добавление возможности рисовать фигуры без контура. Этого можно было бы достигнуть путём установки прозрачного цвета обводки. Однако наличие контура раньше учитывалось при заполнении основной фигуры. Теперь эти инструкции разделены, и для контура можно просто указать значение none.

Rough.js: как заставить компьютер рисовать «от руки»
        rc.rectangle(240, 10, 250, 250, { stroke: 'none', fill: 'blue' });
    

Заключение

Напоследок несколько примеров использования библиотеки:

  1. Wired elements – набор элементов на основе Rough.js.
  2. Змейка из элементов графики Rough.js
  3. Semiotic – визуализация данных для React, использующая Rough.js для рендеринга.
  4. Плагин Rough.js для Leaflet – библиотека JavaScript с открытым исходным кодом для мобильных интерактивных карт.
  5. React Bindings для Rough.js
  6. Инструмент для создания графиков, есть возможность использовать Rough.js

Источники

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Golang разработчик (middle)
от 230000 RUB до 300000 RUB
DevOps
Санкт-Петербург, от 150000 RUB до 400000 RUB

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ