最近笔者为了能够做到对用户友好,深入地学习了一些前端的知识,等回过神来,发现已经好久没有在优快云上更过东西了… 结合最近看到的某些前端框架的底层,突然想到为什么不在Grasshopper上玩动画呢?比如电池拖出来之后,缓缓变形,然后出现一些UI的元素。
本来已经做好了手动写 Timer
或者写个线程循环来操作,结果人家Grasshopper自带这些个做动画必备的东西,那就简单了,抱着试试看的态度,完成了一个好玩的动画效果。就如同文章最开始的 gif 动画一般。
下面就 “简单” 介绍一下如何在Grasshopper中实现这个效果。
动画即循环
我们需要了解到动画的本质就是个循环绘制,在上图中的展示的效果中,GH_Canvas
的Refresh()
方法会被反复调用,每一次的刷新都会改变电池的大小,当刷新得足够快的时候,看起来就像动画了。随后的按钮的出现也是类似,每一次的刷新都会调整一次可见度,将可见度慢慢调高,快速刷新多次之后,这个按钮看起来就好像是“慢慢浮现”的效果了。
因此,“什么时候刷新”,或者说是“什么时候停止刷新”,这个条件就显得十分重要了。在上面的这个例子中,我们共有两段动画:
- 电池的高度不断变高
- 电池上按钮的浮现
第一段动画在 “ 电池高度达到指定高度 ” 后完成,第二段动画在 “ 透明度达到指定值 ” 后完成,于是,我们就形成了一个由下面if ... else ...
构成的逻辑:
if (电池高度未达到指定高度)
{
// 此时处在第一段动画中
电池高度++;
渲染电池;
刷新画布;
}
else if (透明度未达到指定透明度)
{
// 此时处在第二段动画中
透明度++;
渲染电池;
渲染按钮;
刷新画布;
}
else
{
渲染电池和按钮;
// 此时不应刷新画布,因两段动画均已结束
}
按照这个主逻辑,我们就可以配置我们的动画了。
Grasshopper中的渲染
熟悉GH_ComponentAttribute
的朋友大概已经知道我们要对其中的Render()
方法进行大刀阔斧地重写了。没错,除此之外,我们还需要重写Layout()
这个方法。 大家都是进阶教程的代码观察师了,详细的就在注释里写啦。
public class AnimatedAttribute : GH_ComponentAttributes
{
public AnimatedAttribute(IGH_Component owner) : base(owner) { }
int addedHeight = 1;
int buttonFade = 0;
protected override void Layout()
{
// 调用基类(GH_ComponentAttributes)的 Layout() 方法,用来生成默认大小的电池外框,
// 我们再基于这个默认生成的外观进行高度的增加
base.Layout();
// 获取电池大小
var bd = Bounds;
// 对原电池大小进行一个修改,这个`addedHeight`修改的值是存储在电池实例上的,并且会在
// Render时,参照我们上文中提到的
bd.Height += addedHeight;
Bounds = bd;
// 之所以在 Layout()函数中修改Bounds,是因为这里是GH框架中完成Bounds测绘的函数,
// 如果在其他函数中对Bounds进行修改,则很有可能造成奇奇怪怪的后果,比如电池大小很诡异
}
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
// 重写的重头戏
// 调用基类的Render方法,由于我们已经针对Bounds调整过其高度了(在Layout函数中)
// 此处可以直接使用基类方法而不做额外改变
// 另外只需要关心电池下方的按钮的绘制即可
base.Render(canvas, graphics, channel);
// 绘制电池相关的内容,只在 channel == GH_CanvasChannel.Objects 时进行操作
if (channel == GH_CanvasChannel.Objects)
{
// 上文中提到的逻辑
if (addedHeight <= 20)
{
addedHeight++; // 以便下一帧的时候,电池的高度会高亿点点…
ExpireLayout(); // 声明该电池已过期,会重新call Layout(),并组织相关绘图所需元素
// 下面这个ScheduleRegen函数就是实现这个动画的重中之重了。
// 为什么不直接canvas.Refresh()?
// 因为程序在执行这行代码时,还处在GH_Canvas的OnPaint()函数中,
// 也就是说,Canvas仍然正在绘制,仍处于上一个Refresh()的调用中。
// 此时立刻Refresh()则会让Canvas再次执行OnPaint(),类似于递归,这并不能做动画
// 我们需要的是能够在这次的Canvas上的所有绘制都完成之后,
// 在这次的OnPaint()调用完毕之后再次执行OnPaint()
// 要实现这种操作,本来是需要写个Timer或者后台线程等待本次执行结束再调用UI线程刷新的
// 不过GH早就给我们准备好了,直接Call这个函数就行,这个函数会等待25毫秒/本次刷新结束之后
// 立刻让画布刷新,达到动画的效果。
canvas.ScheduleRegen(25);
}
else if (buttonFade < 10)
{
// 更新一下透明度
buttonFade++;
// 创造一个按钮图案
GH_Capsule capsule = GH_Capsule.CreateTextCapsule(
new RectangleF(Bounds.Left + 2, Bounds.Bottom - 18, Bounds.Width - 4, 16),
new RectangleF(Bounds.Left + 3, Bounds.Bottom - 17, Bounds.Width - 6, 14),
GH_Palette.Grey,
"Button");
// 以特定的透明度渲染这个按钮
capsule.Render(graphics, System.Drawing.Color.FromArgb(buttonFade * 10, System.Drawing.Color.Black));
// 常规操作
ExpireLayout();
capsule.Dispose();
canvas.ScheduleRegen(25);
}
else
{
// 由于此时动画已经结束,这里就是正常渲染的操作了,
// 最后不会进行 ScheduleRegen 函数的调用
GH_Capsule capsule = GH_Capsule.CreateTextCapsule(
new RectangleF(Bounds.Left + 2, Bounds.Bottom - 18, Bounds.Width - 4, 16),
new RectangleF(Bounds.Left + 3, Bounds.Bottom - 17, Bounds.Width - 6, 14),
GH_Palette.Grey,
"Button");
capsule.Render(graphics, System.Drawing.Color.FromArgb(buttonFade * 10, System.Drawing.Color.Black));
capsule.Dispose();
}
}
}
}
写完Attributes
之后,随便来个电池,挂上我们这个新鲜的Attributes
,开始电池动画的表演吧:
public class ComponentWithAnimatedAttributes : GH_Component
{
public ComponentWithAnimatedAttributes()
: base("AnimatedAttribute", "AA",
"Description",
"Params", "DigitalCrab") {}
public override void CreateAttributes()
{
this.Attributes = new AnimatedAttribute(this);
}
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddGenericParameter("in", "in", "", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddGenericParameter("out", "out", "", GH_ParamAccess.item);
}
protected override void SolveInstance(IGH_DataAccess DA) {}
protected override System.Drawing.Bitmap Icon => null;
public override Guid ComponentGuid => throw new Exception("请自行生成GUID并赋值此处");
}
最后的结果就是文章最开始的GIF图片了。
妙啊!