前言
近期学习了一段时间的GDI+,突然想着用GDI+绘制点啥,用来验证下类与方法。有兴趣的,可以查阅Windows GDI+学习笔记相关文章。
效果展示
实现步骤
- 定义心形函数 。
这里实现两种心形函数
函数一:
函数二:
- 生成指定宽度、高度的心形坐标集 。
- 在定时器中控制,定时刷新绘制区域,控制当前绘制的心形点集,轮廓的点序号、心跳状态。
- 根据当前心形点集绘制一个心形用于绕着另一个心形边缘点旋转并逐渐绘制出另一个完整心形。
- 用渐变色填充最内层心形。
- 增加心形宽、高,生成另一个心形。
- 用大的心形围绕待绘制的心形边缘旋转逐渐绘制出另一个完整心形。
- 当有两层心形时,交替填充显示最内一层或二层的心形,使其有跳动的效果。
- 继续加大心形的宽、高,并在外面绘制更多层的心形。
- 当达到一定层数后,用另一个心形函数再次重复上次的绘制过程。
源代码
使用VS创建一个C# WinForm解决方案,将窗体重命名为FrmHeartbeat的,替换窗体中的代码,可直接运行。或在文章后下载解决方案。
心形坐标类
用于记录心形轮廓上每个点的坐标,以及该点相对中心的角度(用于绘制旋转心形)
/// <summary>
/// 心形坐标
/// </summary>
public class HeartPt
{
/// <summary>
/// 点的角度(用于旋转)
/// </summary>
public float Angle { get; set; }
/// <summary>
/// 点的坐标
/// </summary>
public PointF Pt { get; set; }
}
心形函数
使用了两种心形函数,生成心形坐标点集
/// <summary>
/// 生成心形1
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
private List<HeartPt> GetHeartPts(float width, float height)
{
var result = new List<HeartPt>();
// 生成心形坐标
for (float angle = 0; angle < 360; angle += angleStep)
{
var radian = Math.PI * angle / 180;
var sinT = Math.Sin(radian);
double x = 16 * Math.Pow(sinT, 3);
double y = 13 * Math.Cos(radian) - 5 * Math.Cos(2 * radian) - 2 * Math.Cos(3 * radian) - Math.Cos(4 * radian);
// 转换坐标到画布上
var xCoord = (float)(x * (width / 2));
var yCoord = (float)(-y * (height / 2));
result.Add(new HeartPt()
{
Angle = angle,
Pt = new PointF(xCoord, yCoord),
});
}
return result;
}
/// <summary>
/// 生成心形2
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
private List<HeartPt> GetHeartPts2(float width,float height)
{
width = width * 10;
height = height * 10;
var result = new List<HeartPt>();
var sqrt2 = Math.Sqrt(2);
// 生成心形坐标
for (float angle = 0; angle < 360; angle+= angleStep)
{
var radian = Math.PI * angle / 180;
var sinT = Math.Sin(radian);
double x = -sqrt2 * Math.Pow(sinT, 3);
var cosT = Math.Cos(radian);
double y = 2 * cosT - Math.Pow(cosT, 2) - Math.Pow(cosT, 3);
// 转换坐标到画布上
var xCoord = (float)(x * (width / 2));
var yCoord = (float)(-y * (height / 2));
result.Add(new HeartPt()
{
Angle = angle,
Pt = new PointF(xCoord, yCoord),
});
}
return result;
}
定时器方法
1、定时刷新绘制区域
2、控制当前心形轮廓点的序号
3、当绘制完一个心形后,记录并生成另一个扩大的心形
4、控制心跳频率
private void Timer_Tick(object sender, EventArgs e)
{
this.Invalidate();
CurrDrawPtIndex++;
CurrDrawPtIndex = HeartPtList.Count == 0 ? 0 : CurrDrawPtIndex % HeartPtList.Count;
if (CurrDrawPtIndex == 0)
{
if (heartWidth > MaxHeartWidth)
{//换样式,重绘
heartWidth = MinHeartWidth;
beatCount = 0;
beat = false;
FinishedHeartList.Clear();
heartType++;
heartType = heartType % 2;
}
else if (CurrHeartPts.Count > 0)
{
FinishedHeartList.Add(CurrHeartPts.Select(z => new PointF(z.X, z.Y)).ToArray());
}
heartWidth++;
if (heartType == 0)
{
HeartPtList = GetHeartPts(heartWidth, heartWidth);
}
else
{
HeartPtList = GetHeartPts2(heartWidth, heartWidth);
}
CurrHeartPts = new List<PointF>();
}
CurrHeartPts.Add(HeartPtList[CurrDrawPtIndex].Pt);
beatCount++;
if (beatCount >= 600 / timer.Interval)
{//多久跳动一次
beatCount = 0;
if (FinishedHeartList.Count > 1)
{
beat = !beat;
}
}
}
绘制函数
1、设计绘制参数、清空背景
2、控制心形中心点坐标
3、绘制已记录的心形,控制最内两层心形的填充和其它层心形的颜色
4、通过待绘制心形点集的序号控制心形绘制位置与旋转角度
5、注意矩阵的变化,使旋转心形的控制点与待绘制轮廓点一致。
private void FrmHeart_Paint(object sender, PaintEventArgs e)
{
e.Graphics.CompositingQuality = CompositingQuality.HighQuality;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.Clear(Color.White);
if (HeartPtList.Count == 0) return;
//心形中心点
e.Graphics.TranslateTransform(this.Width / 2, this.Height / 2.2f);
var pen = FinishedHeartList.Count % 2 == 0 ? Pens.Red : Pens.Pink;
Color color = Color.Red;
for (int i = FinishedHeartList.Count - 1; i >= 0; i--)
{
var heart = FinishedHeartList[i];
if (i <= 1)
{//渐变色填充
PathGradientBrush pathGBrush = new PathGradientBrush(heart);
{
if (beat)
{//跳动时,最里面不绘制
if (i == 0) break;
color = Color.Red;
}
else
{
color = Colors[i];
}
pathGBrush.SurroundColors = new Color[] { color };
pathGBrush.CenterPoint = new PointF(0, heart.Max(z=>z.Y)/10f);
pathGBrush.CenterColor = Color.White;
var gPath = new GraphicsPath();
gPath.AddClosedCurve(heart);
e.Graphics.FillPath(pathGBrush, gPath);
}
}
else
{
color = GetColor(i);
using (var penC = new Pen(color,2))
{//心形轮廓
e.Graphics.DrawClosedCurve(penC, heart);
}
}
}
color = GetColor(FinishedHeartList.Count);
using (var penC = new Pen(color,2))
{
if (CurrHeartPts.Count > 2)
{//绘制当前心形
e.Graphics.DrawCurve(penC, CurrHeartPts.ToArray());
}
var currHeartPt = HeartPtList[CurrDrawPtIndex].Pt;
var firstHeartPt = HeartPtList[0].Pt;
//绘制旋转的心形
using (var gPath = new GraphicsPath())
{
gPath.AddClosedCurve(HeartPtList.Select(z => z.Pt).ToArray());
using (var matrix = new Matrix())
{
matrix.Translate(currHeartPt.X, currHeartPt.Y, MatrixOrder.Append);
matrix.Translate(-firstHeartPt.X, -firstHeartPt.Y, MatrixOrder.Append);
matrix.RotateAt(HeartPtList[CurrDrawPtIndex].Angle, currHeartPt, MatrixOrder.Append);
gPath.Transform(matrix);
e.Graphics.DrawPath(penC, gPath);
}
}
}
}
完整源码
public partial class FrmHeartbeat : Form
{
public FrmHeartbeat()
{
InitializeComponent();
this.FormBorderStyle = FormBorderStyle.FixedToolWindow;
this.Width = 960;
this.Height = 800;
this.StartPosition = FormStartPosition.CenterScreen;
this.DoubleBuffered = true;
this.Load += new System.EventHandler(this.FrmHeart_Load);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.FrmHeart_Paint);
}
private void FrmHeart_Load(object sender, EventArgs e)
{
this.Text = "心动";
timer = new Timer();
timer.Interval = 20;//控制绘制速度
timer.Tick += Timer_Tick;
timer.Start();
}
const int MinHeartWidth = 15;
const int MaxHeartWidth = 25;
private int heartWidth = MinHeartWidth;
private bool beat = false;
private int beatCount = 0;
private int heartType = 0;
private void Timer_Tick(object sender, EventArgs e)
{
this.Invalidate();
CurrDrawPtIndex++;
CurrDrawPtIndex = HeartPtList.Count == 0 ? 0 : CurrDrawPtIndex % HeartPtList.Count;
if (CurrDrawPtIndex == 0)
{
if (heartWidth > MaxHeartWidth)
{//换样式,重绘
heartWidth = MinHeartWidth;
beatCount = 0;
beat = false;
FinishedHeartList.Clear();
heartType++;
heartType = heartType % 2;
}
else if (CurrHeartPts.Count > 0)
{
FinishedHeartList.Add(CurrHeartPts.Select(z => new PointF(z.X, z.Y)).ToArray());
}
heartWidth++;
if (heartType == 0)
{
HeartPtList = GetHeartPts(heartWidth, heartWidth);
}
else
{
HeartPtList = GetHeartPts2(heartWidth, heartWidth);
}
CurrHeartPts = new List<PointF>();
}
CurrHeartPts.Add(HeartPtList[CurrDrawPtIndex].Pt);
beatCount++;
if (beatCount >= 600 / timer.Interval)
{//多久跳动一次
beatCount = 0;
if (FinishedHeartList.Count > 1)
{
beat = !beat;
}
}
}
/// <summary>
/// 中间已绘制的心形
/// </summary>
List<PointF[]> FinishedHeartList = new List<PointF[]>();
/// <summary>
/// 当前心形轮廓
/// </summary>
List<HeartPt> HeartPtList = new List<HeartPt>();
/// <summary>
/// 当前心形轮廓
/// </summary>
List<PointF> CurrHeartPts = new List<PointF>();
Timer timer;
/// <summary>
/// 当前心形坐标集序号
/// </summary>
int CurrDrawPtIndex = 0;
/// <summary>
/// 角度边长(越大越快)
/// </summary>
float angleStep = 2f;
/// <summary>
/// 生成心形1
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
private List<HeartPt> GetHeartPts(float width, float height)
{
var result = new List<HeartPt>();
// 生成心形坐标
for (float angle = 0; angle < 360; angle += angleStep)
{
var radian = Math.PI * angle / 180;
var sinT = Math.Sin(radian);
double x = 16 * Math.Pow(sinT, 3);
double y = 13 * Math.Cos(radian) - 5 * Math.Cos(2 * radian) - 2 * Math.Cos(3 * radian) - Math.Cos(4 * radian);
// 转换坐标到画布上
var xCoord = (float)(x * (width / 2));
var yCoord = (float)(-y * (height / 2));
result.Add(new HeartPt()
{
Angle = angle,
Pt = new PointF(xCoord, yCoord),
});
}
return result;
}
/// <summary>
/// 生成心形2
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
private List<HeartPt> GetHeartPts2(float width,float height)
{
width = width * 10;
height = height * 10;
var result = new List<HeartPt>();
var sqrt2 = Math.Sqrt(2);
// 生成心形坐标
for (float angle = 0; angle < 360; angle+= angleStep)
{
var radian = Math.PI * angle / 180;
var sinT = Math.Sin(radian);
double x = -sqrt2 * Math.Pow(sinT, 3);
var cosT = Math.Cos(radian);
double y = 2 * cosT - Math.Pow(cosT, 2) - Math.Pow(cosT, 3);
// 转换坐标到画布上
var xCoord = (float)(x * (width / 2));
var yCoord = (float)(-y * (height / 2));
result.Add(new HeartPt()
{
Angle = angle,
Pt = new PointF(xCoord, yCoord),
});
}
return result;
}
Color[] Colors = new Color[] { Color.Red, Color.Pink };
/// <summary>
/// 根据轮廓层次获取其对应颜色
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
private Color GetColor(int index)
{
Color color;
if (beat)
{
color = Colors[(index + 1) % 2];
}
else
{
color = Colors[index % 2];
}
return color;
}
private void FrmHeart_Paint(object sender, PaintEventArgs e)
{
e.Graphics.CompositingQuality = CompositingQuality.HighQuality;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.Clear(Color.White);
if (HeartPtList.Count == 0) return;
//心形中心点
e.Graphics.TranslateTransform(this.Width / 2, this.Height / 2.2f);
var pen = FinishedHeartList.Count % 2 == 0 ? Pens.Red : Pens.Pink;
Color color = Color.Red;
for (int i = FinishedHeartList.Count - 1; i >= 0; i--)
{
var heart = FinishedHeartList[i];
if (i <= 1)
{//渐变色填充
PathGradientBrush pathGBrush = new PathGradientBrush(heart);
{
if (beat)
{//跳动时,最里面不绘制
if (i == 0) break;
color = Color.Red;
}
else
{
color = Colors[i];
}
pathGBrush.SurroundColors = new Color[] { color };
pathGBrush.CenterPoint = new PointF(0, heart.Max(z=>z.Y)/10f);
pathGBrush.CenterColor = Color.White;
var gPath = new GraphicsPath();
gPath.AddClosedCurve(heart);
e.Graphics.FillPath(pathGBrush, gPath);
}
}
else
{
color = GetColor(i);
using (var penC = new Pen(color,2))
{//心形轮廓
e.Graphics.DrawClosedCurve(penC, heart);
}
}
}
color = GetColor(FinishedHeartList.Count);
using (var penC = new Pen(color,2))
{
if (CurrHeartPts.Count > 2)
{//绘制当前心形
e.Graphics.DrawCurve(penC, CurrHeartPts.ToArray());
}
var currHeartPt = HeartPtList[CurrDrawPtIndex].Pt;
var firstHeartPt = HeartPtList[0].Pt;
//绘制旋转的心形
using (var gPath = new GraphicsPath())
{
gPath.AddClosedCurve(HeartPtList.Select(z => z.Pt).ToArray());
using (var matrix = new Matrix())
{
matrix.Translate(currHeartPt.X, currHeartPt.Y, MatrixOrder.Append);
matrix.Translate(-firstHeartPt.X, -firstHeartPt.Y, MatrixOrder.Append);
matrix.RotateAt(HeartPtList[CurrDrawPtIndex].Angle, currHeartPt, MatrixOrder.Append);
gPath.Transform(matrix);
e.Graphics.DrawPath(penC, gPath);
}
}
}
}
private void FrmHeartbeat_Click(object sender, EventArgs e)
{
if(timer.Enabled)
{
timer.Stop();
}
else
{
timer.Start();
}
}
}
/// <summary>
/// 心形坐标
/// </summary>
public class HeartPt
{
/// <summary>
/// 点的角度(用于旋转)
/// </summary>
public float Angle { get; set; }
/// <summary>
/// 点的坐标
/// </summary>
public PointF Pt { get; set; }
}
结束语
个人认为,要实现本文的绘制有两点需要注意:
- 心形函数
使用一个函数来实现一个完美的心形绘制,这决对不是一件简单的事,本文中的两个函数可能是数学家的呕心沥血之作,向前辈致敬!但对于我们使用它,可能就是一件相对简单的事了。 - 控制旋转心形点与待绘制心形轮廓点一致
在图形的绘制过程中,矩阵的应用一直是个难点(至少对于本人来说),本文中要控制旋转心形与待绘制心形的点一致,使其感觉是用一个心形来绘制另一个心形,其中的矩阵变换,从结果回看,感觉很简单,但在实际应用过程,往往需要重复多次去尝试(肯定是数学理论没过关),有时仅仅因为顺序的不同,导致结果天差地别。在了解了GDI+大部分函数与功能后,对于矩阵变换还是需要花更多时间去研究。
完整源码下载