The wrapping image game trick
December 25, 2009
- Download EDs_Binary - 1.18 MB
- Download EDs_Source - 212.28 KB
- Download Endless_Ocean_Binary - 3.55 MB
- Download Endless_Ocean_Source - 244.36 KB
Introduction
Ever wondered how long is the moving background picture of a video game? You must have noticed that at some point, it just keeps repeating and this is what we are going to learn in this article.. Two days ago I was in a park with my toddler when I noticed the way the footpath tiles are placed making up an endless repeating shapes and that's when I decided to look up this problem in a mathematical approach.
The illusion

Manipulating images through colors and textures has always been used in tricking our minds into giving an implied illusion.
To make the tiling illusion, the first problem to tackle is to make a picture seamlessly "tileable" so that its ends fit to make up part of the overall picture by repeating part of it.. In other words, it's about placing a piece of texture next to each other without having a visible seam and making sure not to have any eye-catching repeating spots in the overall picture.
It might be useful to say that not any picture will do, you need to have some sort of a repetitive pattern at least at the edges of the picture.
Now the picture have been chosen, make sure you cover two basic requirements:
- The picture should be taken in a 90° angle.
- There shouldn't be much visible lightning scales in different parts of the picture.
TIP: To easily spot different lightning scales in a picture, try applying a gray-scale filter and then darken/lighten the different part as required.
Now that our picture is ready, we have two options in the way we may tile it, we might want to tile it only in one dimension i.e. horizontally/Vertically so the picture fits in a row/column or we could make it completely tileable by making sure that all ends match up. However, if you're intending to make a fully tileable picture, you'd better make it a perfect square of a size based on a power of 2 (i.e. 2^5 = 32) For two main reasons:
- Many commercial imaging filters (most notably clouds) will create already seamless tiles, but only if the original image is based on a power of 2.
- Powers of two are easy to manage and check when it comes to pictures and even your monitor resolution is based on a power of two!
Background
To make the picture tileable, the first thing you need to do is to offset it horizontally and/or vertically preferably by 50% of its original dimensions, this is best demonstrated by means of a picture. So in a picture like this one, the creepy alien seem to be nudged over from the picture to reappear on the other side of it. “Sorry Bob ;p”
Now we made sure the edges match seamlessly, let’s see how the endless wrapping works.. We need to look at the picture in a different way recognizing the over all image as a beam of columns and rows of pixels those when meet will make a perfect cylinder.
Now, to make an illusion of a picture moving to the right, we simply iterate through the columns of the picture taking the most right column or columns depending on how smooth the movement of the picture should be (the more columns you take out in a single movement, the less smoothness the movement of the picture will have) we take out that column(s), shift the rest of the picture to exactly cover the gap created by the taken column(s) and then we reattach those columns back to the picture but to the gap created in the beginning of the picture when we moved (Clip1) to cover for the taken out columns (Clip2) in the first place.
The following pictures shows the movement technique by indexing the pixels of a sample picture of a 4X4
Notice the shift of the indexes in the blue to move the respective image from left to right and the green one to give the illusion of a downward moving picture. Also notice that if more than one column was moved, the series still maintains to keep ordered in a contiguous manner.
Using the code
I'm going to explain only the endless ocean solution as it covers the wrapping trick for both dimensions.
Note that you can change the direction of both samples by holding down the Control key and pressing the respective Arrow key.
To move the picture, we first need to clip it as explained above and here is the function to do it
private Bitmap Crop(Bitmap srcBitmap, Rectangle rectClip)
{
Bitmap tmpBmp = new Bitmap(rectClip.Width, rectClip.Height);
Graphics g = Graphics.FromImage(tmpBmp);
g.DrawImage(srcBitmap, 0, 0, rectClip, GraphicsUnit.Pixel);
g.Dispose();
return tmpBmp;
}
The function takes the source bitmap as its first parameter and the rectangle to cut out considering the start location of the clipping and the dimensions then returns that part as a bitmap object.
We also used some user control inherited from the PictureBox class to support background transparency.
using System;
using System.Windows.Forms;
namespace UI_Test
{
public class TransparentPictureBox : PictureBox
{
public TransparentPictureBox()
{
this.SetStyle(ControlStyles.Opaque, true);
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
}
protected override CreateParams CreateParams
{
get
{
CreateParams parms = base.CreateParams;
parms.ExStyle |= 0x20;
return parms;
}
}
}
}
Once compiled, you'll see the control added to you solution components list at the top of the toolbox pane with the label TransparentPictureBox
now let's take a look at the entire code listing and discuss each part separately.
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Media;
namespace SampleGameII
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private enum Style
{
Left,
Right,
Up,
Down
}
private void tmr_MoveBG_Tick(object sender, EventArgs e)
{
if (!chReverse.Checked)
PIC1.Image = MoveImage((Bitmap)PIC1.Image.Clone(), iSpeed, sDirection);
else
{
if (iSpeed >= Properties.Resources.Ocean.Width - iSpeed)
iSpeed = 3;
else
iSpeed += 3;
PIC1.Image = MoveImage(Properties.Resources.Ocean, iSpeed, sDirection);
}
Spaceship.Refresh();
}
private Bitmap Crop(Bitmap srcBitmap, Rectangle rectClip)
{
Bitmap tmpBmp = new Bitmap(rectClip.Width, rectClip.Height);
Graphics g = Graphics.FromImage(tmpBmp);
g.DrawImage(srcBitmap, 0, 0, rectClip, GraphicsUnit.Pixel);
g.Dispose();
return tmpBmp;
}
private Bitmap MoveImage(Bitmap srcBitmap, int iMargin, string sDirection)
{
Bitmap tmpBmp, Clip1, Clip2;
tmpBmp = Clip1 = Clip2 = null;
switch (sDirection)
{
case "Left":
{
tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
Clip1 = Crop(
srcBitmap,
new Rectangle(
new Point(0, 0),
new Size(srcBitmap.Width - iMargin, srcBitmap.Height)));
Clip2 = Crop(
srcBitmap,
new Rectangle(
new Point(srcBitmap.Width - iMargin, 0),
new Size(iMargin, srcBitmap.Height)));
Graphics g = Graphics.FromImage(tmpBmp);
if (!chReverse.Checked)
{
g.DrawImage(Clip1, iMargin, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
g.DrawImage(Clip2, 0, 0, iMargin, srcBitmap.Height);
}
else
{
g.DrawImage(Clip2, iMargin, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
g.DrawImage(Clip1, 0, 0, iMargin, srcBitmap.Height);
}
g.Dispose();
break;
}
case "Right":
{
tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
Clip1 = Crop(
srcBitmap,
new Rectangle(
new Point(iMargin, 0),
new Size(srcBitmap.Width - iMargin, srcBitmap.Height)));
Clip2 = Crop(
srcBitmap,
new Rectangle(
new Point(0, 0),
new Size(iMargin, srcBitmap.Height)));
Graphics g = Graphics.FromImage(tmpBmp);
if (!chReverse.Checked)
{
g.DrawImage(Clip1, 0, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
g.DrawImage(Clip2, srcBitmap.Width - iMargin, 0, iMargin, srcBitmap.Height);
}
else
{
g.DrawImage(Clip2, 0, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
g.DrawImage(Clip1, srcBitmap.Width - iMargin, 0, iMargin, srcBitmap.Height);
}
g.Dispose();
break;
}
case "Up":
{
tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
Clip1 = Crop(
srcBitmap,
new Rectangle(
new Point(0, 0),
new Size(srcBitmap.Width, srcBitmap.Height - iMargin)));
Clip2 = Crop(
srcBitmap,
new Rectangle(
new Point(0, srcBitmap.Height - iMargin),
new Size(srcBitmap.Width, iMargin)));
Graphics g = Graphics.FromImage(tmpBmp);
if (!chReverse.Checked)
{
g.DrawImage(Clip1, 0, iMargin, srcBitmap.Width, srcBitmap.Height - iMargin);
g.DrawImage(Clip2, 0, 0, srcBitmap.Width, iMargin);
}
else
{
g.DrawImage(Clip2, 0, iMargin, srcBitmap.Width, srcBitmap.Height - iMargin);
g.DrawImage(Clip1, 0, 0, srcBitmap.Width, iMargin);
}
g.Dispose();
break;
}
case "Down":
{
tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
Clip1 = Crop(
srcBitmap,
new Rectangle(
new Point(0, iMargin),
new Size(srcBitmap.Width, srcBitmap.Height - iMargin)));
Clip2 = Crop(
srcBitmap,
new Rectangle(
new Point(0, 0),
new Size(srcBitmap.Width, iMargin)));
Graphics g = Graphics.FromImage(tmpBmp);
if (!chReverse.Checked)
{
g.DrawImage(Clip1, 0, 0, srcBitmap.Width, srcBitmap.Height - iMargin);
g.DrawImage(Clip2, 0, srcBitmap.Height - iMargin, srcBitmap.Width, iMargin);
}
else
{
g.DrawImage(Clip2, 0, 0, srcBitmap.Width, srcBitmap.Height - iMargin);
g.DrawImage(Clip1, 0, srcBitmap.Height - iMargin, srcBitmap.Width, iMargin);
}
g.Dispose();
break;
}
}
return tmpBmp;
}
int iSpeed;
string sDirection;
private void Form1_Load(object sender, EventArgs e)
{
sDirection = "Up";
iSpeed = 3;
tmr_MoveBG.Start();
}
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right
|| e.KeyCode == Keys.Up || e.KeyCode == Keys.Down)
{
sDirection = e.KeyCode.ToString();
iSpeed = 3;
tmr_MoveBG.Start();
Bitmap bm;
bm = new Bitmap(Properties.Resources.Spaceship2);
if (e.KeyCode == Keys.Right)
bm.RotateFlip((System.Drawing.RotateFlipType)Enum.Parse(typeof(System.Drawing.RotateFlipType), "Rotate90FlipNone", true));
if (e.KeyCode == Keys.Left)
bm.RotateFlip((System.Drawing.RotateFlipType)Enum.Parse(typeof(System.Drawing.RotateFlipType), "Rotate270FlipNone", true));
if (e.KeyCode == Keys.Down)
bm.RotateFlip((System.Drawing.RotateFlipType)Enum.Parse(typeof(System.Drawing.RotateFlipType), "Rotate180FlipNone", true));
Spaceship.Image = bm;
}
}
private void chReverse_CheckedChanged(object sender, EventArgs e)
{
if (!chReverse.Checked)
{
PIC1.Image = Properties.Resources.Ocean;
tmr_MoveBG.Stop();
iSpeed = 3;
tmr_MoveBG.Start();
}
}
private void tbSpeed_Scroll(object sender, EventArgs e)
{
tmr_MoveBG.Interval = 50 - (tbSpeed.Value * 5);
lblSpeed.Text = "Speed: "+ (tbSpeed.Value+1).ToString();
}
}
}
The motion illusion is initiated and carried out using the tmr_MoveBG timer. The MoveImage function is called with every tick of the timer passing a clone of the current modified Image of the picture box and assigning it back to the PictureBox control to be passed again with the next tick.
private void tmr_MoveBG_Tick(object sender, EventArgs e)
{
if (!chReverse.Checked)
PIC1.Image = MoveImage((Bitmap)PIC1.Image.Clone(), iSpeed, sDirection);
else
{
if (iSpeed >= Properties.Resources.Ocean.Width - iSpeed)
iSpeed = 3;
else
iSpeed += 3;
PIC1.Image = MoveImage(Properties.Resources.Ocean, iSpeed, sDirection);
}
Spaceship.Refresh();
}
The Move ImageImage function takes the source image as its first parameter as a Bitmap along with the margin to clip and the desired direction to move to.
The direction parameter is evaluated through a switch case block to clip and redraw the right image and then finally return it to be set back to the picturebox control of which will be taken again as a clone to be the new source image to pass to the function.
private Bitmap MoveImage(Bitmap srcBitmap, int iMargin, string sDirection)
{
Bitmap tmpBmp, Clip1, Clip2;
tmpBmp = Clip1 = Clip2 = null;
switch (sDirection)
{
case "Left":
{
tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
Clip1 = Crop(
srcBitmap,
new Rectangle(
new Point(0, 0),
new Size(srcBitmap.Width - iMargin, srcBitmap.Height)));
Clip2 = Crop(
srcBitmap,
new Rectangle(
new Point(srcBitmap.Width - iMargin, 0),
new Size(iMargin, srcBitmap.Height)));
Graphics g = Graphics.FromImage(tmpBmp);
if (!chReverse.Checked)
{
g.DrawImage(Clip1, iMargin, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
g.DrawImage(Clip2, 0, 0, iMargin, srcBitmap.Height);
}
else
{
g.DrawImage(Clip2, iMargin, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
g.DrawImage(Clip1, 0, 0, iMargin, srcBitmap.Height);
}
g.Dispose();
break;
}
case "Right":
{
tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
Clip1 = Crop(
srcBitmap,
new Rectangle(
new Point(iMargin, 0),
new Size(srcBitmap.Width - iMargin, srcBitmap.Height)));
Clip2 = Crop(
srcBitmap,
new Rectangle(
new Point(0, 0),
new Size(iMargin, srcBitmap.Height)));
Graphics g = Graphics.FromImage(tmpBmp);
if (!chReverse.Checked)
{
g.DrawImage(Clip1, 0, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
g.DrawImage(Clip2, srcBitmap.Width - iMargin, 0, iMargin, srcBitmap.Height);
}
else
{
g.DrawImage(Clip2, 0, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
g.DrawImage(Clip1, srcBitmap.Width - iMargin, 0, iMargin, srcBitmap.Height);
}
g.Dispose();
break;
}
case "Up":
{
tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
Clip1 = Crop(
srcBitmap,
new Rectangle(
new Point(0, 0),
new Size(srcBitmap.Width, srcBitmap.Height - iMargin)));
Clip2 = Crop(
srcBitmap,
new Rectangle(
new Point(0, srcBitmap.Height - iMargin),
new Size(srcBitmap.Width, iMargin)));
Graphics g = Graphics.FromImage(tmpBmp);
if (!chReverse.Checked)
{
g.DrawImage(Clip1, 0, iMargin, srcBitmap.Width, srcBitmap.Height - iMargin);
g.DrawImage(Clip2, 0, 0, srcBitmap.Width, iMargin);
}
else
{
g.DrawImage(Clip2, 0, iMargin, srcBitmap.Width, srcBitmap.Height - iMargin);
g.DrawImage(Clip1, 0, 0, srcBitmap.Width, iMargin);
}
g.Dispose();
break;
}
case "Down":
{
tmpBmp = new Bitmap(srcBitmap.Width, srcBitmap.Height);
Clip1 = Crop(
srcBitmap,
new Rectangle(
new Point(0, iMargin),
new Size(srcBitmap.Width, srcBitmap.Height - iMargin)));
Clip2 = Crop(
srcBitmap,
new Rectangle(
new Point(0, 0),
new Size(srcBitmap.Width, iMargin)));
Graphics g = Graphics.FromImage(tmpBmp);
if (!chReverse.Checked)
{
g.DrawImage(Clip1, 0, 0, srcBitmap.Width, srcBitmap.Height - iMargin);
g.DrawImage(Clip2, 0, srcBitmap.Height - iMargin, srcBitmap.Width, iMargin);
}
else
{
g.DrawImage(Clip2, 0, 0, srcBitmap.Width, srcBitmap.Height - iMargin);
g.DrawImage(Clip1, 0, srcBitmap.Height - iMargin, srcBitmap.Width, iMargin);
}
g.Dispose();
break;
}
}
return tmpBmp;
}
In order to change the direction of the image, we capture the keyCode and assign it as a string to the sDirection variable and simply flips the spaceship image to match then new direction. Notice that the background image changes its direction as the sDirection variable value changes for the timer is still running and uses it to determine the moving direction.
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right
|| e.KeyCode == Keys.Up || e.KeyCode == Keys.Down)
{
sDirection = e.KeyCode.ToString();
iSpeed = 3;
tmr_MoveBG.Start();
Bitmap bm;
bm = new Bitmap(Properties.Resources.Spaceship2);
if (e.KeyCode == Keys.Right)
bm.RotateFlip((System.Drawing.RotateFlipType)Enum.Parse(typeof(System.Drawing.RotateFlipType), "Rotate90FlipNone", true));
if (e.KeyCode == Keys.Left)
bm.RotateFlip((System.Drawing.RotateFlipType)Enum.Parse(typeof(System.Drawing.RotateFlipType), "Rotate270FlipNone", true));
if (e.KeyCode == Keys.Down)
bm.RotateFlip((System.Drawing.RotateFlipType)Enum.Parse(typeof(System.Drawing.RotateFlipType), "Rotate180FlipNone", true));
Spaceship.Image = bm;
}
}
The reversed effect is decided by checking the "Reverse Effect" check box which simply restarts the moving timer to use the new settings
private void chReverse_CheckedChanged(object sender, EventArgs e)
{
if (!chReverse.Checked)
{
PIC1.Image = Properties.Resources.Ocean;
tmr_MoveBG.Stop();
iSpeed = 3;
tmr_MoveBG.Start();
}
}
The new setting for the reverse effect is handled by switching the placement of the clips that the bigger clip (Clip 1) takes the place of the smaller one (Clip 2) and vise versa and hence the moving stretch effect to fit the clips in the wrong sizes.
if (!chReverse.Checked)
{
g.DrawImage(Clip1, iMargin, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
g.DrawImage(Clip2, 0, 0, iMargin, srcBitmap.Height);
}
else
{
g.DrawImage(Clip2, iMargin, 0, srcBitmap.Width - iMargin, srcBitmap.Height);
g.DrawImage(Clip1, 0, 0, iMargin, srcBitmap.Height);
}
Finally we determine the speed of the movement by increasing/decreasing the interval value of the timer
private void tbSpeed_Scroll(object sender, EventArgs e)
{
tmr_MoveBG.Interval = 50 - (tbSpeed.Value * 5);
lblSpeed.Text = "Speed: "+ (tbSpeed.Value+1).ToString();
}
Of course we could have increased the number of columns taken out in a clip sacrificing some of the smoothness by increasing the margin in the MoveImage function.
PIC1.Image = MoveImage((Bitmap)PIC1.Image.Clone(), iSpeed, sDirection);
Note that he iSpeed value is predetermined in this example in the form_load event along with the sDirection value.
private void Form1_Load(object sender, EventArgs e)
{
sDirection = "Up";
iSpeed = 3;
tmr_MoveBG.Start();
}
Points of Interest
A major drawback of this technique is that the newly drawn image is obscuring the overlayingPictureBox image and hence the timer keeps refreshing it with every tick which generates this inconvenient flickering of the image private void tmr_MoveBG_Tick(object sender, EventArgs e)
{
...
Spaceship.Refresh();
}
Posted by Muammar Yacoob. Posted In : C# Programming