JavaScript 中的曲线相交点

Curve intersecting points in JavaScript

我希望通过上下移动像素列来修改图像,使每个列的偏移量都遵循一条曲线。

我希望曲线以某种平滑的方式与 6 个左右的点相交。我想象一下遍历图像 x 坐标并调用一个曲线函数,该函数 returns 曲线在该偏移处的 y 坐标,从而告诉我每列像素移动多少。

我研究了各种类型的曲线,但坦率地说我有点迷茫,我希望有一个现成的解决方案可以让我插入我的点坐标并吐出我需要的数据.我不太介意用什么样的曲线,只要看起来"smooth".

谁能帮我解决这个问题?

我正在使用 HTML5 和 canvas。给出的答案 看起来像我想要的那种东西,但它指的是一个 R 库(我猜),这对我来说是希腊语!

S 形曲线

如果只想要 y 方向的曲线,一个非常简单的解决方案是使用 S 形曲线在控制点之间插入 y 位置

// where 0 <= x <= 1 and p > 1
// return value between 0 and 1 inclusive.
// p is power and determines the amount of curve
function sigmoidCurve(x, p){
    x = x < 0 ? 0 : x > 1 ? 1 : x;
    var xx = Math.pow(x, p);
    return xx / (xx + Math.pow(1 - x, p))    
}

如果您想要位于两个控制点 x1y1x2 之间的 x 坐标 px 处的 y 位置,y2

先求pxx1,x2

之间的归一化位置
var nx = (px - x1) / (x2 - x1); // normalised dist between points

将值代入 sigmoidCurve

var c = sigmoidCurve(nx, 2); // curve power 2

使用该值计算 y

var py = (y2 - y1) * c + y1;

你在两点之间的曲线上有一个点。

作为单个表达式

var py = (y2 - y1) *sigmoidCurve((px - x1) / (x2 - x1), 2) + y1;

如果将 S 形曲线的幂设置为 1.5,那么它几乎可以完美匹配三次贝塞尔曲线

例子

此示例显示动画曲线。函数 getPointOnCurve 将获取曲线上任意点在 x

位置的 y 坐标

const ctx = canvas.getContext("2d");
const curve = [[10, 0], [120, 100], [200, 50], [290, 150]];
const pos = {};
function  cubicInterpolation(x, p0, p1, p2, p3){
    x = x < 0 ? 0 : x > 1 ? 1 : x;
    return p1 + 0.5*x*(p2 - p0 + x*(2*p0 - 5*p1 + 4*p2 - p3 + x*(3*(p1 - p2) + p3 - p0)));   
}
function sigmoidCurve(x, p){
    x = x < 0 ? 0 : x > 1 ? 1 : x;
 var xx = Math.pow(x, p);
 return xx / (xx + Math.pow(1 - x, p))    
}
// functional for loop
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
// functional iterator 
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };


// find index for x in curve
// returns pos{ index, y }
// if x is at a control point then return the y value and index set to -1
// if not at control point the index is of the point befor x
function getPosOnCurve(x,curve, pos = {}){
  var len = curve.length;
  var i;
  pos.index = -1;
  pos.y = null;
  if(x <= curve[0][0]) { return (pos.y = curve[0][1], pos) }
  if(x >= curve[len - 1][0]) { return  (pos.y = curve[len - 1][1], pos) }
  i = 0;
  var found = false;
  while(!found){  // some JS optimisers will mark "Do not optimise" 
                  // code that do not have an exit clause.
    if(curve[i++][0] <x && curve[i][0] >= x) { break }
  }
  i -= 1;
  if(x === curve[i][0]) { return (pos.y = curve[i][1], pos) }
  pos.index =i
  return pos;
}
// Using Cubic interpolation to create the curve
function getPointOnCubicCurve(x, curve, power){
  getPosOnCurve(x, curve, pos);
  if(pos.index === -1) { return pos.y };
  var i = pos.index;
  // get interpolation values for points around x
  var p0,p1,p2,p3;
  p1 = curve[i][1];
  p2 = curve[i+1][1];
  p0 = i === 0 ? p1 : curve[i-1][1];
  p3 = i === curve.length - 2 ? p2 : curve[i+2][1];
  // get unit distance of x between curve i, i+1
  var ux = (x - curve[i][0]) / (curve[i + 1][0] - curve[i][0]);
  return cubicInterpolation(ux, p0, p1, p2, p3);
}




// Using Sigmoid function to get curve.
// power changes curve power = 1 is line power > 1 tangents become longer
// With the power set to 1.5 this is almost a perfect match for
// cubic bezier solution.
function getPointOnCurve(x, curve, power){
  getPosOnCurve(x, curve, pos);
  if(pos.index === -1) { return pos.y };
  var i = pos.index;
  var p = sigmoidCurve((x - curve[i][0]) / (curve[i + 1][0] - curve[i][0]) ,power);
  return curve[i][1] + (curve[i + 1][1] - curve[i][1]) * p;
}

const step = 2;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center width and height
var ch = h / 2;
function update(timer){
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
  ctx.clearRect(0,0,w,h);
    eachOf(curve, (point) => {
      point[1] = Math.sin(timer / (((point[0] + 10) % 71) * 100) ) * ch * 0.8 + ch;
    });
    
    ctx.strokeStyle = "black";
    ctx.beginPath();
    doFor(w / step, x => { ctx.lineTo(x * step, getPointOnCurve(x * step, curve, 1.5) - 10)});
    ctx.stroke();
    ctx.strokeStyle = "blue";
    ctx.beginPath();
    doFor(w / step, x => { ctx.lineTo(x * step, getPointOnCubicCurve(x * step, curve, 1.5) + 10)});
    ctx.stroke();    

    
    ctx.strokeStyle = "black";
    eachOf(curve,point => ctx.strokeRect(point[0] - 2,point[1] - 2 - 10, 4, 4) );
    eachOf(curve,point => ctx.strokeRect(point[0] - 2,point[1] - 2 + 10, 4, 4) );
    requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>

更新

我在上面的演示中添加了第二种曲线类型,作为蓝色曲线从黑色的原始 sigmoid 曲线偏移。

三次多项式

以上函数可以适配多种插值方式。我添加了函数

function  cubicInterpolation(x, p0, p1, p2, p3){
    x = x < 0 ? 0 : x > 1 ? 1 : x;
    return p1 + 0.5*x*(p2 - p0 + x*(2*p0 - 5*p1 + 4*p2 - p3 + x*(3*(p1 - p2) + p3 - p0)));   
}

根据 x 两侧两点直线的斜率生成曲线。此方法适用于均匀间隔的点,但如果您的间距不均匀(例如本例),它仍然有效。如果间距太不均匀,您会注意到该点的曲线有点扭结。

另外,射门上方和下方的曲线可能是个问题。

有关 Maths of cubic interpolation.

的更多信息