Animating graphic objects in Windows Forms.
A cool animation is great to look at but how do you go about writing one? Games in particular can use animations of hundreds of objects at once. What defines an object? How does an object move around? These questions are often seen in the various online forums but a comprehensive answer is difficult to give in a short post. This article will explain all the nitty-gritty details. In it, you will see how to create an application that bounces a few hundred animated shapes around screen in real-time. Graphical objects.GDI+ has no concept of a persistent graphical object such as a square or line that you can move from place to place or touch with the mouse cursor. The graphics are written to the display surface and then forgotten about completely. In order to have an object that can be repositioned or made larger and smaller we must create a retained mode graphics engine. The basic thing we need to do is to define some sort of entity, a generic shape perhaps, that has the attributes of position, size and colour. This entity will know how to draw itself to a graphics object. There may be more than one type of shape, such as a rectangle, a pentagon and a star but they will all generally obey the same rules so object oriented architectures enable us to create a "shape" base class with all the basic attributes and then override the small details when we create our specialized objects. The architecture for this particular retained mode system will contain a shape with the following basic capabilities:
Here are the beginnings of the Shape class. public class Shape { Point _location; public Point Location { get{return _location;} set{_location=value;} }
Size _size; public Size Size { get{return _size;} set{_size=value;} }
Color _backColor; public Color BackColor { get{return _backColor;} set{_backColor=value;} }
Color _foreColor; public Color ForeColor { get{return _foreColor;} set{_foreColor=value;} }
int _lineThickness; public int LineThickness { get{return _lineThickness;} set{_lineThickness=value;} }
float _transparency; public float Transparency { get{return _transparency;} set { _transparency=(value>=0 ? (value<=1 ? value : 1) : 0); } }
float _rotation; public float Rotation { get{return _rotation;} set{_rotation=value;} }
Size _vector; public Size Vector { get{return _vector;} set{_vector=value;} }
float _rotationDelta; public float RotationDelta { get{return _rotationDelta;} set{_rotationDelta=value;} }
Rectangle _limits; public Rectangle Limits { get{return _limits;} set{_limits=value;} } }
Public Class Shape Private _location As Point
Public Property Location() As Point Get Return _location End Get Set _location = value End Set End Property Private _size As Size
Public Property Size() As Size Get Return _size End Get Set _size = value End Set End Property Private _backColor As Color
Public Property BackColor() As Color Get Return _backColor End Get Set _backColor = value End Set End Property Private _foreColor As Color
Public Property ForeColor() As Color Get Return _foreColor End Get Set _foreColor = value End Set End Property Private _lineThickness As Integer
Public Property LineThickness() As Integer Get Return _lineThickness End Get Set _lineThickness = value End Set End Property Private _transparency As Single
Public Property Transparency() As Single Get Return _transparency End Get Set _transparency = IIf(value >= 0, IIf(value <= 1, value, 1), 0) End Set End Property
Private _rotation As Single
Public Property Rotation() As Single Get Return _rotation End Get Set _rotation = value End Set End Property Private _vector As Size
Public Property Vector() As Size Get Return _vector End Get Set _vector = value End Set End Property Private _rotationDelta As Single
Public Property RotationDelta() As Single Get Return _rotationDelta End Get Set _rotationDelta = value End Set End Property Private _limits As Rectangle
Public Property Limits() As Rectangle Get Return _limits End Get Set _limits = value End Set End Property
End Class 'Shape Transforms and order.When creating such a retained-mode system, the individual objects will position themselves before they draw the content of the object. The easiest way to do this is with a graphic transform represented by a matrix. Many beginners see the transform as something that is applied to the whole page to scale the drawing or modify the coordinate system in some way but in retained mode systems the transform is generally used on a per-object basis in addition to whatever global transform is applied. Usually the current state of the Graphics system should be saved, the transform applied, the drawing done and the previous state of the graphics system restored before drawing of other objects takes place. This lets your code maintain an orderly and progressive series of state-changes rather than trying to apply a transform to create a particular state an then apply another to get to a second state. More complex systems that have hierarchies of objects will maintain a graphics stack with many levels of transforms. This demonstration only requires one. The order that transforms are applied is of paramount importance. Applying two identical transforms in different orders will produce wildly different results. This animation system uses rotation to spin an object in space and displacement or translation to move it in an X or Y plane. Although it isn't implemented in this demo objects can also be scaled to make them larger or smaller or distort them along the axes. The order that transforms should be applied is generally Scale, Rotate, Translate. The code for the shape base-class provides a method for saving the state of the current Graphics object, applying a transform to the Graphics, drawing and then restoring the state of the Graphics object once more. The methods shown below show how the object sets up the transform, draws the object and then restores the state of the Graphics object. GraphicsState _state;
/// <summary> /// Sets up the transform for each shape /// </summary> /// <remarks> /// As each shape is drawn the transform for that shape including rotation and /// location is made to a new Matrix object. /// This matrix is used to modify the graphics transform <i>For each shape</i> /// </remarks> /// <param name="g">The Graphics being drawn on</param> protected void SetupTransform(Graphics g) { _state=g.Save(); Matrix mx=new Matrix(); mx.Rotate(_rotation,MatrixOrder.Append); mx.Translate(this.Location.X,this.Location.Y,MatrixOrder.Append); g.Transform=mx; }
/// <summary> /// Simply restores the original state of the Graphics object /// </summary> /// <param name="g">The Graphics object being drawn upon</param> protected void RestoreTransform(Graphics g) { g.Restore(_state); }
public void Draw(Graphics g) { SetupTransform(g); RenderObject(g); RestoreTransform(g); } Private _state As GraphicsState
'/ <summary> '/ Sets up the transform for each shape '/ </summary> '/ <remarks> '/ As each shape is drawn the transform for that shape including rotation and location is made to a new Matrix object. '/ This matrix is used to modify the graphics transform <i>For each shape</i> '/ </remarks> '/ <param name="g">The Graphics being drawn on</param> Protected Sub SetupTransform(g As Graphics) _state = g.Save() Dim mx As New Matrix() mx.Rotate(_rotation, MatrixOrder.Append) mx.Translate(Me.Location.X, Me.Location.Y, MatrixOrder.Append) g.Transform = mx End Sub 'SetupTransform
'/ <summary> '/ Simply restores the original state of the Graphics object '/ </summary> '/ <param name="g">The Graphics object being drawn upon</param> Protected Sub RestoreTransform(g As Graphics) g.Restore(_state) End Sub 'RestoreTransform
Public Sub Draw(g As Graphics) SetupTransform(g) RenderObject(g) RestoreTransform(g) End Sub 'Draw
Animating the object.Animation systems generally work from a timebase of some kind. In Windows Forms there are several sorts of timers that we can use to give the animated objects a "pulse". This system provides each object with a "Tick" method that uses the Vector to move a shape, the RotationDelta to rotate the shape, and the Limits to ensure that the shape remains inside the desired area while it bounces around the screen. The following listing shows how the Tick method performs the animations public virtual void Tick() { //ensure that the object is in the page. //this is in case the window was resized if(this.Location.X>this.Limits.Right) this.Location=new Point(this.Limits.Right-1,this.Location.Y); if(this.Location.Y>this.Limits.Bottom) this.Location=new Point(this.Location.X,this.Limits.Bottom-1);
//Generate a new location adding in the vectors //check the limits and switch vector directions as needed int newx=this.Location.X+this.Vector.Width; if(newx>this.Limits.Right || newx<this.Limits.Left) this.Vector=new Size(-1*this.Vector.Width,this.Vector.Height); int newy=this.Location.Y+this.Vector.Height; if(newy>this.Limits.Bottom || newy<this.Limits.Top) this.Vector=new Size(this.Vector.Width,-1*this.Vector.Height);
//This is the new position Location=new Point( this.Location.X+this.Vector.Width, this.Location.Y+this.Vector.Height);
//Apply the rotation factor this.Rotation+=this.RotationDelta; //Limit just to be neat Rotation=(Rotation<360f ? (Rotation>=0 ? Rotation : Rotation+360f) : Rotation-360f); } Public Overridable Sub Tick() 'ensure that the object is in the page. 'this is in case the window was resized If Me.Location.X > Me.Limits.Right Then Me.Location = New Point(Me.Limits.Right - 1, Me.Location.Y) End If If Me.Location.Y > Me.Limits.Bottom Then Me.Location = New Point(Me.Location.X, Me.Limits.Bottom - 1) End If 'Generate a new location adding in the vectors 'check the limits and switch vector directions as needed Dim newx As Integer = Me.Location.X + Me.Vector.Width If newx > Me.Limits.Right OrElse newx < Me.Limits.Left Then Me.Vector = New Size(- 1 * Me.Vector.Width, Me.Vector.Height) End If Dim newy As Integer = Me.Location.Y + Me.Vector.Height If newy > Me.Limits.Bottom OrElse newy < Me.Limits.Top Then Me.Vector = New Size(Me.Vector.Width, - 1 * Me.Vector.Height) End If 'This is the new position Location = New Point(Me.Location.X + Me.Vector.Width, Me.Location.Y + Me.Vector.Height)
'Apply the rotation factor Me.Rotation += Me.RotationDelta 'Limit just to be neat Rotation = IIf(Rotation < 360F, IIf(Rotation >= 0, Rotation, Rotation + 360F), Rotation - 360F) End Sub 'Tick
Drawing the object.Finally, after all the setup work is done, the shape must draw itself onto the display. The Shape class provides a RenderObject virtual method to accomplish this. Derived classes can override this method and draw their own appearance when asked to do so. The Draw method provided by the base class performs the additional tasks of setting up and restoring the object. public virtual void RenderObject(Graphics g) { } Public Overridable Sub RenderObject(g As Graphics) End Sub 'RenderObject Real shapes.Now that the groundwork is laid we can construct real shapes. These override the Shape class and draw themselves in the RenderObject override. The following listing shows three shapes, a Square, a Star and a Pentagon that render themselves after the base class Draw routine has done all the setup work. public class Square : Shape { /// <summary> /// Draws a square. Note that the square is drawn about the origin. /// </summary> /// <param name="g">The graphics to draw on.</param> public override void RenderObject(Graphics g) { Pen p=new Pen(ForeColor,LineThickness); SolidBrush sb=new SolidBrush( Color.FromArgb((int)(255*this.Transparency), this.BackColor)); g.FillRectangle(sb, -this.Size.Width/2, -this.Size.Height/2, this.Size.Width, this.Size.Height); g.DrawRectangle(p, -this.Size.Width/2, -this.Size.Height/2, this.Size.Width, this.Size.Height); sb.Dispose(); p.Dispose(); }
}
public class Star : Shape { /// <summary> /// Draws a star. Note that the star is drawn about the origin. /// </summary> /// <param name="g">The graphics to draw on.</param> public override void RenderObject(Graphics g) { Pen p=new Pen(ForeColor,LineThickness); SolidBrush sb=new SolidBrush( Color.FromArgb((int)(255*this.Transparency), this.BackColor)); Point[] pts=new Point[11]; bool pointy=true; float a=0; for(int c=0;c<10;c++) { float dist=pointy ? 1 : 0.6f; pts[c]=new Point( (int)(dist* (this.Size.Width/2)*Math.Cos(a)), (int)(dist *(this.Size.Height/2)*Math.Sin(a))); a+=(float)Math.PI*2/10; pointy=!pointy; } pts[10]=pts[0]; g.FillPolygon(sb,pts); g.DrawPolygon(p,pts); sb.Dispose(); p.Dispose(); } }
public class Pentagon : Shape { /// <summary> /// Draws a pentagon. Note that the pentagon is drawn about the origin. /// </summary> /// <param name="g">The graphics to draw on.</param> public override void RenderObject(Graphics g) { Pen p=new Pen(ForeColor,LineThickness); SolidBrush sb=new SolidBrush( Color.FromArgb((int)(255*this.Transparency), this.BackColor)); Point[] pts=new Point[6]; float a=0; for(int c=0;c<5;c++) { pts[c]=new Point( (int)((this.Size.Width/2)*Math.Cos(a)), (int)((this.Size.Height/2)*Math.Sin(a))); a+=(float)Math.PI*2/5; } pts[5]=pts[0]; g.FillPolygon(sb,pts); g.DrawPolygon(p,pts); sb.Dispose(); p.Dispose(); }
}
Public Class Square Inherits Shape
'/ <summary> '/ Draws a square. Note that the square is drawn about the origin. '/ </summary> '/ <param name="g">The graphics to draw on.</param> Public Overrides Sub RenderObject(g As Graphics) Dim p As New Pen(ForeColor, LineThickness) Dim sb As New SolidBrush( _ Color.FromArgb(CInt(255 * Me.Transparency), _ Me.BackColor)) g.FillRectangle(sb, _ CSng(-Me.Size.Width / 2), _ CSng(-Me.Size.Height / 2), _ CSng(Me.Size.Width), _ CSng(Me.Size.Height)) g.DrawRectangle(p, _ CSng(-Me.Size.Width / 2), _ CSng(-Me.Size.Height / 2), _ CSng(Me.Size.Width), _ CSng(Me.Size.Height)) sb.Dispose() p.Dispose() End Sub 'RenderObject End Class 'Square
Public Class Star Inherits Shape
'/ <summary> '/ Draws a star. Note that the star is drawn about the origin. '/ </summary> '/ <param name="g">The graphics to draw on.</param> Public Overrides Sub RenderObject(g As Graphics) Dim p As New Pen(ForeColor, LineThickness) Dim sb As New SolidBrush(Color.FromArgb( _ CInt(255 * Me.Transparency), _ Me.BackColor)) Dim pts(10) As Point Dim pointy As Boolean = True Dim a As Single = 0 Dim c As Integer For c = 0 To 9 Dim dist As Single = IIf(pointy, 1, 0.6F) pts(c) = New Point( _ CInt(dist *(Me.Size.Width / 2) * Math.Cos(a)), _ CInt(dist *(Me.Size.Height / 2) * Math.Sin(a))) a += CSng(Math.PI) * 2 / 10 pointy = Not pointy Next c pts(10) = pts(0) g.FillPolygon(sb, pts) g.DrawPolygon(p, pts) sb.Dispose() p.Dispose() End Sub 'RenderObject End Class 'Star
Public Class Pentagon Inherits Shape
'/ <summary> '/ Draws a pentagon. Note that the pentagon is drawn about the origin. '/ </summary> '/ <param name="g">The graphics to draw on.</param> Public Overrides Sub RenderObject(g As Graphics) Dim p As New Pen(ForeColor, LineThickness) Dim sb As New SolidBrush( _ Color.FromArgb(CInt(255 * Me.Transparency), _ Me.BackColor)) Dim pts(5) As Point Dim a As Single = 0 Dim c As Integer For c = 0 To 4 pts(c) = New Point( _ CInt(Me.Size.Width / 2 * Math.Cos(a)), _ CInt(Me.Size.Height / 2 * Math.Sin(a))) a += CSng(Math.PI) * 2 / 5 Next c pts(5) = pts(0) g.FillPolygon(sb, pts) g.DrawPolygon(p, pts) sb.Dispose() p.Dispose() End Sub 'RenderObject End Class 'Pentagon Collect your thoughts.Now a shape had been defined and a family of objects derived from that basic object. The shape knows how to move, rotate, limit it's area of movement to remain in a given area and can be used to provide literally thousands of different possibilities for their visual appearance. Now the application needs to know how to deal with these objects. For this we'll create a collection of Shape objects that can hold as many of them that we like. /// <summary> /// Manages a collection of shape objects /// </summary> public class ShapeCollection : CollectionBase { public void Add(Shape s) { List.Add(s); }
public void Remove(Shape s) { List.Remove(s); }
public Shape this[int index] { get{return (Shape)List[index];} set{List[index]=value;} } }
'/ <summary> '/ Manages a collection of shape objects '/ </summary> Public Class ShapeCollection Inherits CollectionBase
Public Sub Add(ByVal s As Shape) List.Add(s) End Sub 'Add
Public Sub Remove(ByVal s As Shape) List.Remove(s) End Sub 'Remove
Default Public Property Item(ByVal index As Integer) As Shape Get Return CType(List(index), Shape) End Get Set(ByVal Value As Shape) List(index) = Value End Set End Property End Class 'ShapeCollection Integrating with the application.The components include shapes and a collection in which to store them. Now the application has to provide a timing method to create the animating ticks once every few milliseconds. This demo uses 40 millisecond intervals because that gives about 25 frames per second. The value of "about" 25 frames per second should be qualified by the information that the standard Windows.Forms.Timer isn't a particularly accurate beast. The application has a ShapeCollection which it populates with some shapes, randomly initialized. It has a timer that fires every 40 milliseconds to update all the shapes by calling their Tick methods and an OnPaint handler that draws all the shapes. The following listing shows the Form based object that does all this. Note also that the OnSize method is overidden to redefine the limits of each of the objects so that they remain in sight as you resize the form. /// <summary> /// Manages a list of animated objects. /// </summary> public class Form1 : System.Windows.Forms.Form {
/// <summary> /// Required designer variable. /// </summary> private System.ComponentModel.Container components = null;
/// <summary> /// A collection of Shape based objects /// </summary> ShapeCollection _shapes=new ShapeCollection();
/// <summary> /// The message-driven timer /// </summary> System.Windows.Forms.Timer wt=new System.Windows.Forms.Timer();
/// <summary> /// Constructs a new form /// </summary> public Form1() { // // Required for Windows Form Designer support // InitializeComponent();
//This form is double buffered SetStyle( ControlStyles.AllPaintingInWmPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true);
//A random number generator for the initial setup Random r=new Random();
//We create 100 objects for(int c=0; c<100; c++) { Shape s=null; //using 1 of 3 randomly chosen shape types switch(r.Next(3)) { case 0: s=new Square(); break; case 1: s=new Pentagon(); break; case 2: s=new Star(); break; }
//The shape is initialized with random parameters. s.Limits=this.ClientRectangle; s.Location=new Point(r.Next(this.ClientRectangle.Width),r.Next(this.ClientRectangle.Height)); s.Size=new Size(1+r.Next(100), 1+r.Next(100)); s.BackColor=Color.FromArgb(r.Next(255),r.Next(255),r.Next(255)); s.ForeColor=Color.FromArgb(r.Next(255),r.Next(255),r.Next(255)); s.RotationDelta=(float)r.Next(20); s.Transparency=(float)r.NextDouble(); s.LineThickness=r.Next(10); s.Vector=new Size(-10+r.Next(20),-10+r.Next(20));
//and added to the list of shapes this._shapes.Add(s); }
//set up the timer so that animation can take place wt.Interval=40; wt.Tick+=new EventHandler(wt_Tick); wt.Enabled=true;
}
/// <summary> /// Clean up any resources being used. /// </summary> protected override void Dispose( bool disposing ) { if( disposing ) { if (components != null) { components.Dispose(); } } base.Dispose( disposing ); }
#region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { // // Form1 // this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.BackColor = System.Drawing.Color.White; this.ClientSize = new System.Drawing.Size(292, 266); this.Name = "Form1"; this.Text = "Form1";
} #endregion
/// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { Application.Run(new Form1()); }
private void wt_Tick(object sender, EventArgs e) { foreach(Shape s in this._shapes) s.Tick(); Invalidate(); }
protected override void OnPaint(PaintEventArgs e) { foreach(Shape s in this._shapes) s.Draw(e.Graphics); }
protected override void OnClosing(CancelEventArgs e) { wt.Enabled=false; wt.Dispose(); base.OnClosing (e); }
protected override void OnClosed(EventArgs e) { base.OnClosed (e); }
/// <summary> /// Updates the limits of all current shapes so that they don't disappear off-screen /// </summary> /// <param name="e"></param> protected override void OnSizeChanged(EventArgs e) { foreach(Shape s in this._shapes) s.Limits=this.ClientRectangle; base.OnSizeChanged (e); }
}
'/ <summary> '/ Manages a list of animated objects. '/ </summary> Public Class Form1 Inherits System.Windows.Forms.Form
'/ <summary> '/ Required designer variable. '/ </summary> Private components As System.ComponentModel.Container = Nothing
'/ <summary> '/ A collection of Shape based objects '/ </summary> Private _shapes As New ShapeCollection
' 'ToDo: Error processing original source shown below ' '/ <summary> '/ The message-driven timer '/ </summary> Private wt As New System.Windows.Forms.Timer
'/ <summary> '/ Constructs a new form '/ </summary> Public Sub New() ' ' Required for Windows Form Designer support ' InitializeComponent()
'This form is double buffered SetStyle(ControlStyles.AllPaintingInWmPaint Or ControlStyles.DoubleBuffer Or ControlStyles.ResizeRedraw Or ControlStyles.UserPaint, True)
'A random number generator for the initial setup Dim r As New Random
'We create 100 objects Dim c As Integer For c = 0 To 99 Dim s As Shape = Nothing 'using 1 of 3 randomly chosen shape types Select Case r.Next(3) Case 0 s = New Square Case 1 s = New Pentagon Case 2 s = New Star End Select
'The shape is initialized with random parameters. s.Limits = Me.ClientRectangle s.Location = New Point(r.Next(Me.ClientRectangle.Width), r.Next(Me.ClientRectangle.Height)) s.Size = New Size(1 + r.Next(100), 1 + r.Next(100)) s.BackColor = Color.FromArgb(r.Next(255), r.Next(255), r.Next(255)) s.ForeColor = Color.FromArgb(r.Next(255), r.Next(255), r.Next(255)) s.RotationDelta = CSng(r.Next(20)) s.Transparency = CSng(r.NextDouble()) s.LineThickness = r.Next(10) s.Vector = New Size(-10 + r.Next(20), -10 + r.Next(20))
'and added to the list of shapes Me._shapes.Add(s) Next c
'set up the timer so that animation can take place ' wt.Interval = 40 AddHandler wt.Tick, AddressOf wt_Tick wt.Enabled = True End Sub 'New '
'/ <summary> '/ Clean up any resources being used. '/ </summary> Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then If Not (components Is Nothing) Then components.Dispose() End If End If MyBase.Dispose(disposing) End Sub 'Dispose
#Region "Windows Form Designer generated code"
'/ <summary> '/ Required method for Designer support - do not modify '/ the contents of this method with the code editor. '/ </summary> Private Sub InitializeComponent() ' ' Form1 ' Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13) Me.BackColor = System.Drawing.Color.White Me.ClientSize = New System.Drawing.Size(292, 266) Me.Name = "Form1" Me.Text = "Form1" End Sub 'InitializeComponent #End Region
'/ <summary> '/ The main entry point for the application. '/ </summary> <STAThread()> _ Shared Sub Main() Application.Run(New Form1) End Sub 'Main
Private Sub wt_Tick(ByVal sender As Object, ByVal e As EventArgs) Dim s As Shape For Each s In Me._shapes s.Tick() Next s Invalidate() End Sub 'wt_Tick
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) Dim s As Shape For Each s In Me._shapes s.Draw(e.Graphics) Next s End Sub 'OnPaint
Protected Overrides Sub OnClosing(ByVal e As CancelEventArgs) wt.Enabled = False wt.Dispose() MyBase.OnClosing(e) End Sub 'OnClosing
Protected Overrides Sub OnClosed(ByVal e As EventArgs) MyBase.OnClosed(e) End Sub 'OnClosed
'/ <summary> '/ Updates the limits of all current shapes so that they don't disappear off-screen '/ </summary> '/ <param name="e"></param> Protected Overrides Sub OnSizeChanged(ByVal e As EventArgs) Dim s As Shape For Each s In Me._shapes s.Limits = Me.ClientRectangle Next s MyBase.OnSizeChanged(e) End Sub 'OnSizeChanged End Class 'Form1
Summary.As you study the code for the application you will notice the simplicity of the OnPaint, OnSize and wt_Tick methods. They all perform the simple action of iterating over the objects in the list and performing some very simple function. The complication of transforming the Graphics object or managing movement is left entirely to the objects themselves. This is how Object Oriented Programming is used at its best in a graphical system. The application source code can be downloaded in ZIP form from this link. The project contains both C# and VB versions. Try adjusting the number of objects created in the initial Form1 constructor by changing the maximum value of "c" in the for loop. Depending on your system speed and graphics card capabilities you will find that many hundreds or even a few thousand shapes will bounce around the screen quite happily. Figure 1 shows the Funimation application in action.
Figure 1. Fun with animation. Return to Windows Forms Tips and Tricks.
|