Как рисовать гладкую кривую через N точек с помощью javascript HTML5 canvas?
Для приложения рисования я сохраняю координаты перемещения мыши в массив, а затем рисую их с помощью lineTo. Полученная строка не является гладкой. Как я могу создать одну кривую между всеми собранными точками?
У меня есть googled, но я нашел только 3 функции для рисования линий: для 2 точек выборки просто используйте lineTo. Для трех точек выборки quadraticCurveTo, для 4 точек выборки, bezierCurveTo.
(Я попытался рисовать bezierCurveTo для каждых 4 точек в массиве, но это приводит к перегибу каждые 4 точки выборки вместо непрерывной гладкой кривой.)
Как написать функцию, чтобы нарисовать гладкую кривую с 5 точками выборки и дальше?
Ответы
Ответ 1
Проблема с присоединением последующих точек выборки вместе с непересекающимися функциями типа "curveTo" заключается в том, что там, где кривые встречаются, не являются гладкими. Это связано с тем, что две кривые имеют конечную точку, но на них влияют полностью непересекающиеся контрольные точки. Одним из решений является "кривая" между серединами между следующими двумя последующими точками выборки. Объединение кривых с использованием этих новых интерполированных точек дает плавный переход в конечных точках (то, что является конечной точкой для одной итерации, становится контрольной точкой для следующей итерации). Другими словами, две несвязанные кривые имеют гораздо больше общего.
Это решение было извлечено из книги "Фундаментальная анимация ActionScript 3.0: перемещение вещей". p.95 - методы рендеринга: создание нескольких кривых.
Примечание: это решение фактически не рисует каждую из точек, что было названием моего вопроса (скорее, оно аппроксимирует кривую через точки выборки, но никогда не проходит через точки выборки), но для моих целей (рисунок приложение), это достаточно хорошо для меня и визуально вы не можете отличить. Существует решение пройти все точки выборки, но это намного сложнее (см. http://www.cartogrammar.com/blog/actionscript-curves-update/)
Вот код чертежа для метода аппроксимации:
// move to the first point
ctx.moveTo(points[0].x, points[0].y);
for (i = 1; i < points.length - 2; i ++)
{
var xc = (points[i].x + points[i + 1].x) / 2;
var yc = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
}
// curve through the last two points
ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x,points[i+1].y);
Ответ 2
Немного поздно, но для записи.
Вы можете добиться плавных линий, используя кардинальные сплайны (ака канонический сплайн), чтобы нарисовать гладкие кривые, проходящие через точки.
Я сделал эту функцию для холста - он разделился на три функции, чтобы увеличить универсальность. Основная функция обертки выглядит следующим образом:
function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) {
showPoints = showPoints ? showPoints : false;
ctx.beginPath();
drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments));
if (showPoints) {
ctx.stroke();
ctx.beginPath();
for(var i=0;i<ptsa.length-1;i+=2)
ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4);
}
}
Чтобы нарисовать кривую, массив с x, y указывает в порядке: x1,y1, x2,y2, ...xn,yn
.
Используйте его следующим образом:
var myPoints = [10,10, 40,30, 100,10]; //minimum two points
var tension = 1;
drawCurve(ctx, myPoints); //default tension=0.5
drawCurve(ctx, myPoints, tension);
Вышеупомянутая функция вызывает две подфункции, одну для вычисления сглаженных точек. Это возвращает массив с новыми точками - это основная функция, которая вычисляет сглаженные точки:
function getCurvePoints(pts, tension, isClosed, numOfSegments) {
// use input value if provided, or use a default value
tension = (typeof tension != 'undefined') ? tension : 0.5;
isClosed = isClosed ? isClosed : false;
numOfSegments = numOfSegments ? numOfSegments : 16;
var _pts = [], res = [], // clone array
x, y, // our x,y coords
t1x, t2x, t1y, t2y, // tension vectors
c1, c2, c3, c4, // cardinal points
st, t, i; // steps based on num. of segments
// clone array so we don't change the original
//
_pts = pts.slice(0);
// The algorithm require a previous and next point to the actual point array.
// Check if we will draw closed or open curve.
// If closed, copy end points to beginning and first points to end
// If open, duplicate first points to befinning, end points to end
if (isClosed) {
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.push(pts[0]);
_pts.push(pts[1]);
}
else {
_pts.unshift(pts[1]); //copy 1. point and insert at beginning
_pts.unshift(pts[0]);
_pts.push(pts[pts.length - 2]); //copy last point and append
_pts.push(pts[pts.length - 1]);
}
// ok, lets start..
// 1. loop goes through point array
// 2. loop goes through each segment between the 2 pts + 1e point before and after
for (i=2; i < (_pts.length - 4); i+=2) {
for (t=0; t <= numOfSegments; t++) {
// calc tension vectors
t1x = (_pts[i+2] - _pts[i-2]) * tension;
t2x = (_pts[i+4] - _pts[i]) * tension;
t1y = (_pts[i+3] - _pts[i-1]) * tension;
t2y = (_pts[i+5] - _pts[i+1]) * tension;
// calc step
st = t / numOfSegments;
// calc cardinals
c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1;
c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2);
c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st;
c4 = Math.pow(st, 3) - Math.pow(st, 2);
// calc x and y cords with common control vectors
x = c1 * _pts[i] + c2 * _pts[i+2] + c3 * t1x + c4 * t2x;
y = c1 * _pts[i+1] + c2 * _pts[i+3] + c3 * t1y + c4 * t2y;
//store points in array
res.push(x);
res.push(y);
}
}
return res;
}
И фактически нарисовать точки в виде сглаженной кривой (или любых других сегментированных строк, если у вас есть массив x, y):
function drawLines(ctx, pts) {
ctx.moveTo(pts[0], pts[1]);
for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]);
}
var ctx = document.getElementById("c").getContext("2d");
function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) {
ctx.beginPath();
drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments));
if (showPoints) {
ctx.beginPath();
for(var i=0;i<ptsa.length-1;i+=2)
ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4);
}
ctx.stroke();
}
var myPoints = [10,10, 40,30, 100,10, 200, 100, 200, 50, 250, 120]; //minimum two points
var tension = 1;
drawCurve(ctx, myPoints); //default tension=0.5
drawCurve(ctx, myPoints, tension);
function getCurvePoints(pts, tension, isClosed, numOfSegments) {
// use input value if provided, or use a default value
tension = (typeof tension != 'undefined') ? tension : 0.5;
isClosed = isClosed ? isClosed : false;
numOfSegments = numOfSegments ? numOfSegments : 16;
var _pts = [], res = [], // clone array
x, y, // our x,y coords
t1x, t2x, t1y, t2y, // tension vectors
c1, c2, c3, c4, // cardinal points
st, t, i; // steps based on num. of segments
// clone array so we don't change the original
//
_pts = pts.slice(0);
// The algorithm require a previous and next point to the actual point array.
// Check if we will draw closed or open curve.
// If closed, copy end points to beginning and first points to end
// If open, duplicate first points to befinning, end points to end
if (isClosed) {
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.push(pts[0]);
_pts.push(pts[1]);
}
else {
_pts.unshift(pts[1]); //copy 1. point and insert at beginning
_pts.unshift(pts[0]);
_pts.push(pts[pts.length - 2]); //copy last point and append
_pts.push(pts[pts.length - 1]);
}
// ok, lets start..
// 1. loop goes through point array
// 2. loop goes through each segment between the 2 pts + 1e point before and after
for (i=2; i < (_pts.length - 4); i+=2) {
for (t=0; t <= numOfSegments; t++) {
// calc tension vectors
t1x = (_pts[i+2] - _pts[i-2]) * tension;
t2x = (_pts[i+4] - _pts[i]) * tension;
t1y = (_pts[i+3] - _pts[i-1]) * tension;
t2y = (_pts[i+5] - _pts[i+1]) * tension;
// calc step
st = t / numOfSegments;
// calc cardinals
c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1;
c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2);
c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st;
c4 = Math.pow(st, 3) - Math.pow(st, 2);
// calc x and y cords with common control vectors
x = c1 * _pts[i] + c2 * _pts[i+2] + c3 * t1x + c4 * t2x;
y = c1 * _pts[i+1] + c2 * _pts[i+3] + c3 * t1y + c4 * t2y;
//store points in array
res.push(x);
res.push(y);
}
}
return res;
}
function drawLines(ctx, pts) {
ctx.moveTo(pts[0], pts[1]);
for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]);
}
canvas { border: 1px solid red; }
<canvas id="c"><canvas>
Ответ 3
Первый ответ не пройдет через все точки. Этот граф будет точно проходить через все точки и будет кривой префекта с точками в виде точек как [{x:, y:}] n таких точек.
var points = [{x:1,y:1},{x:2,y:3},{x:3,y:4},{x:4,y:2},{x:5,y:6}] //took 5 example points
ctx.moveTo((points[0].x), points[0].y);
for(var i = 0; i < points.length-1; i ++)
{
var x_mid = (points[i].x + points[i+1].x) / 2;
var y_mid = (points[i].y + points[i+1].y) / 2;
var cp_x1 = (x_mid + points[i].x) / 2;
var cp_y1 = (y_mid + points[i].y) / 2;
var cp_x2 = (x_mid + points[i+1].x) / 2;
var cp_y2 = (y_mid + points[i+1].y) / 2;
ctx.quadraticCurveTo(cp_x1,points[i].y ,x_mid, y_mid);
ctx.quadraticCurveTo(cp_x2,points[i+1].y ,points[i+1].x,points[i+1].y);
}
Ответ 4
Как Даниэль Говард указывает, Роб Спенсер описывает, что вы хотите в http://scaledinnovation.com/analytics/splines/aboutSplines.html.
Здесь интерактивная демонстрация: http://jsbin.com/ApitIxo/2/
Здесь это как фрагмент в случае, если jsbin не работает.
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Demo smooth connection</title>
</head>
<body>
<div id="display">
Click to build a smooth path.
(See Rob Spencer <a href="http://scaledinnovation.com/analytics/splines/aboutSplines.html">article</a>)
<br><label><input type="checkbox" id="showPoints" checked> Show points</label>
<br><label><input type="checkbox" id="showControlLines" checked> Show control lines</label>
<br>
<label>
<input type="range" id="tension" min="-1" max="2" step=".1" value=".5" > Tension <span id="tensionvalue">(0.5)</span>
</label>
<div id="mouse"></div>
</div>
<canvas id="canvas"></canvas>
<style>
html { position: relative; height: 100%; width: 100%; }
body { position: absolute; left: 0; right: 0; top: 0; bottom: 0; }
canvas { outline: 1px solid red; }
#display { position: fixed; margin: 8px; background: white; z-index: 1; }
</style>
<script>
function update() {
$("tensionvalue").innerHTML="("+$("tension").value+")";
drawSplines();
}
$("showPoints").onchange = $("showControlLines").onchange = $("tension").onchange = update;
// utility function
function $(id){ return document.getElementById(id); }
var canvas=$("canvas"), ctx=canvas.getContext("2d");
function setCanvasSize() {
canvas.width = parseInt(window.getComputedStyle(document.body).width);
canvas.height = parseInt(window.getComputedStyle(document.body).height);
}
window.onload = window.onresize = setCanvasSize();
function mousePositionOnCanvas(e) {
var el=e.target, c=el;
var scaleX = c.width/c.offsetWidth || 1;
var scaleY = c.height/c.offsetHeight || 1;
if (!isNaN(e.offsetX))
return { x:e.offsetX*scaleX, y:e.offsetY*scaleY };
var x=e.pageX, y=e.pageY;
do {
x -= el.offsetLeft;
y -= el.offsetTop;
el = el.offsetParent;
} while (el);
return { x: x*scaleX, y: y*scaleY };
}
canvas.onclick = function(e){
var p = mousePositionOnCanvas(e);
addSplinePoint(p.x, p.y);
};
function drawPoint(x,y,color){
ctx.save();
ctx.fillStyle=color;
ctx.beginPath();
ctx.arc(x,y,3,0,2*Math.PI);
ctx.fill()
ctx.restore();
}
canvas.onmousemove = function(e) {
var p = mousePositionOnCanvas(e);
$("mouse").innerHTML = p.x+","+p.y;
};
var pts=[]; // a list of x and ys
// given an array of x,y's, return distance between any two,
// note that i and j are indexes to the points, not directly into the array.
function dista(arr, i, j) {
return Math.sqrt(Math.pow(arr[2*i]-arr[2*j], 2) + Math.pow(arr[2*i+1]-arr[2*j+1], 2));
}
// return vector from i to j where i and j are indexes pointing into an array of points.
function va(arr, i, j){
return [arr[2*j]-arr[2*i], arr[2*j+1]-arr[2*i+1]]
}
function ctlpts(x1,y1,x2,y2,x3,y3) {
var t = $("tension").value;
var v = va(arguments, 0, 2);
var d01 = dista(arguments, 0, 1);
var d12 = dista(arguments, 1, 2);
var d012 = d01 + d12;
return [x2 - v[0] * t * d01 / d012, y2 - v[1] * t * d01 / d012,
x2 + v[0] * t * d12 / d012, y2 + v[1] * t * d12 / d012 ];
}
function addSplinePoint(x, y){
pts.push(x); pts.push(y);
drawSplines();
}
function drawSplines() {
clear();
cps = []; // There will be two control points for each "middle" point, 1 ... len-2e
for (var i = 0; i < pts.length - 2; i += 1) {
cps = cps.concat(ctlpts(pts[2*i], pts[2*i+1],
pts[2*i+2], pts[2*i+3],
pts[2*i+4], pts[2*i+5]));
}
if ($("showControlLines").checked) drawControlPoints(cps);
if ($("showPoints").checked) drawPoints(pts);
drawCurvedPath(cps, pts);
}
function drawControlPoints(cps) {
for (var i = 0; i < cps.length; i += 4) {
showPt(cps[i], cps[i+1], "pink");
showPt(cps[i+2], cps[i+3], "pink");
drawLine(cps[i], cps[i+1], cps[i+2], cps[i+3], "pink");
}
}
function drawPoints(pts) {
for (var i = 0; i < pts.length; i += 2) {
showPt(pts[i], pts[i+1], "black");
}
}
function drawCurvedPath(cps, pts){
var len = pts.length / 2; // number of points
if (len < 2) return;
if (len == 2) {
ctx.beginPath();
ctx.moveTo(pts[0], pts[1]);
ctx.lineTo(pts[2], pts[3]);
ctx.stroke();
}
else {
ctx.beginPath();
ctx.moveTo(pts[0], pts[1]);
// from point 0 to point 1 is a quadratic
ctx.quadraticCurveTo(cps[0], cps[1], pts[2], pts[3]);
// for all middle points, connect with bezier
for (var i = 2; i < len-1; i += 1) {
// console.log("to", pts[2*i], pts[2*i+1]);
ctx.bezierCurveTo(
cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1],
cps[(2*(i-1))*2], cps[(2*(i-1))*2+1],
pts[i*2], pts[i*2+1]);
}
ctx.quadraticCurveTo(
cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1],
pts[i*2], pts[i*2+1]);
ctx.stroke();
}
}
function clear() {
ctx.save();
// use alpha to fade out
ctx.fillStyle = "rgba(255,255,255,.7)"; // clear screen
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.restore();
}
function showPt(x,y,fillStyle) {
ctx.save();
ctx.beginPath();
if (fillStyle) {
ctx.fillStyle = fillStyle;
}
ctx.arc(x, y, 5, 0, 2*Math.PI);
ctx.fill();
ctx.restore();
}
function drawLine(x1, y1, x2, y2, strokeStyle){
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
if (strokeStyle) {
ctx.save();
ctx.strokeStyle = strokeStyle;
ctx.stroke();
ctx.restore();
}
else {
ctx.save();
ctx.strokeStyle = "pink";
ctx.stroke();
ctx.restore();
}
}
</script>
</body>
</html>
Ответ 5
Дайте KineticJS попытку - вы можете определить сплайн с массивом точек. Вот пример:
Старый url: http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/
См. архив url: https://web.archive.org/web/20141204030628/http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/
Ответ 6
Я решил добавить, вместо того, чтобы отправлять мое решение на другое сообщение.
Ниже будет решение, которое я построю, может быть не идеальным, но пока что результат хорош.
Важно: он пройдет через все точки!
Если у вас есть идея, чтобы сделать ее лучше, пожалуйста, поделитесь со мной. Спасибо.
Вот сравнение предыдущего:
![введите описание изображения здесь]()
Сохраните этот код в HTML, чтобы проверить его.
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="1200" height="700" style="border:1px solid #d3d3d3;">Your browser does not support the HTML5 canvas tag.</canvas>
<script>
var cv = document.getElementById("myCanvas");
var ctx = cv.getContext("2d");
function gradient(a, b) {
return (b.y-a.y)/(b.x-a.x);
}
function bzCurve(points, f, t) {
//f = 0, will be straight line
//t suppose to be 1, but changing the value can control the smoothness too
if (typeof(f) == 'undefined') f = 0.3;
if (typeof(t) == 'undefined') t = 0.6;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
var m = 0;
var dx1 = 0;
var dy1 = 0;
var preP = points[0];
for (var i = 1; i < points.length; i++) {
var curP = points[i];
nexP = points[i + 1];
if (nexP) {
m = gradient(preP, nexP);
dx2 = (nexP.x - curP.x) * -f;
dy2 = dx2 * m * t;
} else {
dx2 = 0;
dy2 = 0;
}
ctx.bezierCurveTo(preP.x - dx1, preP.y - dy1, curP.x + dx2, curP.y + dy2, curP.x, curP.y);
dx1 = dx2;
dy1 = dy2;
preP = curP;
}
ctx.stroke();
}
// Generate random data
var lines = [];
var X = 10;
var t = 40; //to control width of X
for (var i = 0; i < 100; i++ ) {
Y = Math.floor((Math.random() * 300) + 50);
p = { x: X, y: Y };
lines.push(p);
X = X + t;
}
//draw straight line
ctx.beginPath();
ctx.setLineDash([5]);
ctx.lineWidth = 1;
bzCurve(lines, 0, 1);
//draw smooth line
ctx.setLineDash([0]);
ctx.lineWidth = 2;
ctx.strokeStyle = "blue";
bzCurve(lines, 0.3, 1);
</script>
</body>
</html>
Ответ 7
Я нашел, что это хорошо работает
function drawCurve(points, tension) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
var t = (tension != null) ? tension : 1;
for (var i = 0; i < points.length - 1; i++) {
var p0 = (i > 0) ? points[i - 1] : points[0];
var p1 = points[i];
var p2 = points[i + 1];
var p3 = (i != points.length - 2) ? points[i + 2] : p2;
var cp1x = p1.x + (p2.x - p0.x) / 6 * t;
var cp1y = p1.y + (p2.y - p0.y) / 6 * t;
var cp2x = p2.x - (p3.x - p1.x) / 6 * t;
var cp2y = p2.y - (p3.y - p1.y) / 6 * t;
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
}
ctx.stroke();
}
Ответ 8
Чтобы добавить к методу кардинальных сплайнов K3N и, возможно, обратиться к Т. Дж. Кроудеру, касающемуся опускания кривых в ложных местах, я вставил следующий код в функцию getCurvePoints()
, перед res.push(x);
if ((y < _pts[i+1] && y < _pts[i+3]) || (y > _pts[i+1] && y > _pts[i+3])) {
y = (_pts[i+1] + _pts[i+3]) / 2;
}
if ((x < _pts[i] && x < _pts[i+2]) || (x > _pts[i] && x > _pts[i+2])) {
x = (_pts[i] + _pts[i+2]) / 2;
}
Это эффективно создает (невидимый) ограничивающий прямоугольник между каждой парой последовательных точек и обеспечивает, чтобы кривая оставалась в пределах этого ограничивающего прямоугольника - т.е. если точка на кривой выше/ниже/слева/справа от обеих точек, она изменяет свое положение в пределах поля. Здесь используется средняя точка, но это может быть улучшено, возможно, с использованием линейной интерполяции.
Ответ 9
Невероятно поздно, но вдохновленный замечательным простым ответом Хомана, позвольте мне опубликовать более общее решение (общее в том смысле, что решение Homan падает на массивы точек с менее чем тремя вершинами):
function smooth(ctx, points)
{
if(points == undefined || points.length == 0)
{
return true;
}
if(points.length == 1)
{
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[0].x, points[0].y);
return true;
}
if(points.length == 2)
{
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
return true;
}
ctx.moveTo(points[0].x, points[0].y);
for (var i = 1; i < points.length - 2; i ++)
{
var xc = (points[i].x + points[i + 1].x) / 2;
var yc = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
}
ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x, points[i+1].y);
}