GDI+ 曲线 "overflowing"
GDI+ curve "overflowing"
我目前正在使用GDI+绘制折线图,并使用Graphics.DrawCurve
来平滑线条。问题是曲线并不总是与我输入的点匹配,这使得曲线在某些点上超出了图框,如下所示(红色为 [=12=,绿色为 Graphics.DrawCurve
).
我该如何解决这个问题?
最简单的解决办法是设置张力:
绿色曲线使用默认张力绘制,蓝色曲线设置张力0.1f
:
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.DrawLines(Pens.Red, points.ToArray());
e.Graphics.DrawCurve(Pens.Green, points.ToArray());
e.Graphics.DrawCurve(Pens.Blue, points.ToArray(), 0.1f);
}
你需要测试什么是最好的折衷方案,0.2f
还可以,0.3f
已经透支了很多..
要获得真正好的解决方案,您需要使用 DrawBeziers
。这将使您绘制的曲线可以通过这些点而不会过度绘制,并且可以完全控制曲线的半径;但是要做到这一点,您将需要 'find',即计算 good control points
,这绝不是微不足道的..:[=28=]
这个结果绝不是完美的,但已经足够复杂了..我已经用相同的颜色显示了curve points
和它们各自的control points
。对于每个点,都有一个 incoming 和一个 outgoing 控制点。对于平滑曲线,他们需要在曲线点中具有相同的 tangents/gradients。
我使用了一些辅助函数来计算关于细分的一些事情:
- 渐变列表
- 梯度符号列表
- 段长度列表
- 点之间的水平和垂直间隙列表
主函数计算bezier points
的数组,也就是curve points
和每对之间的前左和右下一个 control points
.
在Paint
事件中是这样使用的:
List<PointF> bezz = getBezz(points);
using (Pen pen = new Pen(Color.Black, 2f))
e.Graphics.DrawBeziers(pen, bezz.ToArray());
以下是我使用的函数:
List<float> getGradients(List<PointF> p)
{
List<float> grads = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
if (dx == 0) grads.Add(dy == 0 ? 0 : dy > 0 ?
float.PositiveInfinity : float.NegativeInfinity);
else grads.Add(dy / dx);
}
return grads;
}
List<float> getLengths(List<PointF> p)
{
List<float> lengs = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
lengs.Add((float)Math.Sqrt(dy * dy + dx * dx));
}
return lengs;
}
List<float> getGaps(List<PointF> p, bool horizontal)
{
List<float> gaps = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
gaps.Add(horizontal ? dx : dy);
}
return gaps;
}
List<int> getSigns(List<float> g)
{
return g.Select(x => x > 0 ? 1 : x == 0 ? 0 : -1).ToList();
}
最后是主要功能;在这里我做了一个区分:极值点(最小值和最大值)的控制点应该与点本身的高度相同。这将防止垂直溢出。它们很容易找到:它们的梯度符号总是交替变化。
其他点需要对传入和传出控制点具有相同的梯度。我使用分段梯度之间的平均值。 (也许加权平均会更好..)我根据段长度权衡它们的距离..
List<PointF> getBezz(List<PointF> points)
{
List<PointF> bezz = new List<PointF>();
int pMax = points.Count;
List<float> hGaps = getGaps(points, true);
List<float> vGaps = getGaps(points, false);
List<float> grads = getGradients(points);
List<float> lengs = getLengths(points);
List<int> signs = getSigns(grads);
PointF[] bezzA = new PointF[pMax * 3 - 2];
// curve points
for (int i = 0; i < pMax; i++) bezzA[i * 3] = points[i];
// left control points
for (int i = 1; i < pMax; i++)
{
float x = points[i].X - hGaps[i - 1] / 2f;
float y = points[i].Y;
if (i < pMax - 1 && signs[i - 1] == signs[i])
{
float m = (grads[i-1] + grads[i]) / 2f;
y = points[i].Y - hGaps[i-1] / 2f * m * vGaps[i-1] / lengs[i-1];
}
bezzA[i * 3 - 1] = new PointF(x, y);
}
// right control points
for (int i = 0; i < pMax - 1; i++)
{
float x = points[i].X + hGaps[i] / 2f;
float y = points[i].Y;
if (i > 0 && signs[i-1] == signs[i])
{
float m = (grads[i-1] + grads[i]) / 2f;
y = points[i].Y + hGaps[i] / 2f * m * vGaps[i] / lengs[i];
}
bezzA[i * 3 + 1] = new PointF(x, y);
}
return bezzA.ToList();
}
请注意,我没有针对具有相同 x 坐标的点的情况进行编码。所以这对于 'functional graphs' 是可以的,但对于数字,例如星星..
也许您只是想将 "overshooting the bounds" 问题视为不是超调问题,而是边界问题。在这种情况下,您可以使用 System.Drawing.Drawing2D.GraphicsPath 对象确定曲线的实际边界:
GraphicsPath gp = new GraphicsPath();
gp.AddCurve(listOfPoints);
RectangleF bounds = gp.GetBounds();
您可以直接绘制该 GraphicsPath:
graphics.DrawPath(Pens.Black, gp);
就解决边界问题而言,线必然超过一些轴上的顶点。当线条与边界对齐时,更容易看出这一点。
鉴于这些点:
为了使它们弯曲,它们必须以某种方式超出其边界:
如果您永远不想超过它们的垂直边界,您可以简单地确保贝塞尔图手柄具有与顶点相同的 Y 值,但它们会在 X 上超过:
或vice-versa:
您可以故意下调到足以避免曲线过冲的方式。这可以通过交换贝塞尔曲线图柄来完成,它可能位于 line-centers,顶点为:
我目前正在使用GDI+绘制折线图,并使用Graphics.DrawCurve
来平滑线条。问题是曲线并不总是与我输入的点匹配,这使得曲线在某些点上超出了图框,如下所示(红色为 [=12=,绿色为 Graphics.DrawCurve
).
我该如何解决这个问题?
最简单的解决办法是设置张力:
绿色曲线使用默认张力绘制,蓝色曲线设置张力0.1f
:
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.DrawLines(Pens.Red, points.ToArray());
e.Graphics.DrawCurve(Pens.Green, points.ToArray());
e.Graphics.DrawCurve(Pens.Blue, points.ToArray(), 0.1f);
}
你需要测试什么是最好的折衷方案,0.2f
还可以,0.3f
已经透支了很多..
要获得真正好的解决方案,您需要使用 DrawBeziers
。这将使您绘制的曲线可以通过这些点而不会过度绘制,并且可以完全控制曲线的半径;但是要做到这一点,您将需要 'find',即计算 good control points
,这绝不是微不足道的..:[=28=]
这个结果绝不是完美的,但已经足够复杂了..我已经用相同的颜色显示了curve points
和它们各自的control points
。对于每个点,都有一个 incoming 和一个 outgoing 控制点。对于平滑曲线,他们需要在曲线点中具有相同的 tangents/gradients。
我使用了一些辅助函数来计算关于细分的一些事情:
- 渐变列表
- 梯度符号列表
- 段长度列表
- 点之间的水平和垂直间隙列表
主函数计算bezier points
的数组,也就是curve points
和每对之间的前左和右下一个 control points
.
在Paint
事件中是这样使用的:
List<PointF> bezz = getBezz(points);
using (Pen pen = new Pen(Color.Black, 2f))
e.Graphics.DrawBeziers(pen, bezz.ToArray());
以下是我使用的函数:
List<float> getGradients(List<PointF> p)
{
List<float> grads = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
if (dx == 0) grads.Add(dy == 0 ? 0 : dy > 0 ?
float.PositiveInfinity : float.NegativeInfinity);
else grads.Add(dy / dx);
}
return grads;
}
List<float> getLengths(List<PointF> p)
{
List<float> lengs = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
lengs.Add((float)Math.Sqrt(dy * dy + dx * dx));
}
return lengs;
}
List<float> getGaps(List<PointF> p, bool horizontal)
{
List<float> gaps = new List<float>();
for (int i = 0; i < p.Count - 1; i++)
{
float dx = p[i + 1].X - p[i].X;
float dy = p[i + 1].Y - p[i].Y;
gaps.Add(horizontal ? dx : dy);
}
return gaps;
}
List<int> getSigns(List<float> g)
{
return g.Select(x => x > 0 ? 1 : x == 0 ? 0 : -1).ToList();
}
最后是主要功能;在这里我做了一个区分:极值点(最小值和最大值)的控制点应该与点本身的高度相同。这将防止垂直溢出。它们很容易找到:它们的梯度符号总是交替变化。
其他点需要对传入和传出控制点具有相同的梯度。我使用分段梯度之间的平均值。 (也许加权平均会更好..)我根据段长度权衡它们的距离..
List<PointF> getBezz(List<PointF> points)
{
List<PointF> bezz = new List<PointF>();
int pMax = points.Count;
List<float> hGaps = getGaps(points, true);
List<float> vGaps = getGaps(points, false);
List<float> grads = getGradients(points);
List<float> lengs = getLengths(points);
List<int> signs = getSigns(grads);
PointF[] bezzA = new PointF[pMax * 3 - 2];
// curve points
for (int i = 0; i < pMax; i++) bezzA[i * 3] = points[i];
// left control points
for (int i = 1; i < pMax; i++)
{
float x = points[i].X - hGaps[i - 1] / 2f;
float y = points[i].Y;
if (i < pMax - 1 && signs[i - 1] == signs[i])
{
float m = (grads[i-1] + grads[i]) / 2f;
y = points[i].Y - hGaps[i-1] / 2f * m * vGaps[i-1] / lengs[i-1];
}
bezzA[i * 3 - 1] = new PointF(x, y);
}
// right control points
for (int i = 0; i < pMax - 1; i++)
{
float x = points[i].X + hGaps[i] / 2f;
float y = points[i].Y;
if (i > 0 && signs[i-1] == signs[i])
{
float m = (grads[i-1] + grads[i]) / 2f;
y = points[i].Y + hGaps[i] / 2f * m * vGaps[i] / lengs[i];
}
bezzA[i * 3 + 1] = new PointF(x, y);
}
return bezzA.ToList();
}
请注意,我没有针对具有相同 x 坐标的点的情况进行编码。所以这对于 'functional graphs' 是可以的,但对于数字,例如星星..
也许您只是想将 "overshooting the bounds" 问题视为不是超调问题,而是边界问题。在这种情况下,您可以使用 System.Drawing.Drawing2D.GraphicsPath 对象确定曲线的实际边界:
GraphicsPath gp = new GraphicsPath();
gp.AddCurve(listOfPoints);
RectangleF bounds = gp.GetBounds();
您可以直接绘制该 GraphicsPath:
graphics.DrawPath(Pens.Black, gp);
就解决边界问题而言,线必然超过一些轴上的顶点。当线条与边界对齐时,更容易看出这一点。
鉴于这些点:
为了使它们弯曲,它们必须以某种方式超出其边界:
如果您永远不想超过它们的垂直边界,您可以简单地确保贝塞尔图手柄具有与顶点相同的 Y 值,但它们会在 X 上超过:
或vice-versa:
您可以故意下调到足以避免曲线过冲的方式。这可以通过交换贝塞尔曲线图柄来完成,它可能位于 line-centers,顶点为: