对于绘图应用程序,我将鼠标移动坐标保存到数组中,然后使用 lineTo 绘制它们。结果线不平滑。如何在所有收集的点之间生成一条曲线?
我用谷歌搜索,但只找到了 3 个用于绘制线条的函数:对于 2 个样本点,只需使用 lineTo
。对于 3 个样本点 quadraticCurveTo
,对于 4 个样本点,bezierCurveTo
。
(我尝试为数组中的每 4 个点绘制一个 bezierCurveTo
,但这会导致每 4 个样本点出现扭结,而不是连续平滑曲线。)
如何编写一个函数来绘制具有 5 个及以上样本点的平滑曲线?
将后续样本点与不相交的“curveTo”类型函数连接在一起的问题是曲线相遇的地方不平滑。这是因为两条曲线共享一个端点,但受完全不相交的控制点的影响。一种解决方案是“弯曲到”接下来的 2 个后续采样点之间的中点。使用这些新的插值点连接曲线可以在端点处实现平滑过渡(一次迭代的端点成为下一次迭代的控制点。)换句话说,两条不相交的曲线现在有更多的共同点。
该解决方案摘自《Foundation ActionScript 3.0 Animation: Making things move》一书。 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);
有点晚了,但为了记录。
您可以通过使用 cardinal splines(又名规范样条)绘制通过点的平滑曲线来实现平滑线。
我为画布制作了这个功能 - 它分为三个功能以增加多功能性。主包装函数如下所示:
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");函数 drawCurve(ctx, ptsa, 张力, isClosed, numOfSegments, showPoints) { ctx.beginPath(); drawLines(ctx, getCurvePoints(ptsa, 张力, isClosed, numOfSegments)); if (showPoints) { ctx.beginPath(); for(var i=0;i
这导致:
https://i.stack.imgur.com/xWCkp.jpg
您可以轻松地扩展画布,以便您可以这样调用它:
ctx.drawCurve(myPoints);
将以下内容添加到 javascript:
if (CanvasRenderingContext2D != 'undefined') {
CanvasRenderingContext2D.prototype.drawCurve =
function(pts, tension, isClosed, numOfSegments, showPoints) {
drawCurve(this, pts, tension, isClosed, numOfSegments, showPoints)}
}
您可以在 NPM (npm i cardinal-spline-js
) 或 GitLab 上找到更优化的版本。
第一个答案不会通过所有点。该图将准确地通过所有点,并且将是一个完美的曲线,其中点为 [{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_x2 = (x_mid + points[i+1].x) / 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);
}
.getContext('2d')
我还需要什么
ctx.stroke()
我决定添加,而不是将我的解决方案发布到另一个帖子。以下是我构建的解决方案,可能并不完美,但到目前为止输出良好。
重要提示:它将通过所有点!
如果你有任何想法,让它变得更好,请分享给我。谢谢。
以下是前后对比:
https://i.stack.imgur.com/M9adH.png
将此代码保存为 HTML 以进行测试。
作为 Daniel Howard points out,Rob Spencer 描述了您对 http://scaledinnovation.com/analytics/splines/aboutSplines.html 的要求。
这是一个交互式演示:http://jsbin.com/ApitIxo/2/
这是一个片段,以防 jsbin 出现故障。
我发现这很好用
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();
}
试试 KineticJS - 你可以用点数组定义样条线。这是一个例子:
旧网址:http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/
令人难以置信的迟到但受到霍曼出色简单答案的启发,请允许我发布一个更通用的解决方案(一般意义上,霍曼的解决方案在少于 3 个顶点的点数组上崩溃):
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);
}
这段代码对我来说是完美的:
this.context.beginPath();
this.context.moveTo(data[0].x, data[0].y);
for (let i = 1; i < data.length; i++) {
this.context.bezierCurveTo(
data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
data[i - 1].y,
data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
data[i].y,
data[i].x,
data[i].y);
}
你有正确的平滑线和正确的端点注意! (y = "画布高度" - y);
对原始问题的回答略有不同;
如果有人想画一个形状:
由一系列点描述的
线在点处有一条小曲线
这条线不一定要穿过这些点(即稍微穿过它们的“内部”)
然后希望我的以下功能可以帮助
你好
我很欣赏 user1693593 的解决方案:Hermite 多项式似乎是控制将要绘制的内容的最佳方法,并且从数学的角度来看也是最令人满意的。这个话题似乎已经关闭了很长时间,但可能像我这样的一些后来者仍然对它感兴趣。我一直在寻找一个免费的交互式绘图构建器,它可以让我存储曲线并在其他任何地方重复使用,但在网络上没有找到这种东西 :所以我从维基百科自行制作user1693593 提到的来源。在这里很难解释它是如何工作的, 了解它是否值得的最好方法是查看 https://sites.google.com/view/divertissements/accueil/splines。
为了添加到 K3N 的基数样条方法并可能解决 TJ Crowder 对曲线在误导性位置“倾斜”的担忧,我在 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;
}
这有效地在每对连续点之间创建了一个(不可见的)边界框,并确保曲线保持在这个边界框内 - 即。如果曲线上的一个点在两个点的上方/下方/左侧/右侧,它会将其位置更改为在框内。这里使用了中点,但这可以改进,也许使用线性插值。
如果您想通过 n 个点确定曲线方程,那么以下代码将为您提供 n-1 次多项式的系数并将这些系数保存到 coefficients[]
数组(从常数项开始)。 x 坐标不必按顺序排列。这是 Lagrange polynomial 的示例。
var xPoints=[2,4,3,6,7,10]; //example coordinates
var yPoints=[2,5,-2,0,2,8];
var coefficients=[];
for (var m=0; m<xPoints.length; m++) coefficients[m]=0;
for (var m=0; m<xPoints.length; m++) {
var newCoefficients=[];
for (var nc=0; nc<xPoints.length; nc++) newCoefficients[nc]=0;
if (m>0) {
newCoefficients[0]=-xPoints[0]/(xPoints[m]-xPoints[0]);
newCoefficients[1]=1/(xPoints[m]-xPoints[0]);
} else {
newCoefficients[0]=-xPoints[1]/(xPoints[m]-xPoints[1]);
newCoefficients[1]=1/(xPoints[m]-xPoints[1]);
}
var startIndex=1;
if (m==0) startIndex=2;
for (var n=startIndex; n<xPoints.length; n++) {
if (m==n) continue;
for (var nc=xPoints.length-1; nc>=1; nc--) {
newCoefficients[nc]=newCoefficients[nc]*(-xPoints[n]/(xPoints[m]-xPoints[n]))+newCoefficients[nc-1]/(xPoints[m]-xPoints[n]);
}
newCoefficients[0]=newCoefficients[0]*(-xPoints[n]/(xPoints[m]-xPoints[n]));
}
for (var nc=0; nc<xPoints.length; nc++) coefficients[nc]+=yPoints[m]*newCoefficients[nc];
}
不定期副业成功案例分享