Кретање цртежа¶
Анимације које смо до сада видели раде тако што у сваком фрејму приказују неку другу, унапред припремљену слику. Сада ћемо слике које приказујемо и померати, тако да се иста слика појављује на различитим местима у прозору, то јест креће се.
Пре него што кренемо на примере, једна општа напомена, која се односи на све будуће програме у овом приручнику.
Напомена: Када приказујемо покретне слике, може доћи до треперења слика на екрану. До тога долази зато што освежавање екрана није синхронизовано са исцртавањем, па се дешава да се екран освежава док је слика полу-нацртана. Да бисмо избегли овај непријатан ефекат, потребно је међу својствима формулара наћи логичко својство DoubleBuffered и задати му вредност true.
Енглеска реч Buffer (бафер) у овом контексту означава простор у меморији рачунара који је резервисан за неку посебну намену. Додељивањем вредности true својству DoubleBuffered изабрали смо да користимо два бафера за смештање слике. У једном баферу се врши цртање, а други се користи за приказивање слике, а након завршеног исцртавања бафери мењају улоге. На тај начин програм неће бити „ухваћен у сред цртања”, што ће смањити поменути непожељан ефекат треперења.
Ауто¶
Направити анимацију у којој се ауто наизменично креће слева на десно а затим с десна на лево. Ауто треба да се помера у једном смеру до потпуног изласка из прозора.
Променљиве које описују сцену су:
Реалне променљиве AutoX и AutoY, које задају положај аута (тачније, горњег левог угла слике аута).
Реална променљива AutoVX, која представља брзину аута изражену у пикселима у секунди. Ова променљива ће при окретању аута узимати вредност супротну претходној, јо јест њена вредност ће мењати знак.
Целобројна променљива AutoSmer, која нам гвоори на коју страну се ауто креће. Ова променљива ће узимати вредности 0 и 1, а користиће се као индекс у низу од две слике аута (једна слика је ауто окренут на десно, а друга на лево).
Мењање вредности ових променљивих утиче на приказивање текуће или будућих слика. Осим њих, међу променљиве - чланице класе ћемо додати и реалну променљиву DT, коју нећемо мењати након постављања почетне вредности. Променљива DT ће представљати време између два фрејма изражено у секундама.
AutoX = 0;
AutoY = ClientSize.Height - slika[0].Height;
DT = timer1.Interval * 0.001f; // vreme jednog frejma u sekundama
AutoVX = 200; // brzina auta u pikselima u sekundi
Поменимо и један технички детаљ. Дата нам је само слика аута који се креће на десно. Другу слику (ауто окренут на лево) можемо да направимо помоћу неког од програма за обраду слике и да је додамо у ресурсе. То, међутим, није неопходно, јер слику можемо да обрнемо и у нашем C# програму. Користимо исти ресурс Properties.Resources.auto (слика аута који је окренут на десно) да слици дамо почетну вредност, а затим на слику применимо функцију RotateFlip, да бисмо добили слику другачије окренутог аута:
Image autoNalevo = (Image)Properties.Resources.auto;
autoNalevo.RotateFlip(RotateFlipType.RotateNoneFlipX);
Комплетан програм изгледа овако:
using System;
using System.Drawing;
using System.Windows.Forms;
namespace AutoLevoDesno
{
public partial class Form1 : Form
{
Image[] slika;
float AutoX, AutoY, AutoVX, DT;
int AutoSmer = 0; // koristi se kao indeks u nizu slika
public Form1()
{
InitializeComponent();
ClientSize = new Size(400, 300);
Text = "Auto";
BackColor = Color.SkyBlue;
Image autoNalevo = (Image)Properties.Resources.auto;
autoNalevo.RotateFlip(RotateFlipType.RotateNoneFlipX);
slika = new Image[] { Properties.Resources.auto, autoNalevo };
AutoX = 0;
AutoY = ClientSize.Height - slika[0].Height;
DT = timer1.Interval * 0.001f; // vreme jednog frejma u sekundama
AutoVX = 200; // brzina auta u pikselima u sekundi
}
private void timer1_Tick(object sender, EventArgs e)
{
AutoX += AutoVX * DT;
if (AutoX > ClientSize.Width || AutoX < -slika[0].Width)
{
AutoSmer = 1 - AutoSmer;
AutoVX = -AutoVX;
}
Invalidate();
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.DrawImage(slika[AutoSmer], AutoX, AutoY);
}
}
}
Као и раније, имамо функцију timer1_Tick, која ажурира стање сцене за сваки фрејм. Оно што је ново у овом примеру је да се положај слике мења из фрејма у фрејм.
Слику приказујемо тако да се њен горњи леви угао појави у тачки (AutoX, AutoY). Да би се ауто кретао, у сваком фрејму на x координату слике додајемо вредност AutoVX * DT. Када је вредност AutoVX позитивна, ауто се креће на десно, а када је негативна, ауто се креће на лево.
При томе још водимо рачуна да када ауто оде сувише десно или сувише лево (изађе из прозора), да променимо смер кретања аута. На тај начин ауто почиње да се враћа и да се постепено поново појављује на слици.
if (AutoX > ClientSize.Width || AutoX < -slika[0].Width)
{
AutoSmer = 1 - AutoSmer;
AutoVX = -AutoVX;
}
Обратите пажњу и на то да пролазак целог аута кроз леву ивицу прозора проверавамо условом \(AutoX < -slika[0].Width\), док је за десну ивицу прозора услов \(AutoX > ClientSize.Width\). Следећа слика вам може помоћи да разумете зашто је то тако. На слици је горњи леви угао слике аута у оба положаја означен црвеним кружићем.
Као што смо у примеру са аутом померали готову слику, на сличан начин можемо да померамо и цртеже које сами нацртамо. При томе и готову слику и цртеж можемо да померамо у било ком смеру. Ево једног примера у коме ћемо нацртани круг померати у разним правцима:
Билијар¶
Направити анимацију билијарске кугле која се правилно одбија о ивице прозора, а између узастопних одбијања се креће равномерно праволинијски.
Погледајте како овде изгледа функција timer1_Tick, а како Form1_Paint.
using System;
using System.Drawing;
using System.Windows.Forms;
namespace Bilijar
{
public partial class Form1 : Form
{
float CX, CY;
float R = 15, DX = 5, DY = 3;
private void timer1_Tick(object sender, EventArgs e)
{
CX += DX;
CY += DY;
if (CX - R < 0 || CX + R > ClientSize.Width) // ako je kugla "probila" levu ili desnu ivicu prozora
DX = -DX; // menjamo joj smer po x osi (ako je isla nadesno, ici ce nalevo i obrnuto)
if (CY - R < 0 || CY + R > ClientSize.Height) // ako je kugla "probila" gornju ili donju ivicu prozora
DY = -DY; // menjamo joj smer po y osi (ako je isla nadole, ici ce nagore i obrnuto)
Invalidate();
}
public Form1()
{
InitializeComponent();
ClientSize = new Size(800, 400);
Text = "Bilijar";
BackColor = Color.DarkGreen;
CX = ClientSize.Width / 2;
CY = ClientSize.Height / 2;
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.FillEllipse(new SolidBrush(Color.White), CX - R, CY - R, 2 * R, 2 * R);
}
}
}
Услове за проверу да ли је билијарска кугла дотакла неку ивицу прозора вреди погледати пажљивије. Крајња десна тачка кугле има x координату једнаку \(cx+r\). Ако би та вредност постала једнака ширини прозора, то би значило да кугла додирује десну ивицу прозора, а ако је \(cx + r > ClientSize.Width\), значи да је је кугла бар делом већ прошла десну ивицу прозора. У том случају наредбом \(dx = -dx\) постижемо да се од следећег фрејма x координати кугле додаје супротна вредност у односу на досадашњу, односно да се убудуће кугла помера за по 3 пиксела на лево. То ће изгледати као да се кугла одбила од десне ивице прозора.
Приметимо још један детаљ: уместо \(cx + r > ClientSize.Width\) могли смо да користимо и \(cx + r >= ClientSize.Width\) и програм би радио скоро исто. Међутим, пошто се кугла не помера за по један пиксел, не би ваљало да смо користили услов \(cx + r == ClientSize.Width\), јер би тада могло да се догоди да кугла прескочи положај који проверавамо и прође кроз ивицу прозора.
Детаљно смо анализирали случај десне ивице прозора, а исто размишљање је при писању програма примењено и на остале ивице. Укупан ефекат двеју if наредби је утисак да се кугла одбија од сваке ивице прозора.
Проверите да ли сте ово разумели тако што ћете одговорити на следећа питања.
Кретање цртежа - питања¶
Q-26: Изрази (cx - r < 0), (cx + r > ClientSize.Width), (cy - r < 0), (cy + r > ClientSize.Height) значе да је кугла из претходног примера у потпуности прошла неку од ивица прозора. Поређајте ивице тако да редом одговарају овим условима.
лева ивица
десна ивица
горња ивица
доња ивица
- на десно
- Покушајте поново
- на горе
- Покушајте поново
- на лево
- Тачно
- на доле
- Покушајте поново
Q-27: На коју страну се помера слика додавањем негативне вредности на њену x координату?
- if (x + sl_sirina < 0)
- Покушајте поново
- if (y + sl_visina < 0)
- Тачно
- if (x < 0)
- Покушајте поново
- if (y < 0)
- Покушајте поново
Q-28: Нека су димензије дате слике sl_sirina и sl_visina, а њен горњи леви угао (x, y). Како проверавамо да ли је слика у потпуности прошла кроз горњу ивицу прозора и више се не види ни један њен део?
Q-29: Нека је w ширина прозора, im_w ширина слике, а (x, y) горњи леви угао слике. Поређајте логичке услове тако да редом имају наведена значења. А: "Слика је изашла кроз леву ивицу прозора", Б: "Слика је почела да излази кроз леву ивицу прозора", В: "Слика је изашла кроз десну ивицу прозора", Г: "Слика је почела да излази кроз десну ивицу прозора".
x+im_w<0
x<0
x>im_w
x+im_w>w
- x = w; dx = -10;
- Тачно
- x = w + im_w; dx = -10;
- Не, то је предалеко од десне ивице.
- x = w - im_w; dx = -10;
- Не, тако је цела слика већ у прозору.
- x = w + im_w; dx = 10;
- Не, слика је предалеко и још ће наставити да се удаљава.
Q-30: Нека је w ширина прозора, im_w ширина слике, (x, y) горњи леви угао слике, а dx величина за коју ће се касније мењати x координата слике. Помоћу којих наредби ће слика почети да се појављује улазећи у прозор кроз десну ивицу?
Утисак покрета може да се постигне и на дисплејима врло ниске резолуције.
Дисплеји слични овом се могу видети на местима где људи гледају на њих са веће даљине и где је величина дисплеја битнија од резолуције. Такав је случај на стадионима (приказ резултата такмичара), неким аутобуским станицама (обавештења о поласцима и доласцима), или у аутобусима (обавештење о следећој станици, тачно време, датум, температура и слично).
Да бисмо на рачунару имитирали рад оваквог дисплеја, треба пре анимације да израчунамо положаје „светлећих диода”. Сцена се затим може описати осветљајима појединих диода. Кроз следећи пример можете добити ближу идеју како такве анимације функционишу.
Комета¶
Овде ћемо симулирати једноставан дисплеј од 20 светлећих диода распоређених у само један ред. Преласком на следећи фрејм, следећа диода са десне стране се укључује на максимум, а све претходне добијају мало тамнију нијансу (последња се тиме потпуно гаси).
То значи да је у овом примеру сцена потпуно описана само једним бројем а то је позиција (редни број) диоде која најјаче светли. У програму је то променљива IndeksVodeceDiode. У функцији timer1_Tick је довољно да ову променљиву повећавамо за 1 по модулу 20.
Да бисмо имплементирали исцртавање, треба да одлучимо колики ће да буде „реп” ове комете. Ми смо изабрали реп дужине десет, што значи да ће у сваком фрејму бити исцртано 10 кругова различите осветљености.
Пре анимације је потребно још израчунати положаје свих диода и нивое осветљености који ће им касније бити додељивани. Следи комплетан код.
using System;
using System.Drawing;
using System.Windows.Forms;
namespace DiodeKometa
{
public partial class Form1 : Form
{
const int MAX_KASNJENJE = 10; // broj dioda koje svetle (takodje broj frejmova tokom kojih dioda svetli)
const int BROJ_DIODA = 20; // ukupan broj dioda na displeju
Brush[] Cetka = new Brush[MAX_KASNJENJE + 1]; // sivi tonovi od bele do crne
int[] X = new int[BROJ_DIODA]; // x koordinate centara dioda
int Y; // y koordinata centra svih dioda
int R; // poluprečnik jedne diode
int IndeksVodeceDiode; // redni broj poslednje uključene diode
public Form1()
{
InitializeComponent();
ClientSize = new Size(400, 100);
Text = "Diode";
BackColor = Color.Black;
R = ClientSize.Width / (2 * BROJ_DIODA);
for (int i = 0; i <= MAX_KASNJENJE; i++)
{
int nijansa = (int)(255.0f * (1.0f - (float)i / MAX_KASNJENJE));
Cetka[i] = new SolidBrush(Color.FromArgb(nijansa, nijansa, nijansa));
}
for (int i = 0; i < BROJ_DIODA; i++)
X[i] = R + i * 2 * R;
Y = ClientSize.Height / 2;
IndeksVodeceDiode = 0;
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
// za svaku diodu koja trenutno svetli
for (int kasnjenje = 0; kasnjenje <= MAX_KASNJENJE; kasnjenje++)
{
// odredimo redni broj diode koja je uključena pre 'kasnjenje' frejmova, a zatim je prikazimo
int iDioda = (IndeksVodeceDiode + BROJ_DIODA - kasnjenje) % BROJ_DIODA;
g.FillEllipse(Cetka[kasnjenje], X[iDioda] - R, Y - R, 2 * R, 2 * R);
}
}
private void timer1_Tick(object sender, EventArgs e)
{
IndeksVodeceDiode = (IndeksVodeceDiode + 1) % BROJ_DIODA; // prelazimo na narednu diodu
Invalidate();
}
}
}
Вероватно сте добили разне идеје о томе шта бисте све могли да шетате по екрану. Ако нисте, можете на пример да преправите један од претходних примера, у коме се променом слика постиже ефекат као да анимирани лик трчи. Преправите га тако да се и положај приказиваних слика мења, што ће утисак кретања учинити још уверљивијим.
Ако слике правите или преузимате са интернета (пазите на ауторска права), не заборавите да подесите транспарентност за боју позадине слика које користите.