PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C++

Krzywe Lissajous Algorytm animacji Odwracający tryb rysowania Program w C++ Kopiowanie fragmentów obrazu Program w C++ (wersja 2) Poprzedni przykład Następny przykład Program w Visual C# Kontakt

Krzywe Lissajous

Dwa niezależne drgania harmoniczne punktu material­nego w kierun­kach wzajemnie prostopa­dłych można opisać równa­niami przedsta­wiającymi zależność jego przemie­szczenia w czasie t:

w których A1A2 są amplitu­dami (maksymal­nymi wychyle­niami), ω1ω2 częstotli­wościami, a φ1φ2 fazami początko­wymi tych drgań. W wyniku złożenia obu drgań uzyskuje się intere­sujące kształty toru wypadko­wego ruchu punktu zwane figurami lub krzywymi Lissajous. Ich obrazy można otrzymać na ekranie oscylo­skopu, a także mechani­cznie za pomocą specjal­nego wahadła zbudowa­nego z lejka napełnio­nego piaskiem, wykonu­jącego drgania jedno­cześnie w dwóch płaszczy­znach i tworzą­cego krzywą z piasku.

Algorytm animacji

Powstawanie krzywej Lissajous zademon­strujemy na ekranie monitora kompute­rowego za pomocą krótkiego programu konsolo­wego w C++ korzysta­jącego biblio­teki WinBGI. Do genero­wania krzywej będą w nim używane zmienne rzeczy­wiste t i dt oznacza­jące czas i stały odstęp pomiędzy dwoma kolejnymi momentami czasowymi, zmienne całkowite x i y określa­jące bieżące położenie ekranowe drgającego punktu material­nego oraz funkcja obliczXY wyzna­czająca ich wartości w momencie t. Podstawowy fragment programu rysowania krzywej jest bardzo prosty:

obliczXY(t = 0, x, y);
moveto(x, y);
while (!kbhit())
{
    Sleep(10);
    obliczXY(t += dt, x, y);
    lineto(x, y);
}

Najpierw oblicza się położenie punktu w momencie początkowym zero i przenosi w to miejsce pisak. Następnie w pętli while wstrzymuje się na chwilę (przyjęto 10 milisekund) wykonanie programu, po czym przechodzi się do kolejnego momentu czasowego przesu­niętego o przyrost dt względem poprze­dniego, wylicza nowe położenie punktu i rysuje odcinek symboli­zujący fragment przebytej przez niego drogi w ostatnim odcinku czasowym. Dzięki wstrzymy­waniu przebiegu programu na początku każdego kroku pętli uzyskuje się efekt animacji.

Odwracający tryb rysowania

Wrażenie ruchu punktu można spotęgować, rysując wokół niego pogrubioną linią mały kwadracik tuż przed wywołaniem funkcji Sleep i likwi­dując go po jej wywołaniu. Można to zrobić za pomocą dwukro­tnego wywołania funkcji rectangle, która rysuje prostokąt o określo­nych współrzę­dnych lewego górnego i prawego dolnego rogu. Przed naryso­waniem kwadracika należy włączyć odwraca­jący tryb ryso­wania (XOR_PUT) i pogru­bienie linii (THICK_WIDTH), a po powtórnym naryso­waniu go przywrócić normalną grubość linii (NORM_WIDTH) i normalny tryb ryso­wania (COPY_PUT):

setwritemode(XOR_PUT);
setlinestyle(SOLID_LINE, 0, THICK_WIDTH);
rectangle(x - 2, y - 2, x + 2, y + 2);
Sleep(PRZERWA);
rectangle(x - 2, y - 2, x + 2, y + 2);
setlinestyle(SOLID_LINE, 0, NORM_WIDTH);
setwritemode(COPY_PUT);

Tryb odwracający polega na wykony­waniu operacji XOR (alterna­tywa rozłączna, różnica symetry­czna) między bitami pamięci ekranu a bitami rysowanych linii (tutaj brzegu kwadracika). Dwukrotne rysowanie tej samej linii w tym trybie powoduje wymazanie jej, czyli przywró­cenie ekranowi jego pierwo­tnego wyglądu.

Program w C++

Pełny program w C++, który wczytuje z klawia­tury parametry dwóch drgań harmoni­cznych wzajemnie prostopa­dłych, a następnie prezentuje w postaci animacji grafi­cznej efekt ich składania, ma następu­jącą postać:

#include <iostream>
#include <cmath>
#include <graphics.h>

using namespace std;

const int PRZERWA = 10;     // Szybkość animacji (ms)
const double WSP = 0.04;    // Współczynnik skalowania
const int DX = 320;         // Współrzędna x środka okna
const int DY = 240;         // Współrzędna y środka okna

double A1, A2, Omega1, Omega2, Fi1, Fi2;

void czytajDane(char Os, int Max, double &A, double &Omega, double &Fi)
{
    int faza;
    cout << "Drgania w kierunku osi " << Os
         << "\n--------------------------\n"
         << "  Amplituda (0 - " << Max << "): ";
    cin >> A;
    cout << "  Częstotliwość (> 0): ";
    cin >> Omega;
    cout << "  Faza początkowa (°): ";
    cin >> faza;
    Fi = M_PI * faza / 180;
}

void obliczXY(double t, int &x, int &y)
{
    x = int(A1 * cos(Omega1 * t + Fi1)) + DX;
    y = DY - int(A2 * cos(Omega2 * t + Fi2));
}

int main()
{
    czytajDane('x', DX, A1, Omega1, Fi1);
    czytajDane('y', DY, A2, Omega2, Fi2);
    double t, dt = WSP / max(Omega1 + Omega2, 1.0);
    initwindow(2 * DX, 2 * DY, "Krzywa Lissajous");
    int x, y;
    obliczXY(t = 0, x, y);
    moveto(x, y);
    while (!kbhit())
    {
        setwritemode(XOR_PUT);
        setlinestyle(SOLID_LINE, 0, THICK_WIDTH);
        rectangle(x - 2, y - 2, x + 2, y + 2);
        Sleep(PRZERWA);
        rectangle(x - 2, y - 2, x + 2, y + 2);
        setlinestyle(SOLID_LINE, 0, NORM_WIDTH);
        setwritemode(COPY_PUT);
        obliczXY(t += dt, x, y);
        lineto(x, y);
    }
    closegraph();
    return 0;
}

Wywoły­wana dwukrotnie w funkcji main funkcja czytajDane wczytuje amplitudę, często­tliwość i fazę począ­tkową drgania harmoni­cznego. Jej dwa pierwsze argumenty pełnią rolę informa­cyjną: znak określający nazwę osi, wzdłuż której odbywa się ruch, i maksymalna wartość amplitudy. Pozostałe trzy reprezen­tują wynik działania funkcji, dlatego są przeka­zywane przez refe­rencję. Podawanie fazy począ­tkowej w stopniach jest wygodniejsze dla użytko­wnika, toteż funkcja przelicza wprowa­dzoną wartość na radiany.

Wartość przyrostu czasowego dt jest po wczytaniu danych wyznaczana jako odwrotnie proporcjo­nalna do sumy częstotli­wości obu drgań. Współ­czynnik WSP został dobrany empiry­cznie dla obszaru roboczego okna 640x480 pikseli. Ustalenia te zapewniają do pewnego stopnia jednakową szybkość animacji dla różnych parametrów drgań.

Poniższy rysunek przedstawia krzywą Lissajous wygene­rowaną przez program dla amplitud A1=250A2=225, częstotli­wości ω1=9ω2=7 oraz faz początko­wych φ1=0oφ2=30o. Jak widać, drgający punkt materialny ozna­czony małym kwadra­cikiem zaczął drugi raz przebiegać po krzywej (nieco pogru­biony fragment toru drugiego obiegu).

Kopiowanie fragmentów obrazu

Innym przedstawieniem drgającego punktu material­nego jest obrazek (bitmapa) przechowy­wany w pamięci opera­cyjnej komputera i kopiowany na ekran (do okna grafi­cznego). Za każdym razem przed takim skopio­waniem fragment ekranu, który ma być przez obrazek zamazany, powinien być skopio­wany do pamięci, by można go było odtwo­rzyć, gdy pozycja punktu ma zostać zmieniona. Najprostszy schemat takiej animacji wygląda nastę­pująco:

obliczXY(t = 0, x, y);
moveto(x, y);
while (!kbhit())
{
    ...             // Kopiuj fragment ekranu do pamięci
    ...             // Kopiuj obrazek z pamięci na ekran
    Sleep(PRZERWA);
    ...             // Odtwórz zamazany fragment ekranu
    obliczXY(t += dt, x, y);
    lineto(x, y);
}

Dostępne w WinBGI funkcje kopiowania obrazu z ekranu do pamięci i z pamięci na ekran działają na prosto­kątnych wycinkach ekranu. I tak, funkcja getimage zapisuje prosto­kątny fragment ekranu do pamięci, a putimage przesyła prosto­kątny obraz z pamięci na ekran. Obie wymagają podania wskaźnika na obszar pamięci, który można traktować jako tablicę jednowy­miarową bajtów. Ich liczbę wyznacza funkcja imagesize zwraca­jąca wartość typu unsigned int. Argu­mentami funkcji są współ­rzędne lewego górnego i prawego dolnego rogu prosto­kątnego wycinka ekranu. Gdy wiadomo, ile bajtów ma zajmować ta tablica, można jej przydzielić dynami­cznie pamięć za pomocą operatora new i skopiować do niej wycinek ekranu. Korzystając z tych narzędzi, możemy po naryso­waniu pięciora­miennej gwiazdki na białym tle przy lewym górnym rogu obszaru roboczego okna grafi­cznego wyobraża­jącej drgający punkt materialny zapisać jej obraz w tablicy dynami­cznej pStar:

const int R = 7;    // Długość ramienia gwiazdki
...
setbkcolor(WHITE);
cleardevice();
setcolor(BLUE);
setfillstyle(SOLID_FILL, LIGHTRED);
gwiazdka(5, R, R, R, R / 2, M_PI / 2);
unsigned int nStar = imagesize(0, 0, 2 * R, 2 * R);
byte *pStar = new byte[nStar];
getimage(0, 0, 2 * R, 2 * R, pStar);
cleardevice();

Argumentami wywołania funkcji gwiazdka są: liczba ramion gwiazdki, współ­rzędne jej środka, długości promieni dwóch koncentry­cznych okręgów, na których naprze­miennie leżą wierz­chołki gwiazdki, i kąt jej obrotu względem osi x (90o). Kopiowany przez funkcję getimage prosto­kątny fragment ekranu identy­fikują współ­rzędne jego dwóch skrajnych rogów. Po zapamię­taniu obrazka gwiazdki obszar roboczy okna zostaje ponownie wyczyszczony.

Po tych wstępnych przygotowaniach możemy przejść do uzupeł­nienia przedsta­wionego wyżej schematu animacji. W każdym jej kroku do chwilo­wego przecho­wywania zamazy­wanego przez obrazek gwiazdki fragmentu ekranu i odtwa­rzania go potrzebna jest druga tablica bajtów, którą tworzymy dynami­cznie przed pętlą. Kompletny algorytm animacji może mieć postać:

byte *pTemp = new byte[nStar];
int x, y;
obliczXY(t = 0, x, y);
moveto(x, y);
while (!kbhit())
{
    getimage(x - R, y - R, x + R, y + R, pTemp);
    putimage(x - R, y - R, pStar, COPY_PUT);
    Sleep(PRZERWA);
    putimage(x - R, y - R, pTemp, COPY_PUT);
    obliczXY(t += dt, x, y);
    lineto(x, y);
}
delete[] pTemp;
delete[] pStar;

Informacjna o rozmiarze prostokątnego fragmentu ekranu kopiowa­nego do pamięci przez funkcję getimage jest zapisywana na początku obszaru pamięci (dwie liczby całkowite określa­jące długości boków), dlatego funkcja putimage przesy­łająca bajty w odwro­tnym kierunku wymaga podania tylko dwóch współrzę­dnych lewego górnego rogu docelowego prostokąta na ekranie. Ostatnim jej argu­mentem jest tryb kopiowania. Po przerwaniu animacji pamięć przydzie­lana dynamicznie obu tablicom jest zwalniana (oddawana do dyspo­zycji systemowi opera­cyjnemu).

Uwaga. Zwalnianie pamięci przydzielanej dynami­cznie, gdy przestaje być potrzebna, należy do zwyczajów dobrego programo­wania. Program, który tego nie robi, jest programem złym. Kiedy w wieloza­daniowym systemie opera­cyjnym program skończy działanie, nie zwalniając pamięci, jest ona zablo­kowana dla systemu i innych programów. Zjawisko to nosi nazwę wycieku pamięci (ang. memory leak). W nowszych środo­wiskach programi­stycznych, jak np. platforma .NET, progamista jest uwolniony od tego obowiązku, gdyż zawierają one specjalny mechanizm (odśmiecacz pamięci, ang. garbage collection) zajmujący się zwalnianiem pamięci.

Program w C++ (wersja 2)

Pełna wersja programu konsolowego w C++ prezentu­jącego powsta­wanie krzywych Lissajous za pomocą animacji opartej na technice kopio­wania fragmentów obrazu ma postać:

#include <iostream>
#include <cmath>
#include <graphics.h>

using namespace std;

const int PRZERWA = 10;     // Szybkość animacji (ms)
const double WSP = 0.04;    // Współczynnik skalowania
const int DX = 320;         // Współrzędna x środka okna
const int DY = 240;         // Współrzędna y środka okna
const int R = 7;            // Długość ramienia gwiazdki

double A1, A2, Omega1, Omega2, Fi1, Fi2;

void czytajDane(char Os, int Max, double &A, double &Omega, double &Fi)
{
    int faza;
    cout << "Drgania w kierunku osi " << Os
         << "\n--------------------------\n"
         << "  Amplituda (0 - " << Max << "): ";
    cin >> A;
    cout << "  Częstotliwość (> 0): ";
    cin >> Omega;
    cout << "  Faza początkowa (°): ";
    cin >> faza;
    Fi = M_PI * faza / 180;
}

void obliczXY(double t, int &x, int &y)
{
    x = int(A1 * cos(Omega1 * t + Fi1)) + DX;
    y = DY - int(A2 * cos(Omega2 * t + Fi2));
}

void gwiazdka(int n, int p, int q, double R, double r, double omega = 0)
{
    double alpha2 = M_PI / n, alpha = 2 * alpha2;
    moveto(R * cos(omega) + p, q - R * sin(omega));
    for (int k = 1; k <= n; k++)
    {
        double fi = k * alpha + omega, psi = fi - alpha2;
        lineto(r * cos(psi) + p, q - r * sin(psi));
        lineto(R * cos(fi) + p, q - R * sin(fi));
    }
    floodfill(p, q, getcolor());
}

int main()
{
    czytajDane('x', DX, A1, Omega1, Fi1);
    czytajDane('y', DY, A2, Omega2, Fi2);
    double t, dt = WSP / max(Omega1 + Omega2, 1.0);
    initwindow(2 * DX, 2 * DY, "Krzywa Lissajous");
    setbkcolor(WHITE);
    cleardevice();
    setcolor(BLUE);
    setfillstyle(SOLID_FILL, LIGHTRED);
    gwiazdka(5, R, R, R, R / 2, M_PI / 2);
    unsigned int nStar = imagesize(0, 0, 2 * R, 2 * R);
    byte *pStar = new byte[nStar], *pTemp = new byte[nStar];
    getimage(0, 0, 2 * R, 2 * R, pStar);
    cleardevice();
    int x, y;
    obliczXY(t = 0, x, y);
    moveto(x, y);
    while (!kbhit())
    {
        getimage(x - R, y - R, x + R, y + R, pTemp);
        putimage(x - R, y - R, pStar, COPY_PUT);
        Sleep(PRZERWA);
        putimage(x - R, y - R, pTemp, COPY_PUT);
        obliczXY(t += dt, x, y);
        lineto(x, y);
    }
    delete[] pTemp;
    delete[] pStar;
    closegraph();
    return 0;
}

A oto rysunek przedstawiający krzywą Lissajous wygene­rowaną przez program dla amplitud A1=250A2=230, częstotli­wości ω1=3,71ω2=2,87 oraz faz początko­wych φ1=φ2=0o:


Opracowanie przykładu: czerwiec 2019