Canvas advanced — как нарисовать плавную кривую?

внешний интерфейс алгоритм Canvas

Сводная информация

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

Что ж, я считаю, что это можно сделать с помощью всего нескольких десятков строк кода для детей, знакомых с холстом.Следующая демонстрация является простым примером:

<!DOCTYPE html>
<html>
<head>
	<title>Sketchpad demo</title>
	<style type="text/css">
		canvas {
			border: 1px blue solid; 
		}
	</style>
</head>
<body>
	<canvas id="canvas" width="800" height="500"></canvas>
	<script type="text/javascript">
		let isDown = false;
		let beginPoint = null;
		const canvas = document.querySelector('#canvas');
		const ctx = canvas.getContext('2d');

		// 设置线条颜色
		ctx.strokeStyle = 'red';
		ctx.lineWidth = 1;
		ctx.lineJoin = 'round';
		ctx.lineCap = 'round';

		canvas.addEventListener('mousedown', down, false);
		canvas.addEventListener('mousemove', move, false);
		canvas.addEventListener('mouseup', up, false);
		canvas.addEventListener('mouseout', up, false);

		function down(evt) {
			isDown = true;
			beginPoint = getPos(evt);
		}

		function move(evt) {
			if (!isDown) return;
			const endPoint = getPos(evt);
			drawLine(beginPoint, endPoint);
			beginPoint = endPoint;
		}

		function up(evt) {
			if (!isDown) return;
			
			const endPoint = getPos(evt);
			drawLine(beginPoint, endPoint);

			beginPoint = null;
			isDown = false;
		}

		function getPos(evt) {
			return {
				x: evt.clientX,
				y: evt.clientY
			}
		}

		function drawLine(beginPoint, endPoint) {
			ctx.beginPath();
			ctx.moveTo(beginPoint.x, beginPoint.y);
			ctx.lineTo(endPoint.x, endPoint.y);
			ctx.stroke();
			ctx.closePath();
		}
	</script>
</body>
</html>

Логика его реализации также очень проста:

  1. Мы в основном отслеживаем три события на холсте холста:mousedown,mouseupа такжеmousemove, а также создаемisDownПеременная;
  2. Когда пользователь нажимает мышь (mousedown, т.е. начать писать) будетisDownустановлен вtrue, при падении мыши (mouseup), установите его наfalse, преимущество этого заключается в том, что он может определить, находится ли пользователь в настоящее время в состоянии рисования;
  3. пройти черезmousemoveСобытие непрерывно собирает точки координат, через которые проходит мышь, тогда и только тогда, когдаisDownдляtrue(то есть в состоянии записи) передать текущую точку через холстlineToМетод соединяется и рисует с предыдущей точкой;

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

howToDrawLineSmoothly_6

Почему это так?

анализ проблемы

Основными причинами этого явления являются:

  • Мы основаны на холстеlineToЕсли метод соединяет точки, соединение между двумя соседними точками представляет собой прямую линию, а не кривую, поэтому то, что нарисовано таким образом, является ломаной линией;
    howToDrawLineSmoothly_1.png
  • Ограничено парой браузеровmousemoveЧастота сбора событий, мы все знаем, что вmousemove, браузер собирает координаты текущей мыши через каждые короткие промежутки времени, поэтому чем быстрее движется мышь, тем дальше расстояние между двумя соседними собранными точками, поэтому «тем очевиднее смысл ломаных линий»;

Как нарисовать плавную кривую?

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

Квадратичная кривая Безье

quadraticCurveTo(cp1x, cp1y, x, y)

передачаquadraticCurveToМетод требует четыре параметра,cp1x,cp1yописывает контрольные точки, аx,yявляется конечной точкой кривой:

howToDrawLineSmoothly_7

Более подробную информацию можно перенестиMDN

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

Есть изящный алгоритм, который помогает нам получить эту информацию.

Алгоритм получения квадратичных ключевых точек Безье

Этот алгоритм понять не сложно, тут я прямо привожу пример:

  1. Предположим, мы собираем в общей сложности 6 координат мыши на одном рисунке, которыеA, B, C, D, E, F;
  2. занять фронтA, B, Cтри балла, рассчитатьBа такжеCсерединаB1Aв качестве отправной точки,Bявляется контрольной точкой,B1в качестве конечной точки используйтеquadraticCurveToНарисуйте сегмент квадратичной кривой Безье;
    howToDrawLineSmoothly_2
  3. Далее вычисляемCа такжеDсередина точкиC1B1в качестве отправной точки,Cдля пункта управления,C1Продолжайте рисовать кривую для конечной точки;
    howToDrawLineSmoothly_3
  4. И так далее и так далее продолжаем рисовать, когда будет достигнута последняя точкаF, затем сDа такжеEсерединаD1в качестве отправной точки сEявляется контрольной точкой,FЗавершите кривую Безье для конечной точки.
    howToDrawLineSmoothly_4

Хорошо, алгоритм такой, далее мы снова обновим существующий код на основе этого алгоритма:

let isDown = false;
let points = [];
let beginPoint = null;
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');

// 设置线条颜色
ctx.strokeStyle = 'red';
ctx.lineWidth = 1;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';

canvas.addEventListener('mousedown', down, false);
canvas.addEventListener('mousemove', move, false);
canvas.addEventListener('mouseup', up, false);
canvas.addEventListener('mouseout', up, false);

function down(evt) {
    isDown = true;
    const { x, y } = getPos(evt);
    points.push({x, y});
    beginPoint = {x, y};
}

function move(evt) {
    if (!isDown) return;

    const { x, y } = getPos(evt);
    points.push({x, y});

    if (points.length > 3) {
        const lastTwoPoints = points.slice(-2);
        const controlPoint = lastTwoPoints[0];
        const endPoint = {
            x: (lastTwoPoints[0].x + lastTwoPoints[1].x) / 2,
            y: (lastTwoPoints[0].y + lastTwoPoints[1].y) / 2,
        }
        drawLine(beginPoint, controlPoint, endPoint);
        beginPoint = endPoint;
    }
}

function up(evt) {
    if (!isDown) return;
    const { x, y } = getPos(evt);
    points.push({x, y});

    if (points.length > 3) {
        const lastTwoPoints = points.slice(-2);
        const controlPoint = lastTwoPoints[0];
        const endPoint = lastTwoPoints[1];
        drawLine(beginPoint, controlPoint, endPoint);
    }
    beginPoint = null;
    isDown = false;
    points = [];
}

function getPos(evt) {
    return {
        x: evt.clientX,
        y: evt.clientY
    }
}

function drawLine(beginPoint, controlPoint, endPoint) {
    ctx.beginPath();
    ctx.moveTo(beginPoint.x, beginPoint.y);
    ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y);
    ctx.stroke();
    ctx.closePath();
}

На основе оригинала мы создали переменнуюpointsперед сохранениемmousemoveТочка, через которую проходит мышь в событии, по алгоритму видно, что для построения квадратичной кривой Безье необходимо как минимум 3 точки, поэтому нам осталось толькоpointsРозыгрыш начинается, когда количество очков больше 3. Дальнейшая обработка аналогична алгоритму и здесь повторяться не будет.

После обновления кода наша кривая также стала намного более гладкой, как показано на следующем рисунке:

howToDrawLineSmoothly_5

Эта статья закончилась, надеюсь, вы хорошо проведете время, «рисуя» на доске для рисования на холсте ~ увидимся в следующий раз :)

Заинтересованная детская обувь можеткликните сюдаСледите за моим блогом, любые свежие и интересные посты в блоге будут публиковаться здесь как можно скорее~