如何在 SVG 中使用贝塞尔路径逼近半余弦曲线?
How to approximate a half-cosine curve with bezier paths in SVG?
假设我想在 SVG 中使用贝塞尔路径来近似半余弦曲线。半余弦应如下所示:
从[x0,y0](左侧控制点)到[x1,y1](右侧控制点)。
如何找到一组可接受的系数来很好地近似此函数?
奖金问题:如何推广公式,例如,余弦的四分之一?
请注意,我不想用一系列相互连接的段来近似余弦,我想使用贝塞尔曲线计算一个好的近似值。
我尝试了评论中的解决方案,但是,使用这些系数,曲线似乎在第二个点之后结束。
经过几次tries/errors,我发现正确的比例是K=0.37.
"M" + x1 + "," + y1
+ "C" + (x1 + K * (x2 - x1)) + "," + y1 + ","
+ (x2 - K * (x2 - x1)) + "," + y2 + ","
+ x2 + "," + y2
查看此示例以了解贝塞尔曲线如何与余弦匹配:http://jsfiddle.net/6165Lxu6/
绿线是实余弦,黑线是贝塞尔曲线。向下滚动以查看 5 个示例。每次刷新点都是随机的。
为了泛化,我建议使用裁剪。
我建议阅读这篇关于贝塞尔曲线和椭圆数学的文章,因为这基本上就是您想要的(绘制椭圆的一部分):
http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
它提供了一些所需的见解。
然后看这张图:
http://www.svgopen.org/2003/papers/AnimatedMathematics/ellipse.svg
其中为椭圆制作示例
现在您已经掌握了数学知识,请参阅 LUA 中的示例 ;)
http://commons.wikimedia.org/wiki/File:Harmonic_partials_on_strings.svg
多田...
假设您希望两端的切线保持水平。所以自然地,解决方案将是对称的,归结为在水平方向上找到第一个控制点。
我写了一个程序来做这个:
/*
* Find the best cubic Bézier curve approximation of a sine curve.
*
* We want a cubic Bézier curve made out of points (0,0), (0,K), (1-K,1), (1,1) that approximates
* the shifted sine curve (y = a⋅sin(bx + c) + d) which has its minimum at (0,0) and maximum at (1,1).
* This is useful for CSS animation functions.
*
* ↑ P2 P3
* 1 ו••••••***×
* | ***
* | **
* | *
* | **
* | ***
* ×***•••••••×------1-→
* P0 P1
*/
const sampleSize = 10000; // number of points to compare when determining the root-mean-square deviation
const iterations = 12; // each iteration gives one more digit
// f(x) = (sin(π⋅(x - 1/2)) + 1) / 2 = (1 - cos(πx)) / 2
const f = x => (1 - Math.cos(Math.PI * x)) / 2;
const sum = function (a, b, c) {
if (Array.isArray(c)) {
return [...arguments].reduce(sum);
}
return [a[0] + b[0], a[1] + b[1]];
};
const times = (c, [x0, x1]) => [c * x0, c * x1];
// starting points for our iteration
let [left, right] = [0, 1];
for (let digits = 1; digits <= iterations; digits++) {
// left and right are always integers (digits after 0), this keeps rounding errors low
// In each iteration, we divide them by a higher power of 10
let power = Math.pow(10, digits);
let min = [null, Infinity];
for (let K = 10 * left; K <= 10 * right; K+= 1) { // note that the candidates for K have one more digit than previous `left` and `right`
const P1 = [K / power, 0];
const P2 = [1 - K / power, 1];
const P3 = [1, 1];
let bezierPoint = t => sum(
times(3 * t * (1 - t) * (1 - t), P1),
times(3 * t * t * (1 - t), P2),
times(t * t * t, P3)
);
// determine the error (root-mean-square)
let squaredErrorSum = 0;
for (let i = 0; i < sampleSize; i++) {
let t = i / sampleSize / 2;
let P = bezierPoint(t);
let delta = P[1] - f(P[0]);
squaredErrorSum += delta * delta;
}
let deviation = Math.sqrt(squaredErrorSum); // no need to divide by sampleSize, since it is constant
if (deviation < min[1]) {
// this is the best K value with ${digits + 1} digits
min = [K, deviation];
}
}
left = min[0] - 1;
right = min[0] + 1;
console.log(`.${min[0]}`);
}
为了简化计算,我使用了归一化的正弦曲线,它通过(0,0)
和(1,1)
作为它的最小/最大点。这对于 CSS 动画也很有用。
它returns(.3642124232,0)
*作为均方根偏差最小的点(关于 0.00013
).
我还创建了一个 Desmos graph 来显示准确性:
*注意用JS做数学运算时有舍入误差,所以估计精度不超过5位左右
由于贝塞尔曲线无法准确重建正弦曲线,因此有多种方法可以创建近似值。我将假设我们的曲线从点 (0, 0) 开始并在 (1, 1) 结束。
简单方法
解决这个问题的一个简单方法是用控制点 (K, 0) 和 ((1 - K), 1) 构造贝塞尔曲线 B 因为所涉及的对称性以及在 t=0 和 t=1 处保持水平切线的愿望。
然后我们只需要找到一个 K 值,使我们的贝塞尔曲线的导数与 t=0.5 处的正弦曲线的导数相匹配,即
.
由于 derivative of our Bezier curve 由
给出,这在 t=0.5 时简化为
。
将其设置为我们想要的导数,我们得到解决方案
因此,我们的近似结果是:
cubic-bezier(0.3633802276324187, 0, 0.6366197723675813, 1)
它非常接近,均方根偏差约为 0.000224528:
高级方法
为了获得更好的近似值,我们可能希望最小化它们的差异 root mean square。这计算起来比较复杂,因为我们现在试图在区间 (0, 1) 中找到使以下表达式最小化的 K 值:
其中 B 定义如下:
cubic-bezier(0.364212423249, 0, 0.635787576751, 1)
假设我想在 SVG 中使用贝塞尔路径来近似半余弦曲线。半余弦应如下所示:
从[x0,y0](左侧控制点)到[x1,y1](右侧控制点)。
如何找到一组可接受的系数来很好地近似此函数?
奖金问题:如何推广公式,例如,余弦的四分之一?
请注意,我不想用一系列相互连接的段来近似余弦,我想使用贝塞尔曲线计算一个好的近似值。
我尝试了评论中的解决方案,但是,使用这些系数,曲线似乎在第二个点之后结束。
经过几次tries/errors,我发现正确的比例是K=0.37.
"M" + x1 + "," + y1
+ "C" + (x1 + K * (x2 - x1)) + "," + y1 + ","
+ (x2 - K * (x2 - x1)) + "," + y2 + ","
+ x2 + "," + y2
查看此示例以了解贝塞尔曲线如何与余弦匹配:http://jsfiddle.net/6165Lxu6/
绿线是实余弦,黑线是贝塞尔曲线。向下滚动以查看 5 个示例。每次刷新点都是随机的。
为了泛化,我建议使用裁剪。
我建议阅读这篇关于贝塞尔曲线和椭圆数学的文章,因为这基本上就是您想要的(绘制椭圆的一部分): http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
它提供了一些所需的见解。
然后看这张图: http://www.svgopen.org/2003/papers/AnimatedMathematics/ellipse.svg
其中为椭圆制作示例
现在您已经掌握了数学知识,请参阅 LUA 中的示例 ;) http://commons.wikimedia.org/wiki/File:Harmonic_partials_on_strings.svg
多田...
假设您希望两端的切线保持水平。所以自然地,解决方案将是对称的,归结为在水平方向上找到第一个控制点。
我写了一个程序来做这个:
/*
* Find the best cubic Bézier curve approximation of a sine curve.
*
* We want a cubic Bézier curve made out of points (0,0), (0,K), (1-K,1), (1,1) that approximates
* the shifted sine curve (y = a⋅sin(bx + c) + d) which has its minimum at (0,0) and maximum at (1,1).
* This is useful for CSS animation functions.
*
* ↑ P2 P3
* 1 ו••••••***×
* | ***
* | **
* | *
* | **
* | ***
* ×***•••••••×------1-→
* P0 P1
*/
const sampleSize = 10000; // number of points to compare when determining the root-mean-square deviation
const iterations = 12; // each iteration gives one more digit
// f(x) = (sin(π⋅(x - 1/2)) + 1) / 2 = (1 - cos(πx)) / 2
const f = x => (1 - Math.cos(Math.PI * x)) / 2;
const sum = function (a, b, c) {
if (Array.isArray(c)) {
return [...arguments].reduce(sum);
}
return [a[0] + b[0], a[1] + b[1]];
};
const times = (c, [x0, x1]) => [c * x0, c * x1];
// starting points for our iteration
let [left, right] = [0, 1];
for (let digits = 1; digits <= iterations; digits++) {
// left and right are always integers (digits after 0), this keeps rounding errors low
// In each iteration, we divide them by a higher power of 10
let power = Math.pow(10, digits);
let min = [null, Infinity];
for (let K = 10 * left; K <= 10 * right; K+= 1) { // note that the candidates for K have one more digit than previous `left` and `right`
const P1 = [K / power, 0];
const P2 = [1 - K / power, 1];
const P3 = [1, 1];
let bezierPoint = t => sum(
times(3 * t * (1 - t) * (1 - t), P1),
times(3 * t * t * (1 - t), P2),
times(t * t * t, P3)
);
// determine the error (root-mean-square)
let squaredErrorSum = 0;
for (let i = 0; i < sampleSize; i++) {
let t = i / sampleSize / 2;
let P = bezierPoint(t);
let delta = P[1] - f(P[0]);
squaredErrorSum += delta * delta;
}
let deviation = Math.sqrt(squaredErrorSum); // no need to divide by sampleSize, since it is constant
if (deviation < min[1]) {
// this is the best K value with ${digits + 1} digits
min = [K, deviation];
}
}
left = min[0] - 1;
right = min[0] + 1;
console.log(`.${min[0]}`);
}
为了简化计算,我使用了归一化的正弦曲线,它通过(0,0)
和(1,1)
作为它的最小/最大点。这对于 CSS 动画也很有用。
它returns(.3642124232,0)
*作为均方根偏差最小的点(关于 0.00013
).
我还创建了一个 Desmos graph 来显示准确性:
*注意用JS做数学运算时有舍入误差,所以估计精度不超过5位左右
由于贝塞尔曲线无法准确重建正弦曲线,因此有多种方法可以创建近似值。我将假设我们的曲线从点 (0, 0) 开始并在 (1, 1) 结束。
简单方法
解决这个问题的一个简单方法是用控制点 (K, 0) 和 ((1 - K), 1) 构造贝塞尔曲线 B 因为所涉及的对称性以及在 t=0 和 t=1 处保持水平切线的愿望。
然后我们只需要找到一个 K 值,使我们的贝塞尔曲线的导数与 t=0.5 处的正弦曲线的导数相匹配,即 .
由于 derivative of our Bezier curve 由 给出,这在 t=0.5 时简化为
。
将其设置为我们想要的导数,我们得到解决方案
因此,我们的近似结果是:
cubic-bezier(0.3633802276324187, 0, 0.6366197723675813, 1)
它非常接近,均方根偏差约为 0.000224528:
高级方法
为了获得更好的近似值,我们可能希望最小化它们的差异 root mean square。这计算起来比较复杂,因为我们现在试图在区间 (0, 1) 中找到使以下表达式最小化的 K 值:
其中 B 定义如下:
cubic-bezier(0.364212423249, 0, 0.635787576751, 1)